Compare commits

...

10 Commits

Author SHA1 Message Date
Dario Tranchitella
7ad75e8216 chore(release)!: switch to goreleaser and migrating tag format (#1094)
Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>
2026-03-01 21:53:41 +01:00
Aleksei Sviridkin
69d62273c2 feat(deployment): make startup probe failure threshold configurable (#1086)
* feat(deployment): make startup probe failure threshold configurable

Add StartupProbeFailureThreshold field to TenantControlPlane CRD
DeploymentSpec, allowing users to configure how many consecutive
startup probe failures are tolerated before a container is considered
failed. The value is applied to all control plane components
(kube-apiserver, controller-manager, and scheduler).

Defaults to 3 (preserving current behavior). With PeriodSeconds=10,
the total startup timeout equals FailureThreshold * 10 seconds.
Setting this to 30 gives 5 minutes, which is useful for
resource-constrained environments.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>

* chore: regenerate CRD manifests for startupProbeFailureThreshold

Run `make manifests` to update Helm CRD files with the new
startupProbeFailureThreshold field in DeploymentSpec.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>

* feat(deployment): expand configurable probes to all probe types

Replace StartupProbeFailureThreshold with a full Probes config
supporting liveness, readiness, and startup probes with configurable
TimeoutSeconds, PeriodSeconds, and FailureThreshold parameters.
Use ptr.Deref for safe pointer dereferencing.

Ref: #471

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>

* chore: regenerate CRD manifests and API documentation

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>

* feat(deployment): add per-component probe overrides and expand ProbeSpec

Add cascading probe configuration: global defaults → per-component
overrides (apiServer, controllerManager, scheduler). Expand ProbeSpec
with InitialDelaySeconds and SuccessThreshold fields.

Ref: #471

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>

* chore: regenerate CRD manifests and API documentation

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>

---------

Signed-off-by: Aleksei Sviridkin <f@lex.la>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-24 16:20:06 +01:00
Patryk Rostkowski
b3ddfcda27 docs: add externalClusterReference with CAPI usage (#1088)
Signed-off-by: Patryk Rostkowski <patrostkowski@gmail.com>
2026-02-24 10:21:41 +01:00
dependabot[bot]
b13eca045c feat(deps): bump github.com/nats-io/nats.go from 1.48.0 to 1.49.0 (#1091)
Bumps [github.com/nats-io/nats.go](https://github.com/nats-io/nats.go) from 1.48.0 to 1.49.0.
- [Release notes](https://github.com/nats-io/nats.go/releases)
- [Commits](https://github.com/nats-io/nats.go/compare/v1.48.0...v1.49.0)

---
updated-dependencies:
- dependency-name: github.com/nats-io/nats.go
  dependency-version: 1.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-24 10:21:29 +01:00
Dario Tranchitella
309d9889e8 refactor!: datastore conditions (#1087)
* feat(api)!: readiness and conditions for datastore

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>

* docs: readiness and conditions for datastore

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>

* feat(controller): readiness and conditions for datastore

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>

* refactor: default datastore checked at reconciliation time

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>

* chore(helm): upgrading to v4.1.1

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>

---------

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>
2026-02-21 12:55:10 +01:00
Patryk Rostkowski
1d05f2bb4c fix: strip load balancer ports before updating TenantControlPlane status (#1082)
Signed-off-by: Patryk Rostkowski <patrostkowski@gmail.com>
2026-02-21 10:45:43 +01:00
Parth Yadav
cc8a8e14fd fix(gateway): allow explicit parentRefs for kube-apiserver TLSRoute (#1074)
Previously, when Kamaji created TLSRoutes for the kube-apiserver and
konnectivity, it automatically set the parentRefs `port` and `sectionName`,
overriding any user-provided values via TCP spec. This prevented users
from targeting specific Gateway listeners.

This change allows users to explicitly define parentRefs for the
kube-apiserver TLSRoute through `TCP.spec.controlPlane.gateway.parentRefs`.

The konnectivity TLSRoute behavior remains unchanged.

Signed-off-by: Parth Yadav <parth@coredge.io>
2026-02-18 18:45:23 +01:00
dependabot[bot]
13a3aa70f5 feat(deps): bump k8s.io/kubernetes in the k8s group (#1079)
Bumps the k8s group with 1 update: [k8s.io/kubernetes](https://github.com/kubernetes/kubernetes).


Updates `k8s.io/kubernetes` from 1.35.0 to 1.35.1
- [Release notes](https://github.com/kubernetes/kubernetes/releases)
- [Commits](https://github.com/kubernetes/kubernetes/compare/v1.35.0...v1.35.1)

---
updated-dependencies:
- dependency-name: k8s.io/kubernetes
  dependency-version: 1.35.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: k8s
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-18 18:45:06 +01:00
dependabot[bot]
49ea678047 feat(deps): bump the etcd group with 2 updates (#1078)
Bumps the etcd group with 2 updates: [go.etcd.io/etcd/api/v3](https://github.com/etcd-io/etcd) and [go.etcd.io/etcd/client/v3](https://github.com/etcd-io/etcd).


Updates `go.etcd.io/etcd/api/v3` from 3.6.7 to 3.6.8
- [Release notes](https://github.com/etcd-io/etcd/releases)
- [Commits](https://github.com/etcd-io/etcd/compare/v3.6.7...v3.6.8)

Updates `go.etcd.io/etcd/client/v3` from 3.6.7 to 3.6.8
- [Release notes](https://github.com/etcd-io/etcd/releases)
- [Commits](https://github.com/etcd-io/etcd/compare/v3.6.7...v3.6.8)

---
updated-dependencies:
- dependency-name: go.etcd.io/etcd/api/v3
  dependency-version: 3.6.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: etcd
- dependency-name: go.etcd.io/etcd/client/v3
  dependency-version: 3.6.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: etcd
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 14:23:13 +01:00
Dario Tranchitella
830d56dac1 chore(makefile): using only ipv4 for metallb in ci (#1076)
Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>
2026-02-13 11:48:29 +01:00
43 changed files with 3591 additions and 676 deletions

View File

@@ -1,10 +0,0 @@
This edge release can be pulled from Docker Hub as follows:
```
docker pull clastix/kamaji:$TAG
```
> As from the v1.0.0 release, CLASTIX no longer provides stable release artefacts.
>
> Stable release artefacts are offered on a subscription basis by CLASTIX, the main Kamaji project contributor.
> Learn more from CLASTIX's [Support](https://clastix.io/support/) section.

View File

@@ -2,17 +2,8 @@ name: Container image build
on:
push:
tags:
- edge-*
- v*
branches:
- master
workflow_dispatch:
inputs:
tag:
description: "Tag to build"
required: true
type: string
jobs:
ko:
@@ -21,17 +12,19 @@ jobs:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: "ko: install"
run: make ko
- name: "ko: login to quay.io container registry"
run: ./bin/ko login quay.io -u ${{ secrets.QUAY_IO_USERNAME }} -p ${{ secrets.QUAY_IO_TOKEN }}
- name: "ko: login to docker.io container registry"
run: ./bin/ko login docker.io -u ${{ secrets.DOCKER_IO_USERNAME }} -p ${{ secrets.DOCKER_IO_TOKEN }}
- name: "ko: build and push tag"
run: make VERSION=${{ github.event.inputs.tag }} KO_LOCAL=false KO_PUSH=true build
if: github.event_name == 'workflow_dispatch'
- name: "ko: build and push latest"
run: make VERSION=latest KO_LOCAL=false KO_PUSH=true build

View File

@@ -15,8 +15,9 @@ jobs:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: generating date metadata
id: date
- name: "tag: compute"
id: git
run: |
CURRENT_DATE=$(date -u +'%Y-%m-%d')
YY=$(date -u +'%y')
@@ -24,52 +25,36 @@ jobs:
FIRST_OF_MONTH=$(date -u -d "$CURRENT_DATE" +%Y-%m-01)
WEEK_NUM=$(( (($(date -u +%s) - $(date -u -d "$FIRST_OF_MONTH" +%s)) / 86400 + $(date -u -d "$FIRST_OF_MONTH" +%u) - 1) / 7 + 1 ))
echo "yy=$YY" >> $GITHUB_OUTPUT
echo "month=$M" >> $GITHUB_OUTPUT
echo "week=$WEEK_NUM" >> $GITHUB_OUTPUT
echo "date=$CURRENT_DATE" >> $GITHUB_OUTPUT
- name: generating tag metadata
id: tag
run: |
TAG="edge-${{ steps.date.outputs.yy }}.${{ steps.date.outputs.month }}.${{ steps.date.outputs.week }}"
TAG="$YY.$M.$WEEK_NUM-edge"
echo "tag=$TAG" >> $GITHUB_OUTPUT
- name: generate release notes from template
- name: "tag: push"
run: |
export TAG="${{ steps.tag.outputs.tag }}"
envsubst < .github/release-template.md > release-notes.md
- name: generate release notes from template
run: |
export TAG="${{ steps.tag.outputs.tag }}"
envsubst < .github/release-template.md > release-notes-header.md
- name: generate GitHub release notes
git tag ${{ steps.git.outputs.tag }}
git push origin ${{ steps.git.outputs.tag }}
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: "deps: installing ko"
run: make ko
- name: "ko: login to quay.io container registry"
run: ./bin/ko login quay.io -u ${{ secrets.QUAY_IO_USERNAME }} -p ${{ secrets.QUAY_IO_TOKEN }}
- name: "ko: login to docker.io container registry"
run: ./bin/ko login docker.io -u ${{ secrets.DOCKER_IO_USERNAME }} -p ${{ secrets.DOCKER_IO_TOKEN }}
- name: "path: expanding with local binaries"
run: echo "${{ github.workspace }}/bin" >> $GITHUB_PATH
- name: "goreleaser: release"
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: "~> v2"
args: release --clean
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release --repo "$GITHUB_REPOSITORY" \
create "${{ steps.tag.outputs.tag }}" \
--generate-notes \
--draft \
--title "temp" \
--notes "temp" > /dev/null || true
gh release view "${{ steps.tag.outputs.tag }}" \
--json body --jq .body > auto-notes.md
gh release delete "${{ steps.tag.outputs.tag }}" --yes || true
- name: combine notes
run: |
cat release-notes-header.md auto-notes.md > release-notes.md
- name: create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "${{ steps.tag.outputs.tag }}" \
--title "${{ steps.tag.outputs.tag }}" \
--notes-file release-notes.md
- name: trigger container build workflow
env:
GH_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
run: |
gh workflow run "Container image build" \
--ref master \
-f tag="${{ steps.tag.outputs.tag }}"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@@ -38,3 +38,4 @@ bin
!deploy/kine/mysql/server-csr.json
!deploy/kine/nats/server-csr.json
charts/kamaji/charts
dist

92
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,92 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2
project_name: kamaji
builds:
- id: kamaji
main: .
binary: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}"
env:
- CGO_ENABLED=0
flags:
- -trimpath
mod_timestamp: '{{ .CommitTimestamp }}'
ldflags:
- "-X github.com/clastix/kamaji/internal.GitCommit={{.Commit}}"
- "-X github.com/clastix/kamaji/internal.GitTag={{.Tag}}"
- "-X github.com/clastix/kamaji/internal.GitDirty={{ if eq .GitTreeState \"dirty\" }}.dev{{ end }}"
- "-X github.com/clastix/kamaji/internal.BuildTime={{.Date}}"
- "-X github.com/clastix/kamaji/internal.GitRepo={{ .GitURL }}"
goos:
- linux
goarch:
- amd64
- arm
- arm64
kos:
- repositories:
- docker.io/clastix/kamaji
- quay.io/clastix/kamaji
tags:
- "{{ .Tag }}"
bare: true
preserve_import_paths: false
platforms:
- linux/amd64
- linux/arm64
- linux/arm
release:
prerelease: auto
footer: |
**Container Images**
```
docker pull clastix/{{ .ProjectName }}:{{ .Tag }}
```
> This is an **edge release** and is intended for testing and evaluation purposes only.
> It may include experimental features and does not provide the stability guarantees of a production-ready build.
>
> **Stable release artefacts** are available on a subscription basis from CLASTIX,
> the primary contributor to the Kamaji project.
>
> For production-grade releases and enterprise support,
> please refer to CLASTIX's [Support](https://clastix.io/support/) offerings.
**Full Changelog**: https://github.com/clastix/{{ .ProjectName }}/compare/{{ .PreviousTag }}...{{ .Tag }}
changelog:
sort: asc
use: github
filters:
exclude:
- 'merge conflict'
- Merge pull request
- Merge remote-tracking branch
- Merge branch
groups:
- title: '🛠 Dependency updates'
regexp: '^.*?(feat|fix)\(deps\)!?:.+$'
order: 300
- title: '✨ New Features'
regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$'
order: 100
- title: '🐛 Bug fixes'
regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$'
order: 200
- title: '📖 Documentation updates'
regexp: ^.*?docs(\([[:word:]]+\))??!?:.+$
order: 400
- title: '🛡️ Security updates'
regexp: ^.*?(sec)(\([[:word:]]+\))??!?:.+$
order: 500
- title: '🚀 Build process updates'
regexp: ^.*?(build|ci)(\([[:word:]]+\))??!?:.+$
order: 600
- title: '📦 Other work'
order: 9999
checksum:
name_template: "checksums.txt"

View File

@@ -47,6 +47,9 @@ GOLANGCI_LINT ?= $(LOCALBIN)/golangci-lint
HELM ?= $(LOCALBIN)/helm
KIND ?= $(LOCALBIN)/kind
KO ?= $(LOCALBIN)/ko
GORELEASER ?= $(LOCALBIN)/goreleaser
COSIGN ?= $(LOCALBIN)/cosign
SYFT ?= $(LOCALBIN)/syft
YQ ?= $(LOCALBIN)/yq
ENVTEST ?= $(LOCALBIN)/setup-envtest
@@ -68,8 +71,34 @@ all: build
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Documentation
.PHONY: docs
docs: ## Serve documentation locally with Docker.
docker run --rm -it \
-p 8000:8000 \
-v "$${PWD}/docs":/docs:Z \
-w /docs \
squidfunk/mkdocs-material \
serve -a 0.0.0.0:8000
##@ Binary
.PHONY: cosign
cosign: $(COSIGN) ## Download cosign locally if necessary.
$(COSIGN): $(LOCALBIN)
test -s $(LOCALBIN)/cosign || GOBIN=$(LOCALBIN) go install github.com/sigstore/cosign/v3/cmd/cosign@v3.0.5
.PHONY: syft
syft: $(SYFT) ## Download syft locally if necessary.
$(SYFT): $(LOCALBIN)
test -s $(LOCALBIN)/syft || GOBIN=$(LOCALBIN) go install github.com/anchore/syft/cmd/syft@v1.42.1
.PHONY: goreleaser
goreleaser: $(GORELEASER) ## Download goreleaser locally if necessary.
$(GORELEASER): $(LOCALBIN)
test -s $(LOCALBIN)/goreleaser || GOBIN=$(LOCALBIN) go install github.com/goreleaser/goreleaser/v2@v2.14.1
.PHONY: ko
ko: $(KO) ## Download ko locally if necessary.
$(KO): $(LOCALBIN)
@@ -83,7 +112,7 @@ $(YQ): $(LOCALBIN)
.PHONY: helm
helm: $(HELM) ## Download helm locally if necessary.
$(HELM): $(LOCALBIN)
test -s $(LOCALBIN)/helm || GOBIN=$(LOCALBIN) CGO_ENABLED=0 go install -ldflags="-s -w" helm.sh/helm/v3/cmd/helm@v3.9.0
test -s $(LOCALBIN)/helm || GOBIN=$(LOCALBIN) CGO_ENABLED=0 go install -ldflags="-s -w" helm.sh/helm/v4/cmd/helm@v4.1.1
.PHONY: ginkgo
ginkgo: $(GINKGO) ## Download ginkgo locally if necessary.
@@ -236,16 +265,29 @@ metallb:
kubectl apply -f "https://raw.githubusercontent.com/metallb/metallb/$$(curl "https://api.github.com/repos/metallb/metallb/releases/latest" | jq -r ".tag_name")/config/manifests/metallb-native.yaml"
kubectl wait pods -n metallb-system -l app=metallb,component=controller --for=condition=Ready --timeout=10m
kubectl wait pods -n metallb-system -l app=metallb,component=speaker --for=condition=Ready --timeout=2m
cat hack/metallb.yaml | sed -E "s|172.19|$$(docker network inspect -f '{{range .IPAM.Config}}{{.Gateway}}{{end}}' kind | sed -E 's|^([0-9]+\.[0-9]+)\..*$$|\1|g')|g" | kubectl apply -f -
@IPV4_PREFIX=$$(docker network inspect kind \
-f '{{range .IPAM.Config}}{{println .Subnet " " .Gateway}}{{end}}' \
| grep -v ':' \
| awk '{print $$2}' \
| sed -E 's|^([0-9]+\.[0-9]+)\..*$$|\1|'); \
sed -E "s|172\.19|$$IPV4_PREFIX|g" hack/metallb.yaml | kubectl apply -f -
cert-manager:
$(HELM) repo add jetstack https://charts.jetstack.io
$(HELM) upgrade --install cert-manager jetstack/cert-manager --namespace certmanager-system --create-namespace --set "installCRDs=true"
gateway-api:
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
# Required for the TLSRoutes. Experimentals.
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/experimental-install.yaml
kubectl apply \
--server-side \
--force-conflicts \
--field-manager=helm \
-f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
# Required for the TLSRoutes. Experimentals.
kubectl apply \
--server-side \
--force-conflicts \
--field-manager=helm \
-f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/experimental-install.yaml
kubectl wait --for=condition=Established crd/gateways.gateway.networking.k8s.io --timeout=60s
envoy-gateway: gateway-api helm ## Install Envoy Gateway for Gateway API tests.

View File

@@ -32,6 +32,7 @@ type Endpoints []string
// +kubebuilder:validation:XValidation:rule="(self.driver != \"etcd\" && has(self.basicAuth)) ? ((has(self.basicAuth.username.secretReference) || has(self.basicAuth.username.content))) : true", message="When driver is not etcd and basicAuth exists, username must have secretReference or content"
// +kubebuilder:validation:XValidation:rule="(self.driver != \"etcd\" && has(self.basicAuth)) ? ((has(self.basicAuth.password.secretReference) || has(self.basicAuth.password.content))) : true", message="When driver is not etcd and basicAuth exists, password must have secretReference or content"
// +kubebuilder:validation:XValidation:rule="(self.driver != \"etcd\") ? (has(self.tlsConfig) || has(self.basicAuth)) : true", message="When driver is not etcd, either tlsConfig or basicAuth must be provided"
// +kubebuilder:validation:XValidation:rule="oldSelf == null || self.driver == oldSelf.driver", message="driver is immutable and cannot be changed after creation"
type DataStoreSpec struct {
// The driver to use to connect to the shared datastore.
Driver Driver `json:"driver"`
@@ -88,6 +89,13 @@ type SecretReference struct {
KeyPath secretReferKeyPath `json:"keyPath"`
}
const (
DataStoreTCPFinalizer = "kamaji.clastix.io/TenantControlPlane"
DataStoreConditionValidType = "kamaji.clastix.io/DataStoreValidation"
DataStoreConditionAllowedDeletionType = "kamaji.clastix.io/DataStoreAllowedDeletion"
)
// DataStoreStatus defines the observed state of DataStore.
type DataStoreStatus struct {
// ObservedGeneration represents the .metadata.generation that was last reconciled.
@@ -95,12 +103,18 @@ type DataStoreStatus struct {
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// List of the Tenant Control Planes, namespaced named, using this data store.
UsedBy []string `json:"usedBy,omitempty"`
// Conditions contains the validation conditions for the given Datastore.
Conditions []metav1.Condition `json:"conditions,omitempty"`
// Ready returns if the DataStore is accepted and ready to get used:
// Kamaji will ensure certificates are available, or correctly referenced.
Ready bool `json:"ready"`
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:resource:scope=Cluster
//+kubebuilder:printcolumn:name="Driver",type="string",JSONPath=".spec.driver",description="Kamaji data store driver"
//+kubebuilder:printcolumn:name="Ready",type="boolean",JSONPath=".status.ready",description="DataStore validated and ready for use"
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
//+kubebuilder:metadata:annotations={"cert-manager.io/inject-ca-from=kamaji-system/kamaji-serving-cert"}

View File

@@ -155,7 +155,6 @@ type IngressSpec struct {
}
// GatewaySpec defines the options for the Gateway which will expose API Server of the Tenant Control Plane.
// +kubebuilder:validation:XValidation:rule="!has(self.parentRefs) || size(self.parentRefs) == 0 || self.parentRefs.all(ref, !has(ref.port) && !has(ref.sectionName))",message="parentRefs must not specify port or sectionName, these are set automatically by Kamaji"
type GatewaySpec struct {
// AdditionalMetadata to add Labels and Annotations support.
AdditionalMetadata AdditionalMetadata `json:"additionalMetadata,omitempty"`
@@ -174,6 +173,54 @@ type ControlPlaneComponentsResources struct {
Kine *corev1.ResourceRequirements `json:"kine,omitempty"`
}
// ProbeSpec defines configurable parameters for a Kubernetes probe.
type ProbeSpec struct {
// InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
//+kubebuilder:validation:Minimum=0
InitialDelaySeconds *int32 `json:"initialDelaySeconds,omitempty"`
// TimeoutSeconds is the number of seconds after which the probe times out.
//+kubebuilder:validation:Minimum=1
TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"`
// PeriodSeconds is how often (in seconds) to perform the probe.
//+kubebuilder:validation:Minimum=1
PeriodSeconds *int32 `json:"periodSeconds,omitempty"`
// SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
// Must be 1 for liveness and startup probes.
//+kubebuilder:validation:Minimum=1
SuccessThreshold *int32 `json:"successThreshold,omitempty"`
// FailureThreshold is the consecutive failure count required to consider the probe failed.
//+kubebuilder:validation:Minimum=1
FailureThreshold *int32 `json:"failureThreshold,omitempty"`
}
// ProbeSet defines per-probe-type configuration.
type ProbeSet struct {
// Liveness defines parameters for the liveness probe.
Liveness *ProbeSpec `json:"liveness,omitempty"`
// Readiness defines parameters for the readiness probe.
Readiness *ProbeSpec `json:"readiness,omitempty"`
// Startup defines parameters for the startup probe.
Startup *ProbeSpec `json:"startup,omitempty"`
}
// ControlPlaneProbes defines probe configuration for Control Plane components.
// Global probe settings (Liveness, Readiness, Startup) apply to all components.
// Per-component settings (APIServer, ControllerManager, Scheduler) override global settings.
type ControlPlaneProbes struct {
// Liveness defines default parameters for liveness probes of all Control Plane components.
Liveness *ProbeSpec `json:"liveness,omitempty"`
// Readiness defines default parameters for the readiness probe of kube-apiserver.
Readiness *ProbeSpec `json:"readiness,omitempty"`
// Startup defines default parameters for startup probes of all Control Plane components.
Startup *ProbeSpec `json:"startup,omitempty"`
// APIServer defines probe overrides for kube-apiserver, taking precedence over global probe settings.
APIServer *ProbeSet `json:"apiServer,omitempty"`
// ControllerManager defines probe overrides for kube-controller-manager, taking precedence over global probe settings.
ControllerManager *ProbeSet `json:"controllerManager,omitempty"`
// Scheduler defines probe overrides for kube-scheduler, taking precedence over global probe settings.
Scheduler *ProbeSet `json:"scheduler,omitempty"`
}
type DeploymentSpec struct {
// RegistrySettings allows to override the default images for the given Tenant Control Plane instance.
// It could be used to point to a different container registry rather than the public one.
@@ -225,6 +272,10 @@ type DeploymentSpec struct {
// AdditionalVolumeMounts allows to mount an additional volume into each component of the Control Plane
// (kube-apiserver, controller-manager, and scheduler).
AdditionalVolumeMounts *AdditionalVolumeMounts `json:"additionalVolumeMounts,omitempty"`
// Probes defines the probe configuration for the Control Plane components
// (kube-apiserver, controller-manager, and scheduler).
// Override TimeoutSeconds, PeriodSeconds, and FailureThreshold for resource-constrained environments.
Probes *ControlPlaneProbes `json:"probes,omitempty"`
//+kubebuilder:default="default"
// ServiceAccountName allows to specify the service account to be mounted to the pods of the Control plane deployment
ServiceAccountName string `json:"serviceAccountName,omitempty"`

View File

@@ -449,6 +449,51 @@ func (in *ControlPlaneExtraArgs) DeepCopy() *ControlPlaneExtraArgs {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ControlPlaneProbes) DeepCopyInto(out *ControlPlaneProbes) {
*out = *in
if in.Liveness != nil {
in, out := &in.Liveness, &out.Liveness
*out = new(ProbeSpec)
(*in).DeepCopyInto(*out)
}
if in.Readiness != nil {
in, out := &in.Readiness, &out.Readiness
*out = new(ProbeSpec)
(*in).DeepCopyInto(*out)
}
if in.Startup != nil {
in, out := &in.Startup, &out.Startup
*out = new(ProbeSpec)
(*in).DeepCopyInto(*out)
}
if in.APIServer != nil {
in, out := &in.APIServer, &out.APIServer
*out = new(ProbeSet)
(*in).DeepCopyInto(*out)
}
if in.ControllerManager != nil {
in, out := &in.ControllerManager, &out.ControllerManager
*out = new(ProbeSet)
(*in).DeepCopyInto(*out)
}
if in.Scheduler != nil {
in, out := &in.Scheduler, &out.Scheduler
*out = new(ProbeSet)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlaneProbes.
func (in *ControlPlaneProbes) DeepCopy() *ControlPlaneProbes {
if in == nil {
return nil
}
out := new(ControlPlaneProbes)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DataStore) DeepCopyInto(out *DataStore) {
*out = *in
@@ -709,6 +754,11 @@ func (in *DeploymentSpec) DeepCopyInto(out *DeploymentSpec) {
*out = new(AdditionalVolumeMounts)
(*in).DeepCopyInto(*out)
}
if in.Probes != nil {
in, out := &in.Probes, &out.Probes
*out = new(ControlPlaneProbes)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentSpec.
@@ -1493,6 +1543,76 @@ func (in *Permissions) DeepCopy() *Permissions {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProbeSet) DeepCopyInto(out *ProbeSet) {
*out = *in
if in.Liveness != nil {
in, out := &in.Liveness, &out.Liveness
*out = new(ProbeSpec)
(*in).DeepCopyInto(*out)
}
if in.Readiness != nil {
in, out := &in.Readiness, &out.Readiness
*out = new(ProbeSpec)
(*in).DeepCopyInto(*out)
}
if in.Startup != nil {
in, out := &in.Startup, &out.Startup
*out = new(ProbeSpec)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProbeSet.
func (in *ProbeSet) DeepCopy() *ProbeSet {
if in == nil {
return nil
}
out := new(ProbeSet)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProbeSpec) DeepCopyInto(out *ProbeSpec) {
*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 ProbeSpec.
func (in *ProbeSpec) DeepCopy() *ProbeSpec {
if in == nil {
return nil
}
out := new(ProbeSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PublicKeyPrivateKeyPairStatus) DeepCopyInto(out *PublicKeyPrivateKeyPairStatus) {
*out = *in

View File

@@ -11,6 +11,10 @@ versions:
jsonPath: .spec.driver
name: Driver
type: string
- description: DataStore validated and ready for use
jsonPath: .status.ready
name: Ready
type: boolean
- description: Age
jsonPath: .metadata.creationTimestamp
name: Age
@@ -272,18 +276,83 @@ versions:
rule: '(self.driver != "etcd" && has(self.basicAuth)) ? ((has(self.basicAuth.password.secretReference) || has(self.basicAuth.password.content))) : true'
- message: When driver is not etcd, either tlsConfig or basicAuth must be provided
rule: '(self.driver != "etcd") ? (has(self.tlsConfig) || has(self.basicAuth)) : true'
- message: driver is immutable and cannot be changed after creation
rule: oldSelf == null || self.driver == oldSelf.driver
status:
description: DataStoreStatus defines the observed state of DataStore.
properties:
conditions:
description: Conditions contains the validation conditions for the given Datastore.
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
type: array
observedGeneration:
description: ObservedGeneration represents the .metadata.generation that was last reconciled.
format: int64
type: integer
ready:
description: |-
Ready returns if the DataStore is accepted and ready to get used:
Kamaji will ensure certificates are available, or correctly referenced.
type: boolean
usedBy:
description: List of the Tenant Control Planes, namespaced named, using this data store.
items:
type: string
type: array
required:
- ready
type: object
type: object
served: true

View File

@@ -6165,6 +6165,397 @@ versions:
type: string
type: object
type: object
probes:
description: |-
Probes defines the probe configuration for the Control Plane components
(kube-apiserver, controller-manager, and scheduler).
Override TimeoutSeconds, PeriodSeconds, and FailureThreshold for resource-constrained environments.
properties:
apiServer:
description: APIServer defines probe overrides for kube-apiserver, taking precedence over global probe settings.
properties:
liveness:
description: Liveness defines parameters for the liveness probe.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
readiness:
description: Readiness defines parameters for the readiness probe.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
startup:
description: Startup defines parameters for the startup probe.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
type: object
controllerManager:
description: ControllerManager defines probe overrides for kube-controller-manager, taking precedence over global probe settings.
properties:
liveness:
description: Liveness defines parameters for the liveness probe.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
readiness:
description: Readiness defines parameters for the readiness probe.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
startup:
description: Startup defines parameters for the startup probe.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
type: object
liveness:
description: Liveness defines default parameters for liveness probes of all Control Plane components.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
readiness:
description: Readiness defines default parameters for the readiness probe of kube-apiserver.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
scheduler:
description: Scheduler defines probe overrides for kube-scheduler, taking precedence over global probe settings.
properties:
liveness:
description: Liveness defines parameters for the liveness probe.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
readiness:
description: Readiness defines parameters for the readiness probe.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
startup:
description: Startup defines parameters for the startup probe.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
type: object
startup:
description: Startup defines default parameters for startup probes of all Control Plane components.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
type: object
registrySettings:
default:
apiServerImage: kube-apiserver
@@ -6896,9 +7287,6 @@ versions:
type: object
type: array
type: object
x-kubernetes-validations:
- message: parentRefs must not specify port or sectionName, these are set automatically by Kamaji
rule: '!has(self.parentRefs) || size(self.parentRefs) == 0 || self.parentRefs.all(ref, !has(ref.port) && !has(ref.sectionName))'
ingress:
description: Defining the options for an Optional Ingress which will expose API Server of the Tenant Control Plane
properties:

View File

@@ -19,46 +19,6 @@
resources:
- tenantcontrolplanes
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: '{{ include "kamaji.webhookServiceName" . }}'
namespace: '{{ .Release.Namespace }}'
path: /validate-kamaji-clastix-io-v1alpha1-datastore
failurePolicy: Fail
name: vdatastore.kb.io
rules:
- apiGroups:
- kamaji.clastix.io
apiVersions:
- v1alpha1
operations:
- CREATE
- UPDATE
- DELETE
resources:
- datastores
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: '{{ include "kamaji.webhookServiceName" . }}'
namespace: '{{ .Release.Namespace }}'
path: /validate--v1-secret
failurePolicy: Ignore
name: vdatastoresecrets.kb.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- DELETE
resources:
- secrets
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:

View File

@@ -20,6 +20,10 @@ spec:
jsonPath: .spec.driver
name: Driver
type: string
- description: DataStore validated and ready for use
jsonPath: .status.ready
name: Ready
type: boolean
- description: Age
jsonPath: .metadata.creationTimestamp
name: Age
@@ -281,18 +285,83 @@ spec:
rule: '(self.driver != "etcd" && has(self.basicAuth)) ? ((has(self.basicAuth.password.secretReference) || has(self.basicAuth.password.content))) : true'
- message: When driver is not etcd, either tlsConfig or basicAuth must be provided
rule: '(self.driver != "etcd") ? (has(self.tlsConfig) || has(self.basicAuth)) : true'
- message: driver is immutable and cannot be changed after creation
rule: oldSelf == null || self.driver == oldSelf.driver
status:
description: DataStoreStatus defines the observed state of DataStore.
properties:
conditions:
description: Conditions contains the validation conditions for the given Datastore.
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
type: array
observedGeneration:
description: ObservedGeneration represents the .metadata.generation that was last reconciled.
format: int64
type: integer
ready:
description: |-
Ready returns if the DataStore is accepted and ready to get used:
Kamaji will ensure certificates are available, or correctly referenced.
type: boolean
usedBy:
description: List of the Tenant Control Planes, namespaced named, using this data store.
items:
type: string
type: array
required:
- ready
type: object
type: object
served: true

View File

@@ -6173,6 +6173,397 @@ spec:
type: string
type: object
type: object
probes:
description: |-
Probes defines the probe configuration for the Control Plane components
(kube-apiserver, controller-manager, and scheduler).
Override TimeoutSeconds, PeriodSeconds, and FailureThreshold for resource-constrained environments.
properties:
apiServer:
description: APIServer defines probe overrides for kube-apiserver, taking precedence over global probe settings.
properties:
liveness:
description: Liveness defines parameters for the liveness probe.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
readiness:
description: Readiness defines parameters for the readiness probe.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
startup:
description: Startup defines parameters for the startup probe.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
type: object
controllerManager:
description: ControllerManager defines probe overrides for kube-controller-manager, taking precedence over global probe settings.
properties:
liveness:
description: Liveness defines parameters for the liveness probe.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
readiness:
description: Readiness defines parameters for the readiness probe.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
startup:
description: Startup defines parameters for the startup probe.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
type: object
liveness:
description: Liveness defines default parameters for liveness probes of all Control Plane components.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
readiness:
description: Readiness defines default parameters for the readiness probe of kube-apiserver.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
scheduler:
description: Scheduler defines probe overrides for kube-scheduler, taking precedence over global probe settings.
properties:
liveness:
description: Liveness defines parameters for the liveness probe.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
readiness:
description: Readiness defines parameters for the readiness probe.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
startup:
description: Startup defines parameters for the startup probe.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
type: object
startup:
description: Startup defines default parameters for startup probes of all Control Plane components.
properties:
failureThreshold:
description: FailureThreshold is the consecutive failure count required to consider the probe failed.
format: int32
minimum: 1
type: integer
initialDelaySeconds:
description: InitialDelaySeconds is the number of seconds after the container has started before the probe is initiated.
format: int32
minimum: 0
type: integer
periodSeconds:
description: PeriodSeconds is how often (in seconds) to perform the probe.
format: int32
minimum: 1
type: integer
successThreshold:
description: |-
SuccessThreshold is the minimum consecutive successes for the probe to be considered successful.
Must be 1 for liveness and startup probes.
format: int32
minimum: 1
type: integer
timeoutSeconds:
description: TimeoutSeconds is the number of seconds after which the probe times out.
format: int32
minimum: 1
type: integer
type: object
type: object
registrySettings:
default:
apiServerImage: kube-apiserver
@@ -6904,9 +7295,6 @@ spec:
type: object
type: array
type: object
x-kubernetes-validations:
- message: parentRefs must not specify port or sectionName, these are set automatically by Kamaji
rule: '!has(self.parentRefs) || size(self.parentRefs) == 0 || self.parentRefs.all(ref, !has(ref.port) && !has(ref.sectionName))'
ingress:
description: Defining the options for an Optional Ingress which will expose API Server of the Tenant Control Plane
properties:

View File

@@ -4,7 +4,6 @@
package manager
import (
"context"
"flag"
"fmt"
"io"
@@ -34,7 +33,6 @@ import (
"github.com/clastix/kamaji/controllers/soot"
"github.com/clastix/kamaji/internal"
"github.com/clastix/kamaji/internal/builders/controlplane"
datastoreutils "github.com/clastix/kamaji/internal/datastore/utils"
"github.com/clastix/kamaji/internal/utilities"
"github.com/clastix/kamaji/internal/webhook"
"github.com/clastix/kamaji/internal/webhook/handlers"
@@ -87,10 +85,6 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
return fmt.Errorf("unable to read webhook CA: %w", err)
}
if err = datastoreutils.CheckExists(context.Background(), scheme, datastore); err != nil {
return err
}
if controllerReconcileTimeout.Seconds() == 0 {
return fmt.Errorf("the controller reconcile timeout must be greater than zero")
}
@@ -276,12 +270,6 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
KubernetesVersion: k8sVersion,
},
},
routes.DataStoreValidate{}: {
handlers.DataStoreValidation{Client: mgr.GetClient()},
},
routes.DataStoreSecrets{}: {
handlers.DataStoreSecretValidation{Client: mgr.GetClient()},
},
})
if err != nil {
setupLog.Error(err, "unable to create webhook")

View File

@@ -0,0 +1,154 @@
---
apiVersion: bootstrap.cluster.x-k8s.io/v1beta2
kind: KubeadmConfigTemplate
metadata:
name: worker-external
namespace: default
spec:
template:
spec:
users:
joinConfiguration:
nodeRegistration:
kubeletExtraArgs:
- name: feature-gates
value: "KubeletCrashLoopBackOffMax=true,KubeletEnsureSecretPulledImages=true"
---
apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
kind: KubevirtMachineTemplate
metadata:
name: worker-external
namespace: default
spec:
template:
spec:
virtualMachineBootstrapCheck:
checkStrategy: ssh
virtualMachineTemplate:
metadata:
namespace: default
spec:
runStrategy: Always
template:
spec:
dnsPolicy: None
dnsConfig:
nameservers:
- 1.1.1.1
- 8.8.8.8
searches: []
options:
- name: ndots
value: "1"
domain:
cpu:
cores: 2
devices:
interfaces:
- name: default
masquerade: {}
disks:
- disk:
bus: virtio
name: containervolume
networkInterfaceMultiqueue: true
memory:
guest: 4Gi
evictionStrategy: External
networks:
- name: default
pod: {}
volumes:
- containerDisk:
image: quay.io/capk/ubuntu-2404-container-disk:v1.34.1
name: containervolume
---
apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
kind: KubevirtClusterTemplate
metadata:
name: kubevirt-external
namespace: default
spec:
template:
metadata:
annotations:
cluster.x-k8s.io/managed-by: kamaji
spec:
controlPlaneServiceTemplate:
spec:
type: LoadBalancer
---
apiVersion: controlplane.cluster.x-k8s.io/v1alpha1
kind: KamajiControlPlaneTemplate
metadata:
name: kamaji-controlplane-external
namespace: default
spec:
template:
spec:
addons:
coreDNS: {}
konnectivity: {}
kubeProxy: {}
dataStoreName: "default" # reference to DataStore present on external cluster
deployment:
externalClusterReference:
deploymentNamespace: kamaji-tenants
kubeconfigSecretName: kind-external-kubeconfig
kubeconfigSecretKey: kubeconfig
network:
serviceType: LoadBalancer
kubelet:
cgroupfs: systemd
preferredAddressTypes:
- InternalIP
registry: "registry.k8s.io"
---
apiVersion: cluster.x-k8s.io/v1beta2
kind: ClusterClass
metadata:
name: kubevirt-kamaji-kubeadm-external
namespace: default
spec:
controlPlane:
templateRef:
apiVersion: controlplane.cluster.x-k8s.io/v1alpha1
kind: KamajiControlPlaneTemplate
name: kamaji-controlplane-external
infrastructure:
templateRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
kind: KubevirtClusterTemplate
name: kubevirt-external
workers:
machineDeployments:
- class: small
bootstrap:
templateRef:
apiVersion: bootstrap.cluster.x-k8s.io/v1beta2
kind: KubeadmConfigTemplate
name: worker-external
infrastructure:
templateRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
kind: KubevirtMachineTemplate
name: worker-external
---
apiVersion: cluster.x-k8s.io/v1beta2
kind: Cluster
metadata:
name: demo-external
namespace: default
spec:
topology:
classRef:
name: kubevirt-kamaji-kubeadm-external
namespace: default
version: v1.34.0
controlPlane:
replicas: 1
workers:
machineDeployments:
- class: small
name: md-small
replicas: 1

View File

@@ -7,19 +7,20 @@ import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
k8stypes "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/util/retry"
"k8s.io/client-go/util/workqueue"
controllerruntime "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"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
@@ -38,19 +39,21 @@ type DataStore struct {
//+kubebuilder:rbac:groups=kamaji.clastix.io,resources=datastores/status,verbs=get;update;patch
func (r *DataStore) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
var err error
logger := log.FromContext(ctx)
var ds kamajiv1alpha1.DataStore
if err := r.Client.Get(ctx, request.NamespacedName, &ds); err != nil {
if k8serrors.IsNotFound(err) {
if dsErr := r.Client.Get(ctx, request.NamespacedName, &ds); dsErr != nil {
if k8serrors.IsNotFound(dsErr) {
logger.Info("resource may have been deleted, skipping")
return reconcile.Result{}, nil
}
logger.Error(err, "cannot retrieve the required resource")
logger.Error(dsErr, "cannot retrieve the required resource")
return reconcile.Result{}, err
return reconcile.Result{}, dsErr
}
if utils.IsPaused(&ds) {
@@ -59,45 +62,179 @@ func (r *DataStore) Reconcile(ctx context.Context, request reconcile.Request) (r
return reconcile.Result{}, nil
}
if ds.GetDeletionTimestamp() == nil && !controllerutil.ContainsFinalizer(&ds, kamajiv1alpha1.DataStoreTCPFinalizer) {
logger.Info("missing finalizer, adding it")
ds.SetFinalizers(append(ds.GetFinalizers(), kamajiv1alpha1.DataStoreTCPFinalizer))
if uErr := r.Client.Update(ctx, &ds); uErr != nil {
return reconcile.Result{}, uErr
}
return reconcile.Result{}, nil
}
// Extracting the list of TenantControlPlane objects referenced by the given DataStore:
// this data is used to reference these in the Status, as well as propagating changes
// that would be required, such as changing TLS Configuration, or Basic Auth.
var tcpList kamajiv1alpha1.TenantControlPlaneList
updateErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
if lErr := r.Client.List(ctx, &tcpList, client.MatchingFieldsSelector{
Selector: fields.OneTermEqualSelector(kamajiv1alpha1.TenantControlPlaneUsedDataStoreKey, ds.GetName()),
}); lErr != nil {
return fmt.Errorf("cannot retrieve list of the Tenant Control Plane using the following instance: %w", lErr)
}
// Updating the status with the list of Tenant Control Plane using the following Data Source
tcpSets := sets.NewString()
for _, tcp := range tcpList.Items {
tcpSets.Insert(getNamespacedName(tcp.GetNamespace(), tcp.GetName()).String())
if lErr := r.Client.List(ctx, &tcpList, client.MatchingFieldsSelector{
Selector: fields.OneTermEqualSelector(kamajiv1alpha1.TenantControlPlaneUsedDataStoreKey, ds.GetName()),
}); lErr != nil {
return reconcile.Result{}, fmt.Errorf("cannot retrieve list of the Tenant Control Plane using the following instance: %w", lErr)
}
tcpSets := sets.NewString()
for _, tcp := range tcpList.Items {
tcpSets.Insert(getNamespacedName(tcp.GetNamespace(), tcp.GetName()).String())
}
ds.Status.UsedBy = tcpSets.List()
// Performing the status update only at the end of the reconciliation loop:
// this is performed in defer to avoid duplication of code,
// and triggering a reconciliation of depending on TenantControlPlanes only if the update was successful.
defer func() {
if meta.IsStatusConditionTrue(ds.Status.Conditions, kamajiv1alpha1.DataStoreConditionAllowedDeletionType) {
logger.Info("removing finalizer upon true condition")
controllerutil.RemoveFinalizer(&ds, kamajiv1alpha1.DataStoreTCPFinalizer)
if uErr := r.Client.Update(ctx, &ds); uErr != nil {
logger.Error(uErr, "cannot update object")
return
}
logger.Info("finalizer removed successfully")
return
}
ds.Status.ObservedGeneration = ds.Generation
ds.Status.UsedBy = tcpSets.List()
ds.Status.Ready = meta.IsStatusConditionTrue(ds.Status.Conditions, kamajiv1alpha1.DataStoreConditionValidType)
if sErr := r.Client.Status().Update(ctx, &ds); sErr != nil {
return fmt.Errorf("cannot update the status for the given instance: %w", sErr)
if err = r.Client.Status().Update(ctx, &ds); err != nil {
logger.Error(err, "cannot update the status for the given instance")
return
}
return nil
if !ds.Status.Ready {
logger.Info("skipping triggering, DataStore is not ready")
return
}
logger.Info("triggering cascading reconciliation for TenantControlPlanes")
for _, tcp := range tcpList.Items {
var shrunkTCP kamajiv1alpha1.TenantControlPlane
shrunkTCP.Name = tcp.Name
shrunkTCP.Namespace = tcp.Namespace
go utils.TriggerChannel(ctx, r.TenantControlPlaneTrigger, shrunkTCP)
}
}()
if ds.GetDeletionTimestamp() != nil {
if len(tcpList.Items) > 0 {
logger.Info("deletion is blocked due to DataStore still being referenced")
meta.SetStatusCondition(&ds.Status.Conditions, metav1.Condition{
Type: kamajiv1alpha1.DataStoreConditionAllowedDeletionType,
Status: metav1.ConditionFalse,
ObservedGeneration: ds.Generation,
Reason: "DataStoreStillUsed",
Message: "The DataStore is still used and referenced by one (or more) TenantControlPlane objects.",
})
return reconcile.Result{}, nil
}
if meta.IsStatusConditionFalse(ds.Status.Conditions, kamajiv1alpha1.DataStoreConditionAllowedDeletionType) {
logger.Info("DataStore is not used by any TenantControlPlane object")
meta.SetStatusCondition(&ds.Status.Conditions, metav1.Condition{
Type: kamajiv1alpha1.DataStoreConditionAllowedDeletionType,
Status: metav1.ConditionTrue,
ObservedGeneration: ds.Generation,
Reason: "DataStoreUnused",
Message: "",
})
return reconcile.Result{}, nil
}
logger.Info("DataStore can be safely deleted")
return reconcile.Result{}, nil
}
if exists := meta.FindStatusCondition(ds.Status.Conditions, kamajiv1alpha1.DataStoreConditionValidType); exists == nil {
logger.Info("missing starting condition")
meta.SetStatusCondition(&ds.Status.Conditions, metav1.Condition{
Type: kamajiv1alpha1.DataStoreConditionValidType,
Status: metav1.ConditionUnknown,
ObservedGeneration: ds.Generation,
Reason: "MissingCondition",
Message: "Controller will process the validation.",
})
if sErr := r.Client.Status().Update(ctx, &ds); sErr != nil {
return reconcile.Result{}, fmt.Errorf("cannot update the status for the given instance: %w", sErr)
}
return reconcile.Result{}, nil
}
if ds.Spec.BasicAuth != nil {
logger.Info("validating basic authentication")
if vErr := r.validateBasicAuth(ctx, ds); vErr != nil {
meta.SetStatusCondition(&ds.Status.Conditions, metav1.Condition{
Type: kamajiv1alpha1.DataStoreConditionValidType,
Status: metav1.ConditionFalse,
ObservedGeneration: ds.Generation,
Reason: "BasicAuthValidationFailed",
Message: vErr.Error(),
})
logger.Info("invalid basic authentication")
return reconcile.Result{}, nil
}
logger.Info("basic authentication is valid")
}
logger.Info("validating TLS configuration")
if vErr := r.validateTLSConfig(ctx, ds); vErr != nil {
meta.SetStatusCondition(&ds.Status.Conditions, metav1.Condition{
Type: kamajiv1alpha1.DataStoreConditionValidType,
Status: metav1.ConditionFalse,
ObservedGeneration: ds.Generation,
Reason: "TLSConfigurationValidationFailed",
Message: vErr.Error(),
})
logger.Info("invalid TLS configuration")
return reconcile.Result{}, nil
}
logger.Info("TLS configuration is valid")
meta.SetStatusCondition(&ds.Status.Conditions, metav1.Condition{
Type: kamajiv1alpha1.DataStoreConditionValidType,
Status: metav1.ConditionTrue,
ObservedGeneration: ds.Status.ObservedGeneration,
Reason: "DataStoreIsValid",
Message: "",
})
if updateErr != nil {
logger.Error(updateErr, "cannot update DataStore status")
return reconcile.Result{}, updateErr
}
// Triggering the reconciliation of the Tenant Control Plane upon a Secret change
for _, tcp := range tcpList.Items {
var shrunkTCP kamajiv1alpha1.TenantControlPlane
shrunkTCP.Name = tcp.Name
shrunkTCP.Namespace = tcp.Namespace
go utils.TriggerChannel(ctx, r.TenantControlPlaneTrigger, shrunkTCP)
}
return reconcile.Result{}, nil
return reconcile.Result{}, err
}
func (r *DataStore) SetupWithManager(mgr controllerruntime.Manager) error {
@@ -112,9 +249,7 @@ func (r *DataStore) SetupWithManager(mgr controllerruntime.Manager) error {
}
//nolint:forcetypeassert
return controllerruntime.NewControllerManagedBy(mgr).
For(&kamajiv1alpha1.DataStore{}, builder.WithPredicates(
predicate.GenerationChangedPredicate{},
)).
For(&kamajiv1alpha1.DataStore{}).
Watches(&kamajiv1alpha1.TenantControlPlane{}, handler.Funcs{
CreateFunc: func(_ context.Context, createEvent event.TypedCreateEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
enqueueFn(createEvent.Object.(*kamajiv1alpha1.TenantControlPlane), w)
@@ -129,3 +264,76 @@ func (r *DataStore) SetupWithManager(mgr controllerruntime.Manager) error {
}).
Complete(r)
}
func (r *DataStore) validateBasicAuth(ctx context.Context, ds kamajiv1alpha1.DataStore) error {
if err := r.validateContentReference(ctx, ds.Spec.BasicAuth.Password); err != nil {
return fmt.Errorf("basic-auth password is not valid, %w", err)
}
if err := r.validateContentReference(ctx, ds.Spec.BasicAuth.Username); err != nil {
return fmt.Errorf("basic-auth username is not valid, %w", err)
}
return nil
}
func (r *DataStore) validateTLSConfig(ctx context.Context, ds kamajiv1alpha1.DataStore) error {
if ds.Spec.TLSConfig == nil && ds.Spec.Driver != kamajiv1alpha1.EtcdDriver {
return nil
}
if err := r.validateContentReference(ctx, ds.Spec.TLSConfig.CertificateAuthority.Certificate); err != nil {
return fmt.Errorf("CA certificate is not valid, %w", err)
}
if ds.Spec.Driver == kamajiv1alpha1.EtcdDriver {
if ds.Spec.TLSConfig.CertificateAuthority.PrivateKey == nil {
return fmt.Errorf("CA private key is required when using the etcd driver")
}
if ds.Spec.TLSConfig.ClientCertificate == nil {
return fmt.Errorf("client certificate is required when using the etcd driver")
}
}
if ds.Spec.TLSConfig.CertificateAuthority.PrivateKey != nil {
if err := r.validateContentReference(ctx, *ds.Spec.TLSConfig.CertificateAuthority.PrivateKey); err != nil {
return fmt.Errorf("CA private key is not valid, %w", err)
}
}
if ds.Spec.TLSConfig.ClientCertificate != nil {
if err := r.validateContentReference(ctx, ds.Spec.TLSConfig.ClientCertificate.Certificate); err != nil {
return fmt.Errorf("client certificate is not valid, %w", err)
}
if err := r.validateContentReference(ctx, ds.Spec.TLSConfig.ClientCertificate.PrivateKey); err != nil {
return fmt.Errorf("client private key is not valid, %w", err)
}
}
return nil
}
func (r *DataStore) validateContentReference(ctx context.Context, ref kamajiv1alpha1.ContentRef) error {
switch {
case len(ref.Content) > 0:
return nil
case ref.SecretRef == nil:
return fmt.Errorf("the Secret reference is mandatory when bare content is not specified")
case len(ref.SecretRef.SecretReference.Name) == 0:
return fmt.Errorf("the Secret reference name is mandatory")
case len(ref.SecretRef.SecretReference.Namespace) == 0:
return fmt.Errorf("the Secret reference namespace is mandatory")
}
if err := r.Client.Get(ctx, k8stypes.NamespacedName{Name: ref.SecretRef.SecretReference.Name, Namespace: ref.SecretRef.SecretReference.Namespace}, &corev1.Secret{}); err != nil {
if k8serrors.IsNotFound(err) {
return fmt.Errorf("secret %s/%s is not found", ref.SecretRef.SecretReference.Namespace, ref.SecretRef.SecretReference.Name)
}
return err
}
return nil
}

View File

@@ -88,6 +88,7 @@ type TenantControlPlaneReconcilerConfig struct {
//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=tlsroutes,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways,verbs=get;list;watch
//nolint:maintidx
func (r *TenantControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
@@ -137,11 +138,13 @@ func (r *TenantControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R
if markedToBeDeleted && !controllerutil.ContainsFinalizer(tenantControlPlane, finalizers.DatastoreFinalizer) {
return ctrl.Result{}, nil
}
// Retrieving the DataStore to use for the current reconciliation
ds, err := r.dataStore(ctx, tenantControlPlane)
if err != nil {
if errors.Is(err, ErrMissingDataStore) {
log.Info(err.Error())
// DataStore preflight checks:
// 1. DataStore must exist
// 2. it must be ready
ds, dsErr := r.dataStore(ctx, tenantControlPlane)
if dsErr != nil {
if errors.Is(dsErr, ErrMissingDataStore) {
log.Info(dsErr.Error())
return ctrl.Result{RequeueAfter: time.Second}, nil
}
@@ -151,6 +154,12 @@ func (r *TenantControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R
return ctrl.Result{}, err
}
if !ds.Status.Ready {
log.Info("cannot reconcile since DataStore is not ready")
return ctrl.Result{RequeueAfter: time.Second}, nil
}
dsConnection, err := datastore.NewStorageConnection(ctx, r.Client, *ds)
if err != nil {
log.Error(err, "cannot generate the DataStore connection for the given instance")

View File

@@ -5,7 +5,7 @@ NAMESPACE:=nats-system
.PHONY: helm
HELM = $(shell pwd)/../../../bin/helm
helm: ## Download helm locally if necessary.
$(call go-install-tool,$(HELM),helm.sh/helm/v3/cmd/helm@v3.9.0)
$(call go-install-tool,$(HELM),helm.sh/helm/v3/cmd/helm@v4.1.1)
nats: nats-certificates nats-secret nats-deployment

View File

@@ -0,0 +1,305 @@
# Kamaji and `externalClusterReference` usage
This document explains how to use **Kamaji's `externalClusterReference`** together with **Cluster API (CAPI)** to run Kubernetes control planes on an **external cluster**, while managing worker nodes from a management cluster.
It assumes the use of the KubeVirt infrastructure provider for ease of deployment and local testing.
---
## High-level Architecture
The following setup operates on **two Kubernetes clusters**:
- **Management cluster** runs Cluster API controllers and the Kamaji control-plane provider, and manages cluster lifecycle and topology.
- **External cluster** - runs Kamaji, hosts the Kubernetes control plane components and receives control plane workloads via Kamaji
---
## Prerequisites
- `docker`
- `kind`
- `kubectl`
- `clusterctl`
- `helm`
---
## Step 1: Create the KIND clusters
Create the **management** cluster:
```bash
kind create cluster --name management
```
Create the **external** cluster that will host control planes:
```bash
kind create cluster --name external
```
Verify contexts:
```bash
kubectl config get-contexts
```
---
## Step 2: Initialize Cluster API controllers
Switch to the management cluster:
```bash
kubectl config use-context kind-management
```
Enable ClusterClass support and initialize Cluster API with Kamaji and KubeVirt:
```bash
export CLUSTER_TOPOLOGY=true
clusterctl init \
--core cluster-api \
--bootstrap kubeadm \
--infrastructure kubevirt \
--control-plane kamaji
```
---
## Step 3: Enable Kamaji external cluster feature gates
Patch the Kamaji controller to enable `externalClusterReference`:
```bash
kubectl -n kamaji-system patch deployment capi-kamaji-controller-manager \
--type='json' \
-p='[
{
"op": "replace",
"path": "/spec/template/spec/containers/0/args/1",
"value": "--feature-gates=ExternalClusterReference=true,ExternalClusterReferenceCrossNamespace=true"
}
]'
```
---
## Step 4: Install KubeVirt
Fetch the latest stable KubeVirt version and install:
```bash
export VERSION=$(curl -s "https://storage.googleapis.com/kubevirt-prow/release/kubevirt/kubevirt/stable.txt")
kubectl apply -f "https://github.com/kubevirt/kubevirt/releases/download/${VERSION}/kubevirt-operator.yaml"
kubectl apply -f "https://github.com/kubevirt/kubevirt/releases/download/${VERSION}/kubevirt-cr.yaml"
```
Enable emulation (optional, if virtualization is not supported):
```bash
kubectl -n kubevirt patch kubevirt kubevirt \
--type=merge \
--patch '{"spec":{"configuration":{"developerConfiguration":{"useEmulation":true}}}}'
```
---
## Step 5: Prepare kubeconfig for the external cluster
Retrieve the external cluster control-plane address:
```bash
EXT_CP_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "external-control-plane")
```
Export and rewrite the kubeconfig:
```bash
kubectl --context kind-external config view --raw --minify --flatten > kind-external.kubeconfig
```
Replace the API endpoint with the cluster IP, required for cross-cluster access from the management cluster:
```bash
bash -c "sed -i -E 's#https://[^:]+:[0-9]+#https://$EXT_CP_IP:6443#g' kind-external.kubeconfig"
```
Create the kubeconfig secret in the management cluster:
```bash
kubectl -n default create secret generic kind-external-kubeconfig \
--from-file=kubeconfig=kind-external.kubeconfig
```
---
## Step 6: Install Kamaji and dependencies on the external cluster
Switch context:
```bash
kubectl config use-context kind-external
```
Install cert-manager:
```bash
helm upgrade --install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set installCRDs=true
```
Install Kamaji:
```bash
helm upgrade --install kamaji clastix/kamaji \
--namespace kamaji-system \
--create-namespace \
--set 'resources=null' \
--version 0.0.0+latest
```
Install MetalLB:
```bash
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.15.3/config/manifests/metallb-native.yaml
```
Configure MetalLB IP address pool:
```bash
SUBNET=$(docker network inspect kind | jq -r '.[0].IPAM.Config[] | select(.Subnet | test(":") | not) | .Subnet' | head -n1)
NET_PREFIX=$(echo "$SUBNET" | cut -d/ -f1 | awk -F. '{print $1"."$2}')
kubectl apply -f - <<EOF
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default
namespace: metallb-system
spec:
addresses:
- ${NET_PREFIX}.255.200-${NET_PREFIX}.255.250
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default
namespace: metallb-system
EOF
```
Create tenant namespace:
```bash
kubectl create namespace kamaji-tenants
```
---
## Step 7: Definition of KamajiControlPlaneTemplate
The `KamajiControlPlaneTemplate` is defined in [the following manifest](https://raw.githubusercontent.com/clastix/kamaji/master/config/capi/clusterclass-kubevirt-kamaji-external.yaml) and can be applied directly.
This template configures how Kamaji deploys and manages the tenant control plane on an external Kubernetes cluster using Cluster API.
```bash
apiVersion: controlplane.cluster.x-k8s.io/v1alpha1
kind: KamajiControlPlaneTemplate
metadata:
name: kamaji-controlplane-external
namespace: default
spec:
template:
spec:
addons:
coreDNS: {}
konnectivity: {}
kubeProxy: {}
dataStoreName: "default" # reference to DataStore present on external cluster
deployment:
externalClusterReference:
deploymentNamespace: kamaji-tenants
kubeconfigSecretName: kind-external-kubeconfig
kubeconfigSecretKey: kubeconfig
network:
serviceType: LoadBalancer
kubelet:
cgroupfs: systemd
preferredAddressTypes:
- InternalIP
registry: "registry.k8s.io"
```
The `.spec.template.spec.deployment.externalClusterReference` section defines how Kamaji connects to and deploys control plane components into the external cluster:
- `deploymentNamespace` - The namespace on the external cluster where `TenantControlPlane` resources and control plane components are created.
- `kubeconfigSecretName` - The name of the Kubernetes Secret containing a kubeconfig that allows Kamaji to authenticate to the external cluster.
- `kubeconfigSecretKey` - The key inside the secret that holds the kubeconfig data.
The referenced secret must exist in the Kamaji management cluster and provide sufficient permissions to create and manage resources in the target external cluster.
---
## Step 8: Create the Cluster
Switch context back to the management cluster:
```bash
kubectl config use-context kind-management
```
Apply the Cluster manifest:
```bash
kubectl apply -f "https://raw.githubusercontent.com/clastix/kamaji/master/config/capi/clusterclass-kubevirt-kamaji-external.yaml"
```
---
## Validation
Check tenant control plane pods running in the external cluster:
```bash
kubectl --context kind-external -n kamaji-tenants get pods
```
Check cluster status in the management cluster:
```bash
kubectl --context kind-management get clusters
kubectl --context kind-management get kamajicontrolplanes
```
Get cluster kubeconfig and confirm it is working:
```bash
kubectl config use-context kind-management
clusterctl get kubeconfig demo-external > demo-external.kubeconfig
KUBECONFIG=./demo-external.kubeconfig kubectl get nodes
```
---
## Clean up
Delete Kind clusters:
```bash
kind delete cluster --name management
kind delete cluster --name external
```
---
## Summary
Using `externalClusterReference` with Kamaji and Cluster API enables:
- Hosted Kubernetes control planes on remote clusters
- Strong separation of concerns
- Multi-cluster management patterns
- Clean integration with ClusterClass

View File

@@ -1,106 +1,34 @@
# Gateway API Support
# Gateway API
Kamaji provides built-in support for the [Gateway API](https://gateway-api.sigs.k8s.io/), allowing you to expose Tenant Control Planes using TLSRoute resources with SNI-based routing. This enables hostname-based routing to multiple Tenant Control Planes through a single Gateway resource, reducing the need for dedicated LoadBalancer services.
Kamaji provides built-in support for the [Gateway API](https://gateway-api.sigs.k8s.io/), allowing Tenant Control Planes to be exposed as SNI-based addresses/urls. This eliminates the need for a dedicated LoadBalancer IP per TCP. A single Gateway resource can be used for multiple Tenant Control Planes and provide access to them with hostname-based routing (like `https://mycluster.xyz.com:6443`).
## Overview
You can configure Gateway in Tenant Control Plane via `tcp.spec.controlPlane.gateway`, Kamaji will automatically create a `TLSRoute` resource with corresponding spec. To make this configuration work, you need to ensure `gateway` exists (is created by you) and `tcp.spec.controlPlane.gateway` points to your gateway.
Gateway API support in Kamaji automatically creates and manages TLSRoute resources for your Tenant Control Planes. When you configure a Gateway for a Tenant Control Plane, Kamaji automatically creates TLSRoutes for the Control Plane API Server. If konnectivity is enabled, a separate TLSRoute is created for it. Both TLSRoutes use the same hostname and Gateway resource, but route to different ports(listeners) using port-based routing and semantic `sectionName` values.
We will cover a few examples below on how this is done.
Therefore, the target `Gateway` resource must have right listener configurations (see the Gateway [example section](#gateway-resource-setup) below).
## How It Works
When you configure `spec.controlPlane.gateway` in a TenantControlPlane resource, Kamaji automatically:
1. **Creates a TLSRoute for the control plane** that routes for port 6443 (or `spec.networkProfile.port`) with sectionName `"kube-apiserver"`
2. **Creates a TLSRoute for Konnectivity** (if konnectivity addon is enabled) that routes for port 8132 (or `spec.addons.konnectivity.server.port`) with sectionName `"konnectivity-server"`
Both TLSRoutes:
- Use the same hostname from `spec.controlPlane.gateway.hostname`
- Reference the same parent Gateway resource via `parentRefs`
- The `port` and `sectionName` fields are set automatically by Kamaji
- Route to the appropriate Tenant Control Plane service
The Gateway resource must have listeners configured for both ports (6443 and 8132) to support both routes.
## Prerequisites
Before using Gateway API support, ensure:
Before using Gateway API mode, please ensure:
1. **Gateway API CRDs are installed** in your cluster (Required CRDs: `GatewayClass`, `Gateway`, `TLSRoute`)
2. **A Gateway resource exists** with appropriate listeners configured:
- At minimum, listeners for ports 6443 (control plane) and 8132 (Konnectivity)
- TLS protocol with Passthrough mode
- Hostname pattern matching your Tenant Control Plane hostnames
2. **A Gateway resource exists** with appropriate configuration (see examples in this guide):
- Listeners for kube-apiserver.
- Use TLS protocol with Passthrough mode
- Hostname (or Hostname pattern) matching your Tenant Control Plane hostname
3. **DNS is configured** to resolve your hostnames to the Gateway's external address
3. (optional) **DNS is configured** to resolve hostnames (or hostname pattern) to the Gateway's LoadBalancer IP address. (This is needed for worker nodes to join, for testing we will use host entries in `/etc/hosts` for this guide)
4. **Gateway controller is running** (e.g., Envoy Gateway, Istio Gateway, etc.)
## Configuration
To replicate the guide below, please install [Envoy Gateway](https://gateway.envoyproxy.io/docs/tasks/quickstart/).
### TenantControlPlane Gateway Configuration
Next, create a gateway resource:
Enable Gateway API mode by setting the `spec.controlPlane.gateway` field in your TenantControlPlane resource:
#### Gateway Resource Setup
```yaml
apiVersion: kamaji.clastix.io/v1alpha1
kind: TenantControlPlane
metadata:
name: tcp-1
spec:
controlPlane:
# ... gateway configuration:
gateway:
hostname: "tcp1.cluster.dev"
parentRefs:
- name: gateway
namespace: default
additionalMetadata:
labels:
environment: production
annotations:
example.com/custom: "value"
# ... rest of the spec
deployment:
replicas: 1
service:
serviceType: ClusterIP
dataStore: default
kubernetes:
version: v1.29.0
kubelet:
cgroupfs: systemd
networkProfile:
port: 6443
certSANs:
- "c11.cluster.dev" # make sure to set this.
addons:
coreDNS: {}
kubeProxy: {}
konnectivity: {}
```
**Required fields:**
- `hostname`: The hostname that will be used for routing (must match Gateway listener hostname pattern)
- `parentRefs`: Array of Gateway references (name and namespace)
**Optional fields:**
- `additionalMetadata.labels`: Custom labels to add to TLSRoute resources
- `additionalMetadata.annotations`: Custom annotations to add to TLSRoute resources
!!! warning "Port and sectionName are set automatically"
Do not specify `port` or `sectionName` in `parentRefs`. Kamaji automatically sets these fields in TLSRoutes.
### Gateway Resource Setup
Your Gateway resource must have listeners configured for both the control plane and Konnectivity ports. Here's an example Gateway configuration:
Your Gateway resource must have listeners configured for the control plane. Here's an example Gateway configuration:
```yaml
apiVersion: gateway.networking.k8s.io/v1
@@ -130,27 +58,94 @@ spec:
kind: TLSRoute
namespaces:
from: All
```
The above gateway is configured with envoy gateway controller. You can achieve the same results with any other gateway controller that supports `TLSRoutes` and TLS passthrough mode.
The rest of this guide focuses on TCP.
## TenantControlPlane Gateway Configuration
Enable Gateway API mode by setting the `spec.controlPlane.gateway` field in your TenantControlPlane resource:
```yaml
apiVersion: kamaji.clastix.io/v1alpha1
kind: TenantControlPlane
metadata:
name: tcp-1
spec:
controlPlane:
# ... gateway configuration:
gateway:
hostname: "tcp1.cluster.dev"
parentRefs:
- name: gateway
namespace: default
sectionName: kube-apiserver
port: 6443
additionalMetadata:
labels:
environment: production
annotations:
example.com/custom: "value"
# ... rest of the spec
deployment:
replicas: 1
service:
serviceType: ClusterIP
dataStore: default
kubernetes:
version: v1.29.0
kubelet:
cgroupfs: systemd
networkProfile:
port: 6443
certSANs:
- "tcp1.cluster.dev" # make sure to set this.
addons:
coreDNS: {}
kubeProxy: {}
# if konnectivity addon is enabled:
- name: konnectivity-server
port: 8132
protocol: TLS
hostname: 'tcp1.cluster.dev'
tls:
mode: Passthrough
allowedRoutes:
kinds:
- group: gateway.networking.k8s.io
kind: TLSRoute
namespaces:
from: All
```
**Required fields:**
## Multiple Tenant Control Planes
- `hostname`: The hostname that will be used for routing (must match Gateway listener hostname pattern)
- `parentRefs`: Array of Gateway references
You can use the same Gateway resource for multiple Tenant Control Planes by using different hostnames:
**Optional fields:**
- `additionalMetadata.labels`: Custom labels to add to TLSRoute resources
- `additionalMetadata.annotations`: Custom annotations to add to TLSRoute resources
!!! info
### Verify
From our kubectl client machine / remote machines we can access the cluster above with the hostname.
**Step 1:** Fetch load balancer IP of the gateway.
`kubectl get gateway gateway -n default -o jsonpath='{.status.addresses[0].value}'`
**Step 2:** Add host entries in `/etc/hosts` with the above hostname and gateway LB IP.
`echo "<LB_IP> tcp1.cluster.dev" | sudo tee -a /etc/hosts`
**Step 3:** Fetch kubeconfig of tcp cluster.
`kubectl get secrets tcp-1-admin-kubeconfig -o jsonpath='{.data.admin\.conf}' | base64 -d > kubeconfig`
**Step 4:** Test connectivity:
`kubectl --kubeconfig kubeconfig cluster-info`
### Multiple Tenant Control Planes
We can use the same Gateway resource for multiple Tenant Control Planes by using different hostnames per tenant cluster.
Let us extend the above example for multiple tenant control planes behind single gateway (and LB IP).
```yaml
# Gateway with wildcard hostname
@@ -160,10 +155,13 @@ metadata:
name: gateway
spec:
listeners:
- hostname: '*.cluster.dev'
name: kube-apiserver
- name: kube-apiserver
port: 6443
# ...
# note: we changed to wildcard hostname pattern matching
# for cluster.dev
hostname: '*.cluster.dev'
# ...
---
# Tenant Control Plane 1
apiVersion: kamaji.clastix.io/v1alpha1
@@ -194,17 +192,37 @@ spec:
# ...
```
Each Tenant Control Plane will get its own TLSRoutes with the respective hostnames, all routing through the same Gateway resource.
Each Tenant Control Plane needs to use a different hostname. For each TCP, Kamaji creates a `TLSRoutes` resource with the respective hostnames, all `TLSRoutes` routing through the same Gateway resource.
You can check the Gateway status in the TenantControlPlane:
```bash
kubectl get tenantcontrolplane tcp-1 -o yaml
### Konnectivity
If konnectivity addon is enabled, Kamaji creates a separate TLSRoute for it. But this is hardcoded with the listener name `konnectivity-server` and port `8132`. All gateways mentioned in `spec.controlPlane.gateway.parentRefs` must contain a listener with the same configuration for the given hostname. Below is example configuration:
```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: gateway
namespace: default
spec:
gatewayClassName: envoy-gw-class
listeners:
- ...
- ...
- name: konnectivity-server
port: 8132
protocol: TLS
hostname: 'tcp1.cluster.dev'
tls:
mode: Passthrough
allowedRoutes:
kinds:
- group: gateway.networking.k8s.io
kind: TLSRoute
namespaces:
from: All
```
Look for the `status.kubernetesResources.gateway` and `status.addons.konnectivity.gateway` fields.
## Additional Resources
- [Gateway API Documentation](https://gateway-api.sigs.k8s.io/)

File diff suppressed because it is too large Load Diff

View File

@@ -18,10 +18,13 @@ Usage of the said artefacts is not suggested for production use-case due to miss
### Edge Releases
Edge Release artifacts are published on a monthly basis as part of the open source project.
Versioning follows the form `edge-{year}.{month}.{incremental}` where incremental refers to the monthly release.
For example, `edge-24.7.1` is the first edge release shipped in July 2024.
Versioning follows the form `{year}.{month}.{incremental}-edge` where incremental refers to the monthly release.
For example, `26.3.1-edge` is the first edge release shipped in March 2027.
The full list of edge release artifacts can be found on the Kamaji's GitHub [releases page](https://github.com/clastix/kamaji/releases).
> _Nota Bene_: all edge releases prior to March 2026 used a different pattern (`edge-{year}.{month}.{incremental}`):
> this change has been required to take advantage of GoReleaser to start our support for CRA compliance.
Edge Release artifacts contain the code in from the main branch at the point in time when they were cut.
This means they always have the latest features and fixes, and have undergone automated testing as well as maintainer code review.
Edge Releases may involve partial features that are later modified or backed out.

View File

@@ -68,6 +68,7 @@ nav:
- cluster-api/other-providers.md
- cluster-api/cluster-autoscaler.md
- cluster-api/cluster-class.md
- cluster-api/external-cluster.md
- 'Guides':
- guides/index.md
- guides/alternative-datastore.md

View File

@@ -48,7 +48,9 @@ var _ = Describe("Deploy a TenantControlPlane with Gateway API and Konnectivity"
},
GatewayParentRefs: []gatewayv1.ParentReference{
{
Name: "test-gateway",
Name: "test-gateway",
Port: pointer.To(gatewayv1.PortNumber(6443)),
SectionName: pointer.To(gatewayv1.SectionName("cp-listener")),
},
},
},
@@ -96,6 +98,35 @@ var _ = Describe("Deploy a TenantControlPlane with Gateway API and Konnectivity"
StatusMustEqualTo(tcp, kamajiv1alpha1.VersionReady)
})
It("Should create control plane TLSRoute preserving user-provided parentRef fields", func() {
Eventually(func() error {
route := &gatewayv1alpha2.TLSRoute{}
if err := k8sClient.Get(context.Background(), types.NamespacedName{
Name: tcp.Name,
Namespace: tcp.Namespace,
}, route); err != nil {
return err
}
if len(route.Spec.ParentRefs) == 0 {
return fmt.Errorf("parentRefs is empty")
}
if route.Spec.ParentRefs[0].SectionName == nil {
return fmt.Errorf("sectionName is nil")
}
if *route.Spec.ParentRefs[0].SectionName != gatewayv1.SectionName("cp-listener") {
return fmt.Errorf("expected sectionName 'cp-listener', got '%s'", *route.Spec.ParentRefs[0].SectionName)
}
if route.Spec.ParentRefs[0].Port == nil {
return fmt.Errorf("port is nil")
}
if *route.Spec.ParentRefs[0].Port != gatewayv1.PortNumber(6443) {
return fmt.Errorf("expected port 6443, got '%d'", *route.Spec.ParentRefs[0].Port)
}
return nil
}).WithTimeout(time.Minute).Should(Succeed())
})
It("Should create Konnectivity TLSRoute with correct sectionName", func() {
Eventually(func() error {
route := &gatewayv1alpha2.TLSRoute{}

View File

@@ -48,7 +48,9 @@ var _ = Describe("Deploy a TenantControlPlane with Gateway API", func() {
},
GatewayParentRefs: []gatewayv1.ParentReference{
{
Name: "test-gateway",
Name: "test-gateway",
Port: pointer.To(gatewayv1.PortNumber(6443)),
SectionName: pointer.To(gatewayv1.SectionName("cp-listener")),
},
},
},
@@ -90,7 +92,7 @@ var _ = Describe("Deploy a TenantControlPlane with Gateway API", func() {
StatusMustEqualTo(tcp, kamajiv1alpha1.VersionReady)
})
It("Should create control plane TLSRoute with correct sectionName", func() {
It("Should create control plane TLSRoute preserving user-provided parentRef fields", func() {
Eventually(func() error {
route := &gatewayv1alpha2.TLSRoute{}
// TODO: Check ownership.
@@ -106,8 +108,14 @@ var _ = Describe("Deploy a TenantControlPlane with Gateway API", func() {
if route.Spec.ParentRefs[0].SectionName == nil {
return fmt.Errorf("sectionName is nil")
}
if *route.Spec.ParentRefs[0].SectionName != gatewayv1.SectionName("kube-apiserver") {
return fmt.Errorf("expected sectionName 'kube-apiserver', got '%s'", *route.Spec.ParentRefs[0].SectionName)
if *route.Spec.ParentRefs[0].SectionName != gatewayv1.SectionName("cp-listener") {
return fmt.Errorf("expected sectionName 'cp-listener', got '%s'", *route.Spec.ParentRefs[0].SectionName)
}
if route.Spec.ParentRefs[0].Port == nil {
return fmt.Errorf("port is nil")
}
if *route.Spec.ParentRefs[0].Port != gatewayv1.PortNumber(6443) {
return fmt.Errorf("expected port 6443, got '%d'", *route.Spec.ParentRefs[0].Port)
}
return nil

View File

@@ -212,7 +212,7 @@ func ScaleTenantControlPlane(tcp *kamajiv1alpha1.TenantControlPlane, replicas in
Expect(err).To(Succeed())
}
// CreateGatewayWithListeners creates a Gateway with both kube-apiserver and konnectivity-server listeners.
// CreateGatewayWithListeners creates a Gateway with control plane and konnectivity-server listeners.
func CreateGatewayWithListeners(gatewayName, namespace, gatewayClassName, hostname string) {
GinkgoHelper()
gateway := &gatewayv1.Gateway{
@@ -224,7 +224,7 @@ func CreateGatewayWithListeners(gatewayName, namespace, gatewayClassName, hostna
GatewayClassName: gatewayv1.ObjectName(gatewayClassName),
Listeners: []gatewayv1.Listener{
{
Name: "kube-apiserver",
Name: "cp-listener",
Port: 6443,
Protocol: gatewayv1.TLSProtocolType,
Hostname: pointer.To(gatewayv1.Hostname(hostname)),

14
go.mod
View File

@@ -15,7 +15,7 @@ require (
github.com/google/uuid v1.6.0
github.com/json-iterator/go v1.1.12
github.com/juju/mutex/v2 v2.0.0
github.com/nats-io/nats.go v1.48.0
github.com/nats-io/nats.go v1.49.0
github.com/onsi/ginkgo/v2 v2.28.1
github.com/onsi/gomega v1.39.1
github.com/prometheus/client_golang v1.23.2
@@ -24,8 +24,8 @@ require (
github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.21.0
github.com/testcontainers/testcontainers-go v0.40.0
go.etcd.io/etcd/api/v3 v3.6.7
go.etcd.io/etcd/client/v3 v3.6.7
go.etcd.io/etcd/api/v3 v3.6.8
go.etcd.io/etcd/client/v3 v3.6.8
go.uber.org/automaxprocs v1.6.0
gomodules.xyz/jsonpatch/v2 v2.5.0
k8s.io/api v0.35.0
@@ -35,7 +35,7 @@ require (
k8s.io/cluster-bootstrap v0.0.0
k8s.io/klog/v2 v2.130.1
k8s.io/kubelet v0.0.0
k8s.io/kubernetes v1.35.0
k8s.io/kubernetes v1.35.1
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
sigs.k8s.io/controller-runtime v0.22.4
sigs.k8s.io/gateway-api v1.4.1
@@ -98,7 +98,7 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/juju/errors v0.0.0-20220203013757-bd733f3c86b9 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/lithammer/dedent v1.1.0 // indirect
@@ -117,7 +117,7 @@ require (
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
@@ -148,7 +148,7 @@ require (
github.com/x448/float16 v0.8.4 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.7 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.8 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect

28
go.sum
View File

@@ -190,8 +190,8 @@ github.com/juju/version/v2 v2.0.0-20211007103408-2e8da085dc23/go.mod h1:Ljlbryh9
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/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
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/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -243,10 +243,10 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
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/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
@@ -369,12 +369,12 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.etcd.io/etcd/api/v3 v3.6.7 h1:7BNJ2gQmc3DNM+9cRkv7KkGQDayElg8x3X+tFDYS+E0=
go.etcd.io/etcd/api/v3 v3.6.7/go.mod h1:xJ81TLj9hxrYYEDmXTeKURMeY3qEDN24hqe+q7KhbnI=
go.etcd.io/etcd/client/pkg/v3 v3.6.7 h1:vvzgyozz46q+TyeGBuFzVuI53/yd133CHceNb/AhBVs=
go.etcd.io/etcd/client/pkg/v3 v3.6.7/go.mod h1:2IVulJ3FZ/czIGl9T4lMF1uxzrhRahLqe+hSgy+Kh7Q=
go.etcd.io/etcd/client/v3 v3.6.7 h1:9WqA5RpIBtdMxAy1ukXLAdtg2pAxNqW5NUoO2wQrE6U=
go.etcd.io/etcd/client/v3 v3.6.7/go.mod h1:2XfROY56AXnUqGsvl+6k29wrwsSbEh1lAouQB1vHpeE=
go.etcd.io/etcd/api/v3 v3.6.8 h1:gqb1VN92TAI6G2FiBvWcqKtHiIjr4SU2GdXxTwyexbM=
go.etcd.io/etcd/api/v3 v3.6.8/go.mod h1:qyQj1HZPUV3B5cbAL8scG62+fyz5dSxxu0w8pn28N6Q=
go.etcd.io/etcd/client/pkg/v3 v3.6.8 h1:Qs/5C0LNFiqXxYf2GU8MVjYUEXJ6sZaYOz0zEqQgy50=
go.etcd.io/etcd/client/pkg/v3 v3.6.8/go.mod h1:GsiTRUZE2318PggZkAo6sWb6l8JLVrnckTNfbG8PWtw=
go.etcd.io/etcd/client/v3 v3.6.8 h1:B3G76t1UykqAOrbio7s/EPatixQDkQBevN8/mwiplrY=
go.etcd.io/etcd/client/v3 v3.6.8/go.mod h1:MVG4BpSIuumPi+ELF7wYtySETmoTWBHVcDoHdVupwt8=
go.etcd.io/etcd/pkg/v3 v3.6.5 h1:byxWB4AqIKI4SBmquZUG1WGtvMfMaorXFoCcFbVeoxM=
go.etcd.io/etcd/pkg/v3 v3.6.5/go.mod h1:uqrXrzmMIJDEy5j00bCqhVLzR5jEJIwDp5wTlLwPGOU=
go.etcd.io/etcd/server/v3 v3.6.5 h1:4RbUb1Bd4y1WkBHmuF+cZII83JNQMuNXzyjwigQ06y0=
@@ -549,8 +549,8 @@ k8s.io/kube-proxy v0.35.0 h1:erv2wYmGZ6nyu/FtmaIb+ORD3q2rfZ4Fhn7VXs/8cPQ=
k8s.io/kube-proxy v0.35.0/go.mod h1:bd9lpN3uLLOOWc/CFZbkPEi9DTkzQQymbE8FqSU4bWk=
k8s.io/kubelet v0.35.0 h1:8cgJHCBCKLYuuQ7/Pxb/qWbJfX1LXIw7790ce9xHq7c=
k8s.io/kubelet v0.35.0/go.mod h1:ciRzAXn7C4z5iB7FhG1L2CGPPXLTVCABDlbXt/Zz8YA=
k8s.io/kubernetes v1.35.0 h1:PUOojD8c8E3csMP5NX+nLLne6SGqZjrYCscptyBfWMY=
k8s.io/kubernetes v1.35.0/go.mod h1:Tzk9Y9W/XUFFFgTUVg+BAowoFe+Pc7koGLuaiLHdcFg=
k8s.io/kubernetes v1.35.1 h1:qmjXSCDPnOuXPuJb5pv+eLzpXhhlD09Jid1pG/OvFU8=
k8s.io/kubernetes v1.35.1/go.mod h1:AaPpCpiS8oAqRbEwpY5r3RitLpwpVp5lVXKFkJril58=
k8s.io/system-validators v1.12.1 h1:AY1+COTLJN/Sj0w9QzH1H0yvyF3Kl6CguMnh32WlcUU=
k8s.io/system-validators v1.12.1/go.mod h1:awfSS706v9R12VC7u7K89FKfqVy44G+E0L1A0FX9Wmw=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=

View File

@@ -51,6 +51,18 @@ const (
kineInitContainerName = "chmod"
)
func applyProbeOverrides(probe *corev1.Probe, spec *kamajiv1alpha1.ProbeSpec) {
if spec == nil {
return
}
probe.InitialDelaySeconds = pointer.Deref(spec.InitialDelaySeconds, probe.InitialDelaySeconds)
probe.TimeoutSeconds = pointer.Deref(spec.TimeoutSeconds, probe.TimeoutSeconds)
probe.PeriodSeconds = pointer.Deref(spec.PeriodSeconds, probe.PeriodSeconds)
probe.SuccessThreshold = pointer.Deref(spec.SuccessThreshold, probe.SuccessThreshold)
probe.FailureThreshold = pointer.Deref(spec.FailureThreshold, probe.FailureThreshold)
}
type DataStoreOverrides struct {
Resource string
DataStore kamajiv1alpha1.DataStore
@@ -384,6 +396,16 @@ func (d Deployment) buildScheduler(podSpec *corev1.PodSpec, tenantControlPlane k
FailureThreshold: 3,
}
if probes := tenantControlPlane.Spec.ControlPlane.Deployment.Probes; probes != nil {
applyProbeOverrides(podSpec.Containers[index].LivenessProbe, probes.Liveness)
applyProbeOverrides(podSpec.Containers[index].StartupProbe, probes.Startup)
if probes.Scheduler != nil {
applyProbeOverrides(podSpec.Containers[index].LivenessProbe, probes.Scheduler.Liveness)
applyProbeOverrides(podSpec.Containers[index].StartupProbe, probes.Scheduler.Startup)
}
}
switch {
case tenantControlPlane.Spec.ControlPlane.Deployment.Resources == nil:
podSpec.Containers[index].Resources = corev1.ResourceRequirements{}
@@ -475,6 +497,17 @@ func (d Deployment) buildControllerManager(podSpec *corev1.PodSpec, tenantContro
SuccessThreshold: 1,
FailureThreshold: 3,
}
if probes := tenantControlPlane.Spec.ControlPlane.Deployment.Probes; probes != nil {
applyProbeOverrides(podSpec.Containers[index].LivenessProbe, probes.Liveness)
applyProbeOverrides(podSpec.Containers[index].StartupProbe, probes.Startup)
if probes.ControllerManager != nil {
applyProbeOverrides(podSpec.Containers[index].LivenessProbe, probes.ControllerManager.Liveness)
applyProbeOverrides(podSpec.Containers[index].StartupProbe, probes.ControllerManager.Startup)
}
}
switch {
case tenantControlPlane.Spec.ControlPlane.Deployment.Resources == nil:
podSpec.Containers[index].Resources = corev1.ResourceRequirements{}
@@ -606,6 +639,19 @@ func (d Deployment) buildKubeAPIServer(podSpec *corev1.PodSpec, tenantControlPla
SuccessThreshold: 1,
FailureThreshold: 3,
}
if probes := tenantControlPlane.Spec.ControlPlane.Deployment.Probes; probes != nil {
applyProbeOverrides(podSpec.Containers[index].LivenessProbe, probes.Liveness)
applyProbeOverrides(podSpec.Containers[index].ReadinessProbe, probes.Readiness)
applyProbeOverrides(podSpec.Containers[index].StartupProbe, probes.Startup)
if probes.APIServer != nil {
applyProbeOverrides(podSpec.Containers[index].LivenessProbe, probes.APIServer.Liveness)
applyProbeOverrides(podSpec.Containers[index].ReadinessProbe, probes.APIServer.Readiness)
applyProbeOverrides(podSpec.Containers[index].StartupProbe, probes.APIServer.Startup)
}
}
podSpec.Containers[index].ImagePullPolicy = corev1.PullAlways
// Volume mounts
var extraVolumeMounts []corev1.VolumeMount

View File

@@ -4,12 +4,21 @@
package controlplane
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
pointer "k8s.io/utils/ptr"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
)
func TestControlplaneDeployment(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Controlplane Deployment Suite")
}
var _ = Describe("Controlplane Deployment", func() {
var d Deployment
BeforeEach(func() {
@@ -43,4 +52,74 @@ var _ = Describe("Controlplane Deployment", func() {
Expect(etcdSerVersOverrides).To(Equal("/events#https://etcd-0;https://etcd-1;https://etcd-2,/pods#https://etcd-3;https://etcd-4;https://etcd-5"))
})
})
Describe("applyProbeOverrides", func() {
var probe *corev1.Probe
BeforeEach(func() {
probe = &corev1.Probe{
InitialDelaySeconds: 0,
TimeoutSeconds: 1,
PeriodSeconds: 10,
SuccessThreshold: 1,
FailureThreshold: 3,
}
})
It("should not modify probe when spec is nil", func() {
applyProbeOverrides(probe, nil)
Expect(probe.InitialDelaySeconds).To(Equal(int32(0)))
Expect(probe.TimeoutSeconds).To(Equal(int32(1)))
Expect(probe.PeriodSeconds).To(Equal(int32(10)))
Expect(probe.SuccessThreshold).To(Equal(int32(1)))
Expect(probe.FailureThreshold).To(Equal(int32(3)))
})
It("should override only FailureThreshold when only it is set", func() {
spec := &kamajiv1alpha1.ProbeSpec{
FailureThreshold: pointer.To(int32(30)),
}
applyProbeOverrides(probe, spec)
Expect(probe.FailureThreshold).To(Equal(int32(30)))
Expect(probe.InitialDelaySeconds).To(Equal(int32(0)))
Expect(probe.TimeoutSeconds).To(Equal(int32(1)))
Expect(probe.PeriodSeconds).To(Equal(int32(10)))
Expect(probe.SuccessThreshold).To(Equal(int32(1)))
})
It("should override all fields when all are set", func() {
spec := &kamajiv1alpha1.ProbeSpec{
InitialDelaySeconds: pointer.To(int32(15)),
TimeoutSeconds: pointer.To(int32(5)),
PeriodSeconds: pointer.To(int32(30)),
SuccessThreshold: pointer.To(int32(2)),
FailureThreshold: pointer.To(int32(10)),
}
applyProbeOverrides(probe, spec)
Expect(probe.InitialDelaySeconds).To(Equal(int32(15)))
Expect(probe.TimeoutSeconds).To(Equal(int32(5)))
Expect(probe.PeriodSeconds).To(Equal(int32(30)))
Expect(probe.SuccessThreshold).To(Equal(int32(2)))
Expect(probe.FailureThreshold).To(Equal(int32(10)))
})
It("should cascade global then component overrides", func() {
global := &kamajiv1alpha1.ProbeSpec{
FailureThreshold: pointer.To(int32(10)),
PeriodSeconds: pointer.To(int32(20)),
}
applyProbeOverrides(probe, global)
component := &kamajiv1alpha1.ProbeSpec{
FailureThreshold: pointer.To(int32(60)),
}
applyProbeOverrides(probe, component)
Expect(probe.FailureThreshold).To(Equal(int32(60)))
Expect(probe.PeriodSeconds).To(Equal(int32(20)))
Expect(probe.TimeoutSeconds).To(Equal(int32(1)))
Expect(probe.InitialDelaySeconds).To(Equal(int32(0)))
Expect(probe.SuccessThreshold).To(Equal(int32(1)))
})
})
})

View File

@@ -1,39 +0,0 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package utils
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
)
// CheckExists ensures that the default Datastore exists before starting the manager.
func CheckExists(ctx context.Context, scheme *runtime.Scheme, datastoreName string) error {
if datastoreName == "" {
return nil
}
ctrlClient, err := client.New(ctrl.GetConfigOrDie(), client.Options{Scheme: scheme})
if err != nil {
return fmt.Errorf("unable to create controlerruntime.Client: %w", err)
}
if err = ctrlClient.Get(ctx, types.NamespacedName{Name: datastoreName}, &kamajiv1alpha1.DataStore{}); err != nil {
if errors.IsNotFound(err) {
return fmt.Errorf("the default Datastore %s doesn't exist", datastoreName)
}
return fmt.Errorf("an error occurred during datastore retrieval: %w", err)
}
return nil
}

View File

@@ -150,6 +150,10 @@ func (r *KubernetesGatewayResource) mutate(tcp *kamajiv1alpha1.TenantControlPlan
tcp.Spec.ControlPlane.Gateway.AdditionalMetadata.Annotations)
r.resource.SetAnnotations(annotations)
if tcp.Spec.ControlPlane.Gateway.GatewayParentRefs != nil {
r.resource.Spec.ParentRefs = tcp.Spec.ControlPlane.Gateway.GatewayParentRefs
}
serviceName := gatewayv1alpha2.ObjectName(tcp.Status.Kubernetes.Service.Name)
servicePort := tcp.Status.Kubernetes.Service.Port
@@ -157,11 +161,6 @@ func (r *KubernetesGatewayResource) mutate(tcp *kamajiv1alpha1.TenantControlPlan
return fmt.Errorf("service not ready, cannot create TLSRoute")
}
if tcp.Spec.ControlPlane.Gateway.GatewayParentRefs != nil {
// Copy parentRefs and explicitly set port and sectionName fields
r.resource.Spec.ParentRefs = NewParentRefsSpecWithPortAndSection(tcp.Spec.ControlPlane.Gateway.GatewayParentRefs, servicePort, "kube-apiserver")
}
rule := gatewayv1alpha2.TLSRouteRule{
BackendRefs: []gatewayv1alpha2.BackendRef{
{

View File

@@ -102,30 +102,6 @@ var _ = Describe("KubernetesGatewayResource", func() {
Expect(shouldUpdate).To(BeTrue())
})
It("should set port and sectionName in parentRefs, overriding any user-provided values", func() {
customPort := gatewayv1.PortNumber(9999)
customSectionName := gatewayv1.SectionName("custom")
tcp.Spec.ControlPlane.Gateway.GatewayParentRefs[0].Port = &customPort
tcp.Spec.ControlPlane.Gateway.GatewayParentRefs[0].SectionName = &customSectionName
err := resource.Define(ctx, tcp)
Expect(err).NotTo(HaveOccurred())
_, err = resource.CreateOrUpdate(ctx, tcp)
Expect(err).NotTo(HaveOccurred())
route := &gatewayv1alpha2.TLSRoute{}
err = resource.Client.Get(ctx, client.ObjectKey{Name: tcp.Name, Namespace: tcp.Namespace}, route)
Expect(err).NotTo(HaveOccurred())
Expect(route.Spec.ParentRefs).To(HaveLen(1))
Expect(route.Spec.ParentRefs[0].Port).NotTo(BeNil())
Expect(*route.Spec.ParentRefs[0].Port).To(Equal(tcp.Status.Kubernetes.Service.Port))
Expect(*route.Spec.ParentRefs[0].Port).NotTo(Equal(customPort))
Expect(route.Spec.ParentRefs[0].SectionName).NotTo(BeNil())
Expect(*route.Spec.ParentRefs[0].SectionName).To(Equal(gatewayv1.SectionName("kube-apiserver")))
Expect(*route.Spec.ParentRefs[0].SectionName).NotTo(Equal(customSectionName))
})
It("should handle multiple parentRefs correctly", func() {
namespace := gatewayv1.Namespace("default")
tcp.Spec.ControlPlane.Gateway.GatewayParentRefs = []gatewayv1alpha2.ParentReference{
@@ -149,13 +125,12 @@ var _ = Describe("KubernetesGatewayResource", func() {
err = resource.Client.Get(ctx, client.ObjectKey{Name: tcp.Name, Namespace: tcp.Namespace}, route)
Expect(err).NotTo(HaveOccurred())
Expect(route.Spec.ParentRefs).To(HaveLen(2))
for i := range route.Spec.ParentRefs {
Expect(route.Spec.ParentRefs[i].Port).NotTo(BeNil())
Expect(*route.Spec.ParentRefs[i].Port).To(Equal(tcp.Status.Kubernetes.Service.Port))
Expect(route.Spec.ParentRefs[i].SectionName).NotTo(BeNil())
Expect(*route.Spec.ParentRefs[i].SectionName).To(Equal(gatewayv1.SectionName("kube-apiserver")))
}
Expect(route.Spec.ParentRefs[0].Name).To(Equal(gatewayv1alpha2.ObjectName("test-gateway-1")))
Expect(route.Spec.ParentRefs[0].Namespace).NotTo(BeNil())
Expect(*route.Spec.ParentRefs[0].Namespace).To(Equal(namespace))
Expect(route.Spec.ParentRefs[1].Name).To(Equal(gatewayv1alpha2.ObjectName("test-gateway-2")))
Expect(route.Spec.ParentRefs[1].Namespace).NotTo(BeNil())
Expect(*route.Spec.ParentRefs[1].Namespace).To(Equal(namespace))
})
})
@@ -292,81 +267,4 @@ var _ = Describe("KubernetesGatewayResource", func() {
Expect(listener.Port).To(Equal(gatewayv1.PortNumber(80)))
})
})
Describe("NewParentRefsSpecWithPortAndSection", func() {
var (
parentRefs []gatewayv1.ParentReference
testPort int32
testSectionName string
)
BeforeEach(func() {
namespace := gatewayv1.Namespace("default")
namespace2 := gatewayv1.Namespace("other")
testPort = int32(6443)
testSectionName = "kube-apiserver"
originalPort := gatewayv1.PortNumber(9999)
originalSectionName := gatewayv1.SectionName("original")
parentRefs = []gatewayv1.ParentReference{
{
Name: "test-gateway-1",
Namespace: &namespace,
Port: &originalPort,
SectionName: &originalSectionName,
},
{
Name: "test-gateway-2",
Namespace: &namespace2,
},
}
})
It("should create copy of parentRefs with port and sectionName set", func() {
result := resources.NewParentRefsSpecWithPortAndSection(parentRefs, testPort, testSectionName)
Expect(result).To(HaveLen(2))
for i := range result {
Expect(result[i].Name).To(Equal(parentRefs[i].Name))
Expect(result[i].Namespace).To(Equal(parentRefs[i].Namespace))
Expect(result[i].Port).NotTo(BeNil())
Expect(*result[i].Port).To(Equal(testPort))
Expect(result[i].SectionName).NotTo(BeNil())
Expect(*result[i].SectionName).To(Equal(gatewayv1.SectionName(testSectionName)))
}
})
It("should not modify original parentRefs", func() {
// Store original values for verification
originalFirstPort := parentRefs[0].Port
originalFirstSectionName := parentRefs[0].SectionName
originalSecondPort := parentRefs[1].Port
originalSecondSectionName := parentRefs[1].SectionName
result := resources.NewParentRefsSpecWithPortAndSection(parentRefs, testPort, testSectionName)
// Original should remain unchanged
Expect(parentRefs[0].Port).To(Equal(originalFirstPort))
Expect(parentRefs[0].SectionName).To(Equal(originalFirstSectionName))
Expect(parentRefs[1].Port).To(Equal(originalSecondPort))
Expect(parentRefs[1].SectionName).To(Equal(originalSecondSectionName))
// Result should have new values
Expect(result[0].Port).NotTo(BeNil())
Expect(*result[0].Port).To(Equal(testPort))
Expect(result[0].SectionName).NotTo(BeNil())
Expect(*result[0].SectionName).To(Equal(gatewayv1.SectionName(testSectionName)))
Expect(result[1].Port).NotTo(BeNil())
Expect(*result[1].Port).To(Equal(testPort))
Expect(result[1].SectionName).NotTo(BeNil())
Expect(*result[1].SectionName).To(Equal(gatewayv1.SectionName(testSectionName)))
})
It("should handle empty parentRefs slice", func() {
parentRefs = []gatewayv1.ParentReference{}
result := resources.NewParentRefsSpecWithPortAndSection(parentRefs, testPort, testSectionName)
Expect(result).To(BeEmpty())
})
})
})

View File

@@ -226,16 +226,3 @@ func BuildGatewayAccessPointsStatus(ctx context.Context, c client.Client, route
return accessPoints, nil
}
// NewParentRefsSpecWithPortAndSection creates a copy of parentRefs with port and sectionName set for each reference.
func NewParentRefsSpecWithPortAndSection(parentRefs []gatewayv1.ParentReference, port int32, sectionName string) []gatewayv1.ParentReference {
result := make([]gatewayv1.ParentReference, len(parentRefs))
sectionNamePtr := gatewayv1.SectionName(sectionName)
for i, parentRef := range parentRefs {
result[i] = *parentRef.DeepCopy()
result[i].Port = &port
result[i].SectionName = &sectionNamePtr
}
return result
}

View File

@@ -48,7 +48,7 @@ func (r *KubernetesServiceResource) CleanUp(context.Context, *kamajiv1alpha1.Ten
}
func (r *KubernetesServiceResource) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
tenantControlPlane.Status.Kubernetes.Service.ServiceStatus = r.resource.Status
tenantControlPlane.Status.Kubernetes.Service.ServiceStatus = StripLoadBalancerPortsFromServiceStatus(r.resource.Status)
tenantControlPlane.Status.Kubernetes.Service.Name = r.resource.GetName()
tenantControlPlane.Status.Kubernetes.Service.Namespace = r.resource.GetNamespace()
tenantControlPlane.Status.Kubernetes.Service.Port = r.resource.Spec.Ports[0].Port

View File

@@ -182,7 +182,7 @@ func (r *KubernetesKonnectivityGatewayResource) mutate(tcp *kamajiv1alpha1.Tenan
if tcp.Spec.ControlPlane.Gateway.GatewayParentRefs == nil {
return fmt.Errorf("control plane gateway parentRefs are not specified")
}
r.resource.Spec.ParentRefs = resources.NewParentRefsSpecWithPortAndSection(tcp.Spec.ControlPlane.Gateway.GatewayParentRefs, servicePort, "konnectivity-server")
r.resource.Spec.ParentRefs = newParentRefsSpecWithPortAndSection(tcp.Spec.ControlPlane.Gateway.GatewayParentRefs, servicePort, "konnectivity-server")
rule := gatewayv1alpha2.TLSRouteRule{
BackendRefs: []gatewayv1alpha2.BackendRef{
@@ -230,3 +230,16 @@ func (r *KubernetesKonnectivityGatewayResource) CreateOrUpdate(ctx context.Conte
func (r *KubernetesKonnectivityGatewayResource) GetName() string {
return "konnectivity_gateway_routes"
}
// newParentRefsSpecWithPortAndSection creates a copy of parentRefs with port and sectionName set for each reference.
func newParentRefsSpecWithPortAndSection(parentRefs []gatewayv1.ParentReference, port int32, sectionName string) []gatewayv1.ParentReference {
result := make([]gatewayv1.ParentReference, len(parentRefs))
sectionNamePtr := gatewayv1.SectionName(sectionName)
for i, parentRef := range parentRefs {
result[i] = *parentRef.DeepCopy()
result[i].Port = &port
result[i].SectionName = &sectionNamePtr
}
return result
}

View File

@@ -113,7 +113,7 @@ func (r *ServiceResource) UpdateTenantControlPlaneStatus(_ context.Context, tena
tenantControlPlane.Status.Addons.Konnectivity.Service.Name = r.resource.GetName()
tenantControlPlane.Status.Addons.Konnectivity.Service.Namespace = r.resource.GetNamespace()
tenantControlPlane.Status.Addons.Konnectivity.Service.Port = r.resource.Spec.Ports[1].Port
tenantControlPlane.Status.Addons.Konnectivity.Service.ServiceStatus = r.resource.Status
tenantControlPlane.Status.Addons.Konnectivity.Service.ServiceStatus = resources.StripLoadBalancerPortsFromServiceStatus(r.resource.Status)
}
return nil

View File

@@ -132,3 +132,18 @@ func getStoredKubeadmConfiguration(ctx context.Context, client client.Client, tm
return config, nil
}
func StripLoadBalancerPortsFromServiceStatus(s corev1.ServiceStatus) corev1.ServiceStatus {
sanitized := s
if len(s.LoadBalancer.Ingress) > 0 {
sanitized.LoadBalancer.Ingress = make([]corev1.LoadBalancerIngress, len(s.LoadBalancer.Ingress))
copy(sanitized.LoadBalancer.Ingress, s.LoadBalancer.Ingress)
}
for i := range sanitized.LoadBalancer.Ingress {
sanitized.LoadBalancer.Ingress[i].Ports = nil
}
return sanitized
}

View File

@@ -0,0 +1,111 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"testing"
corev1 "k8s.io/api/core/v1"
)
func TestStripLoadBalancerPortsFromServiceStatus(t *testing.T) {
ipModeProxy := corev1.LoadBalancerIPModeProxy
tests := []struct {
name string
input corev1.ServiceStatus
assert func(t *testing.T, orig, got corev1.ServiceStatus)
}{
{
name: "ip ingress with ports and ipMode",
input: corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{
{
IP: "172.18.0.3",
IPMode: &ipModeProxy,
Ports: []corev1.PortStatus{
{Port: 6443, Protocol: corev1.ProtocolTCP},
{Port: 8132, Protocol: corev1.ProtocolTCP},
},
},
},
},
},
assert: func(t *testing.T, orig, got corev1.ServiceStatus) {
t.Helper()
if got.LoadBalancer.Ingress[0].Ports != nil {
t.Fatalf("expected ports stripped, got %#v", got.LoadBalancer.Ingress[0].Ports)
}
if got.LoadBalancer.Ingress[0].IP != "172.18.0.3" {
t.Fatalf("IP not preserved")
}
if got.LoadBalancer.Ingress[0].IPMode == nil || *got.LoadBalancer.Ingress[0].IPMode != ipModeProxy {
t.Fatalf("IPMode not preserved")
}
if orig.LoadBalancer.Ingress[0].Ports == nil {
t.Fatalf("original ports mutated")
}
},
},
{
name: "hostname ingress with ports",
input: corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{
{
Hostname: "example.local",
Ports: []corev1.PortStatus{
{Port: 6443, Protocol: corev1.ProtocolTCP},
},
},
},
},
},
assert: func(t *testing.T, orig, got corev1.ServiceStatus) {
t.Helper()
if got.LoadBalancer.Ingress[0].Ports != nil {
t.Fatalf("expected ports stripped")
}
if got.LoadBalancer.Ingress[0].Hostname != "example.local" {
t.Fatalf("hostname not preserved")
}
},
},
{
name: "no ingress",
input: corev1.ServiceStatus{},
assert: func(t *testing.T, _, got corev1.ServiceStatus) {
t.Helper()
if len(got.LoadBalancer.Ingress) != 0 {
t.Fatalf("expected no ingress")
}
},
},
{
name: "ingress with nil ports",
input: corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{
{IP: "10.0.0.1"},
},
},
},
assert: func(t *testing.T, _, got corev1.ServiceStatus) {
t.Helper()
if got.LoadBalancer.Ingress[0].Ports != nil {
t.Fatalf("expected ports to stay nil")
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
orig := tt.input
got := StripLoadBalancerPortsFromServiceStatus(tt.input)
tt.assert(t, orig, got)
})
}
}

View File

@@ -1,144 +0,0 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package handlers
import (
"context"
"fmt"
"gomodules.xyz/jsonpatch/v2"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
)
type DataStoreValidation struct {
Client client.Client
}
func (d DataStoreValidation) OnCreate(object runtime.Object) AdmissionResponse {
return func(ctx context.Context, _ admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
ds := object.(*kamajiv1alpha1.DataStore) //nolint:forcetypeassert
return nil, d.validate(ctx, *ds)
}
}
func (d DataStoreValidation) OnDelete(object runtime.Object) AdmissionResponse {
return func(ctx context.Context, _ admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
ds := object.(*kamajiv1alpha1.DataStore) //nolint:forcetypeassert
tcpList := &kamajiv1alpha1.TenantControlPlaneList{}
if err := d.Client.List(ctx, tcpList, client.MatchingFieldsSelector{Selector: fields.OneTermEqualSelector(kamajiv1alpha1.TenantControlPlaneUsedDataStoreKey, ds.GetName())}); err != nil {
return nil, fmt.Errorf("cannot retrieve TenantControlPlane list used by the DataStore: %w", err)
}
if len(tcpList.Items) > 0 {
return nil, fmt.Errorf("the DataStore is used by multiple TenantControlPlanes and cannot be removed")
}
return nil, nil
}
}
func (d DataStoreValidation) OnUpdate(object runtime.Object, oldObj runtime.Object) AdmissionResponse {
return func(ctx context.Context, _ admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
newDs, oldDs := object.(*kamajiv1alpha1.DataStore), oldObj.(*kamajiv1alpha1.DataStore) //nolint:forcetypeassert
if oldDs.Spec.Driver != newDs.Spec.Driver {
return nil, fmt.Errorf("driver of a DataStore cannot be changed")
}
return nil, d.validate(ctx, *newDs)
}
}
func (d DataStoreValidation) validate(ctx context.Context, ds kamajiv1alpha1.DataStore) error {
if ds.Spec.BasicAuth != nil {
if err := d.validateBasicAuth(ctx, ds); err != nil {
return err
}
}
return d.validateTLSConfig(ctx, ds)
}
func (d DataStoreValidation) validateBasicAuth(ctx context.Context, ds kamajiv1alpha1.DataStore) error {
if err := d.validateContentReference(ctx, ds.Spec.BasicAuth.Password); err != nil {
return fmt.Errorf("basic-auth password is not valid, %w", err)
}
if err := d.validateContentReference(ctx, ds.Spec.BasicAuth.Username); err != nil {
return fmt.Errorf("basic-auth username is not valid, %w", err)
}
return nil
}
func (d DataStoreValidation) validateTLSConfig(ctx context.Context, ds kamajiv1alpha1.DataStore) error {
if ds.Spec.TLSConfig == nil && ds.Spec.Driver != kamajiv1alpha1.EtcdDriver {
return nil
}
if err := d.validateContentReference(ctx, ds.Spec.TLSConfig.CertificateAuthority.Certificate); err != nil {
return fmt.Errorf("CA certificate is not valid, %w", err)
}
if ds.Spec.Driver == kamajiv1alpha1.EtcdDriver {
if ds.Spec.TLSConfig.CertificateAuthority.PrivateKey == nil {
return fmt.Errorf("CA private key is required when using the etcd driver")
}
if ds.Spec.TLSConfig.ClientCertificate == nil {
return fmt.Errorf("client certificate is required when using the etcd driver")
}
}
if ds.Spec.TLSConfig.CertificateAuthority.PrivateKey != nil {
if err := d.validateContentReference(ctx, *ds.Spec.TLSConfig.CertificateAuthority.PrivateKey); err != nil {
return fmt.Errorf("CA private key is not valid, %w", err)
}
}
if ds.Spec.TLSConfig.ClientCertificate != nil {
if err := d.validateContentReference(ctx, ds.Spec.TLSConfig.ClientCertificate.Certificate); err != nil {
return fmt.Errorf("client certificate is not valid, %w", err)
}
if err := d.validateContentReference(ctx, ds.Spec.TLSConfig.ClientCertificate.PrivateKey); err != nil {
return fmt.Errorf("client private key is not valid, %w", err)
}
}
return nil
}
func (d DataStoreValidation) validateContentReference(ctx context.Context, ref kamajiv1alpha1.ContentRef) error {
switch {
case len(ref.Content) > 0:
return nil
case ref.SecretRef == nil:
return fmt.Errorf("the Secret reference is mandatory when bare content is not specified")
case len(ref.SecretRef.SecretReference.Name) == 0:
return fmt.Errorf("the Secret reference name is mandatory")
case len(ref.SecretRef.SecretReference.Namespace) == 0:
return fmt.Errorf("the Secret reference namespace is mandatory")
}
if err := d.Client.Get(ctx, types.NamespacedName{Name: ref.SecretRef.SecretReference.Name, Namespace: ref.SecretRef.SecretReference.Namespace}, &corev1.Secret{}); err != nil {
if k8serrors.IsNotFound(err) {
return fmt.Errorf("secret %s/%s is not found", ref.SecretRef.SecretReference.Namespace, ref.SecretRef.SecretReference.Name)
}
return err
}
return nil
}

View File

@@ -1,21 +0,0 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package routes
import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
)
//+kubebuilder:webhook:path=/validate--v1-secret,mutating=false,failurePolicy=ignore,sideEffects=None,groups="",resources=secrets,verbs=delete,versions=v1,name=vdatastoresecrets.kb.io,admissionReviewVersions=v1
type DataStoreSecrets struct{}
func (d DataStoreSecrets) GetPath() string {
return "/validate--v1-secret"
}
func (d DataStoreSecrets) GetObject() runtime.Object {
return &corev1.Secret{}
}

View File

@@ -1,22 +0,0 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package routes
import (
"k8s.io/apimachinery/pkg/runtime"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
)
//+kubebuilder:webhook:path=/validate-kamaji-clastix-io-v1alpha1-datastore,mutating=false,failurePolicy=fail,sideEffects=None,groups=kamaji.clastix.io,resources=datastores,verbs=create;update;delete,versions=v1alpha1,name=vdatastore.kb.io,admissionReviewVersions=v1
type DataStoreValidate struct{}
func (d DataStoreValidate) GetPath() string {
return "/validate-kamaji-clastix-io-v1alpha1-datastore"
}
func (d DataStoreValidate) GetObject() runtime.Object {
return &kamajiv1alpha1.DataStore{}
}