Compare commits

...

34 Commits

Author SHA1 Message Date
Alfredo Suarez
880b36e0fa feat: gateway api support (#1000)
* Feat: Gateway Routes Specs, plus resource and status init progress

* Generated content, RBAC and start of e2e

* latest code POC Working but e2e fails

* Use Gateway API v1.2.0

* Remove draft comment

* Use TCPRoute

* Revert the charts folder to reduce noise

* Use the correct controller-gen version

* Rename fields and fix tcp/tls typos

* Rename TLSRouteSpec to GatewayRouteSpec

* Remove last instance of tcproute

* Renaming more fields to match the gateway api naming

* Remove ownership of the gateway

* Revert Ko to 0.14.1 and makefile comments

* service discovery, webhooks, and deadcode removal.

* add conditional check for gateway api resources and mark is as owned!

* removing duplicated code and note for maybe a refactor later

* E2E now works!

* e2e suite modifications to support Gateway API v1alpha2 TLSRoute

* Suggestions commit, naming and other related.

* First pass at the status update

* Rename route to gateway

* Only allow one hostname in gateway

* Update status types

* WIP: testing conditions

* Update status API

* Add tests

* Detect endpoint

* Update manifests

* Remove old code and use proper condition check

* Fix compilation error

* Watch the Gateway resources

* Rename fields

* Add missing port

* Add ingress endpoint to the kubeadm

* Error if access points are empty

* Check the spec and status to delay the creation of the kubeadm

* Use the spec for the hostname

* Update api/v1alpha1/tenantcontrolplane_types.go

Co-authored-by: Dario Tranchitella <dario@tranchitella.eu>

* PR fixes, CEL k8s validations, proper status updates checks

* more context and separation of functions

* resolve all pr comments, with indexer

* merge master - go {sum,mod} updates dependabot

* Feat: Gateway Routes Specs, plus resource and status init progress

* Use Gateway API v1.2.0

* merge master - go {sum,mod} updates dependabot

* sum go mod tidy

* leftover comments

* clean go.sum

* fix: missing generated crds spec

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

* docs: gateway api support

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

* golint comments

* linting and test fix.

* Gateway API resource watching was made conditional to prevent crashes when CRDs are absent, and TLSRoute creation now returns an error when the service isn't ready instead of creating invalid resources with empty rules.

* unit test was incorrect after all the fixes we did, gracefull errors are not expected due to conditional adds

* fix(conditional-indexer): Gateway Indexer should also be conditional

* fix(conditional-indexer): Gateway Indexer should also be conditional

---------

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>
Co-authored-by: Hadrien Kohl <hadrien.kohl@gmail.com>
Co-authored-by: Dario Tranchitella <dario@tranchitella.eu>
2025-11-26 10:34:09 +01:00
dependabot[bot]
ac7da57454 chore(ci): bump actions/checkout from 5 to 6 (#1016)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-23 11:21:10 +01:00
Matthieu ROBIN
20cc50b748 chore(adopters): add Hikube as a new vendor 2025-11-23 11:20:54 +01:00
Dario Tranchitella
4956790e2b feat: loose control over k8s patch version (#1014)
* feat: loose control over k8s patch version

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

* chore(ci): fixing no space left on device

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

---------

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>
2025-11-20 10:18:37 +01:00
Dario Tranchitella
9069c9be47 docs: cluster autoscaler provisioning request (#1015)
* docs: refactoring cluster autoscaler docs

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

* fix(docs): missing required cluster autoscaler annotations

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

* docs: cluster autoscaler provisioningrequest support

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

* docs: updating to latest capi cp provider spec

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

---------

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>
2025-11-19 19:55:49 +01:00
dependabot[bot]
be33e55c11 feat(deps): bump k8s.io/kubernetes in the k8s group (#1008)
Bumps the k8s group with 1 update: [k8s.io/kubernetes](https://github.com/kubernetes/kubernetes).


Updates `k8s.io/kubernetes` from 1.34.1 to 1.34.2
- [Release notes](https://github.com/kubernetes/kubernetes/releases)
- [Commits](https://github.com/kubernetes/kubernetes/compare/v1.34.1...v1.34.2)

---
updated-dependencies:
- dependency-name: k8s.io/kubernetes
  dependency-version: 1.34.2
  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>
2025-11-14 15:31:06 +01:00
dependabot[bot]
db94780ab5 feat(deps): bump the etcd group with 2 updates (#1009)
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.5 to 3.6.6
- [Release notes](https://github.com/etcd-io/etcd/releases)
- [Commits](https://github.com/etcd-io/etcd/compare/v3.6.5...v3.6.6)

Updates `go.etcd.io/etcd/client/v3` from 3.6.5 to 3.6.6
- [Release notes](https://github.com/etcd-io/etcd/releases)
- [Commits](https://github.com/etcd-io/etcd/compare/v3.6.5...v3.6.6)

---
updated-dependencies:
- dependency-name: go.etcd.io/etcd/api/v3
  dependency-version: 3.6.6
  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.6
  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>
2025-11-13 06:20:09 +01:00
Léonard Suslian
ae80c50eb3 feat: add mistral as adopter (#1007) 2025-11-12 10:49:41 +01:00
dependabot[bot]
6244a8354c feat(deps): bump github.com/testcontainers/testcontainers-go (#1004)
Bumps [github.com/testcontainers/testcontainers-go](https://github.com/testcontainers/testcontainers-go) from 0.39.0 to 0.40.0.
- [Release notes](https://github.com/testcontainers/testcontainers-go/releases)
- [Commits](https://github.com/testcontainers/testcontainers-go/compare/v0.39.0...v0.40.0)

---
updated-dependencies:
- dependency-name: github.com/testcontainers/testcontainers-go
  dependency-version: 0.40.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>
2025-11-08 10:10:18 +01:00
dependabot[bot]
3e7c102728 feat(deps): bump sigs.k8s.io/controller-runtime from 0.22.3 to 0.22.4 (#1002)
Bumps [sigs.k8s.io/controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) from 0.22.3 to 0.22.4.
- [Release notes](https://github.com/kubernetes-sigs/controller-runtime/releases)
- [Changelog](https://github.com/kubernetes-sigs/controller-runtime/blob/main/RELEASE.md)
- [Commits](https://github.com/kubernetes-sigs/controller-runtime/compare/v0.22.3...v0.22.4)

---
updated-dependencies:
- dependency-name: sigs.k8s.io/controller-runtime
  dependency-version: 0.22.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-06 20:30:06 +01:00
dependabot[bot]
351f977d21 feat(deps): bump github.com/docker/docker (#1003)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.5.1+incompatible to 28.5.2+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.5.1...v28.5.2)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-version: 28.5.2+incompatible
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-06 20:29:58 +01:00
Dario Tranchitella
081b4c72b3 feat: additional service ports (#999)
* feat: additional service ports

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

* docs: additional service ports

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

---------

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>
2025-10-30 13:22:15 +01:00
dependabot[bot]
cb8086754b feat(deps): bump github.com/onsi/ginkgo/v2 from 2.27.1 to 2.27.2 (#998)
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.27.1 to 2.27.2.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.27.1...v2.27.2)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-version: 2.27.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-28 08:58:52 +01:00
dependabot[bot]
34c02a96f6 feat(deps): bump github.com/onsi/ginkgo/v2 from 2.26.0 to 2.27.1 (#995)
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.26.0 to 2.27.1.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.26.0...v2.27.1)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-version: 2.27.1
  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>
2025-10-27 10:37:44 +01:00
TAnderson
f29a10ba5f correction (#996)
Co-authored-by: Johannes Ibald <johannes.ibald@etes.de>
2025-10-23 14:49:45 +02:00
athena967
0656dbb803 Netalia vendor (#994)
* Update ADOPTERS.md with Netalia as vendor.
2025-10-23 10:31:59 +02:00
Adriano Pezzuto
99fb71ecf9 fix(helm): use 0.0.0+latest for kamaji-crd (#982)
Co-authored-by: bsctl <bsctl@clastix.io>
2025-10-18 16:01:24 +02:00
Adriano Pezzuto
da180c9ce0 feat(docs): document how to install helm kamaji-crds (#983)
* feat(docs): document how to install helm kamaji-crds

* fix(docs): document how to install helm kamaji-crds

---------

Co-authored-by: bsctl <bsctl@clastix.io>
2025-10-18 16:01:04 +02:00
dependabot[bot]
4cced97991 feat(deps): bump github.com/docker/docker (#985)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.5.0+incompatible to 28.5.1+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.5.0...v28.5.1)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-version: 28.5.1+incompatible
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-18 16:00:17 +02:00
dependabot[bot]
a00e4544f9 feat(deps): bump sigs.k8s.io/controller-runtime from 0.22.1 to 0.22.3 (#988)
Bumps [sigs.k8s.io/controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) from 0.22.1 to 0.22.3.
- [Release notes](https://github.com/kubernetes-sigs/controller-runtime/releases)
- [Changelog](https://github.com/kubernetes-sigs/controller-runtime/blob/main/RELEASE.md)
- [Commits](https://github.com/kubernetes-sigs/controller-runtime/compare/v0.22.1...v0.22.3)

---
updated-dependencies:
- dependency-name: sigs.k8s.io/controller-runtime
  dependency-version: 0.22.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-18 16:00:05 +02:00
dependabot[bot]
b1984dc66d feat(deps): bump github.com/nats-io/nats.go from 1.46.1 to 1.47.0 (#989)
Bumps [github.com/nats-io/nats.go](https://github.com/nats-io/nats.go) from 1.46.1 to 1.47.0.
- [Release notes](https://github.com/nats-io/nats.go/releases)
- [Commits](https://github.com/nats-io/nats.go/compare/v1.46.1...v1.47.0)

---
updated-dependencies:
- dependency-name: github.com/nats-io/nats.go
  dependency-version: 1.47.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>
2025-10-18 15:59:45 +02:00
Timofei Larkin
8d25078c47 feat: don't clobber 3rd-party labels (#992)
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-10-17 17:35:20 +02:00
dependabot[bot]
bc85d8b73c feat(deps): bump github.com/docker/docker (#978)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.4.0+incompatible to 28.5.0+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.4.0...v28.5.0)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-version: 28.5.0+incompatible
  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>
2025-10-06 14:12:20 +02:00
Dario Tranchitella
de459fb5da feat!: write permissions (#937)
* fix: decoding object only if requested

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

* feat(api): limiting write permissions

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

* feat: write permissions handlers, routes, and controller

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

* docs: write permissions

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

---------

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>
2025-10-03 14:30:58 +02:00
dependabot[bot]
2b707423ff feat(deps): bump github.com/onsi/ginkgo/v2 from 2.25.3 to 2.26.0 (#977)
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.25.3 to 2.26.0.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.25.3...v2.26.0)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-version: 2.26.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>
2025-10-03 14:02:10 +02:00
Loïc Brun
285cef0f02 fix(konnectivity): rotate certicate during certificate authority rotation (#976) 2025-10-02 16:17:59 +02:00
dependabot[bot]
f6686f6efa feat(deps): bump github.com/nats-io/nats.go from 1.46.0 to 1.46.1 (#973)
Bumps [github.com/nats-io/nats.go](https://github.com/nats-io/nats.go) from 1.46.0 to 1.46.1.
- [Release notes](https://github.com/nats-io/nats.go/releases)
- [Commits](https://github.com/nats-io/nats.go/compare/v1.46.0...v1.46.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 14:55:52 +02:00
Dario Tranchitella
2a809a79c4 docs(readme): managed kubernetes on hetzner article (#974) 2025-10-01 21:20:06 +02:00
Dario Tranchitella
f477df2a84 docs(readme): minikube medium article (#972) 2025-09-30 15:46:02 +02:00
Adriano Pezzuto
464dc7ef49 fix(docs): missing export env vars in capi proxmox sample (#971)
* fix(docs): missing export env vars in capi proxmox sample

* fix(docs): escape token in proxmox capi

---------

Co-authored-by: bsctl <bsctl@clastix.io>
2025-09-29 09:10:02 +02:00
dependabot[bot]
b550865da3 feat(deps): bump github.com/nats-io/nats.go from 1.45.0 to 1.46.0 (#968)
Bumps [github.com/nats-io/nats.go](https://github.com/nats-io/nats.go) from 1.45.0 to 1.46.0.
- [Release notes](https://github.com/nats-io/nats.go/releases)
- [Commits](https://github.com/nats-io/nats.go/compare/v1.45.0...v1.46.0)

---
updated-dependencies:
- dependency-name: github.com/nats-io/nats.go
  dependency-version: 1.46.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>
2025-09-23 18:18:45 +02:00
outbackdingo
89c8615ce4 fix(docs): exporting variables for proxmox-infra-provider (#969)
* update context in proxmox-infra-provider.md

* Update docs/content/cluster-api/proxmox-infra-provider.md

Co-authored-by: Dario Tranchitella <dario@tranchitella.eu>

---------

Co-authored-by: dingo <dingo@optimcloud.com>
Co-authored-by: Dario Tranchitella <dario@tranchitella.eu>
2025-09-23 18:18:32 +02:00
Dario Tranchitella
cb2152d5a7 feat: kubeconfig generator (#933)
* feat(api): kubeconfig generator

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

* refactor: abstracting enqueue to channel function

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

* fix: avoiding multiple context registration

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

* feat: kubeconfig generator

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

* docs: kubeconfig generator

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

* feat(helm): deployment for kubeconfig generator

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

---------

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>
2025-09-22 15:32:50 +02:00
dependabot[bot]
4bace03fc3 feat(deps): bump the etcd group with 2 updates (#966)
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.4 to 3.6.5
- [Release notes](https://github.com/etcd-io/etcd/releases)
- [Commits](https://github.com/etcd-io/etcd/compare/v3.6.4...v3.6.5)

Updates `go.etcd.io/etcd/client/v3` from 3.6.4 to 3.6.5
- [Release notes](https://github.com/etcd-io/etcd/releases)
- [Commits](https://github.com/etcd-io/etcd/compare/v3.6.4...v3.6.5)

---
updated-dependencies:
- dependency-name: go.etcd.io/etcd/api/v3
  dependency-version: 3.6.5
  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.5
  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>
2025-09-22 09:27:32 +00:00
79 changed files with 6687 additions and 320 deletions

View File

@@ -11,7 +11,7 @@ jobs:
name: integration
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
@@ -20,7 +20,7 @@ jobs:
name: lint
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
@@ -36,7 +36,7 @@ jobs:
name: diff
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v6

View File

@@ -35,7 +35,7 @@ jobs:
name: Kubernetes
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v6
@@ -46,5 +46,9 @@ jobs:
sudo apt-get install -y golang-cfssl
sudo swapoff -a
sudo modprobe br_netfilter
- name: install required Go tools
run: make kind ko helm ginkgo
- name: cleaning up go mod
run: go clean -modcache
- name: e2e testing
run: make e2e

View File

@@ -11,7 +11,7 @@ jobs:
name: diff
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- run: make -C charts/kamaji docs
@@ -23,7 +23,7 @@ jobs:
lint:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: azure/setup-helm@v4
with:
version: 3.3.4
@@ -40,7 +40,7 @@ jobs:
needs: [ "lint", "diff" ]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Publish Helm chart
uses: stefanprodan/helm-gh-pages@master
with:

View File

@@ -18,7 +18,7 @@ jobs:
ko:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v6

View File

@@ -12,7 +12,7 @@ jobs:
release:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: generating date metadata

View File

@@ -13,7 +13,9 @@ Feel free to open a Pull-Request to get yours listed.
| Vendor | Coredge | 2025 | [link](https://coredge.io/) | Coredge uses Kamaji in its K8saaS offering to save infrastructure costs in its Sovereign Cloud & AI Infrastructure Platform for end-user organisations. |
| Vendor | DCloud | 2024 | [link](https://dcloud.co.id) | DCloud is an Indonesian Cloud Provider using Kamaji to build and offer [Managed Kubernetes Service](https://dcloud.co.id/dkubes.html). |
| Vendor | Dinova | 2025 | [link](https://dinova.one/) | Dinova is an Italian cloud services provider that integrates Kamaji in its datacenters to offer fully managed Kubernetes clusters. |
| Vendor | Hikube | 2024 | [link](https://hikube.cloud/) | Hikube.cloud is a Swiss sovereign cloud platform with triple replication across three Swiss datacenters, offering enterprise-grade infrastructure with full data sovereignty. |
| End-user | KINX | 2024 | [link](https://kinx.net/?lang=en) | KINX is an Internet infrastructure service provider and will use kamaji for its new [Managed Kubernetes Service](https://kinx.net/service/cloud/kubernetes/intro/?lang=en). |
| Vendor | Netalia | 2025 | [link](https://www.netalia.it) | Netalia uses Kamaji for the Italian cloud
| Vendor | Netsons | 2023 | [link](https://www.netsons.com) | Netsons is an Italian hosting and cloud provider and uses Kamaji in its [Managed Kubernetes](https://www.netsons.com/kubernetes) offering. |
| Vendor | NVIDIA | 2024 | [link](https://github.com/NVIDIA/doca-platform) | DOCA Platform Framework manages provisioning and service orchestration for NVIDIA Bluefield DPUs. |
| R&D | Orange | 2024 | [link](https://gitlab.com/Orange-OpenSource/kanod) | Orange is a French telecommunications company using Kamaji for experimental research purpose, with Kanod research solution. |
@@ -29,6 +31,7 @@ Feel free to open a Pull-Request to get yours listed.
| R&D | IONOS Cloud | 2024 | [link](https://cloud.ionos.com/) | IONOS Cloud is a German Cloud Provider evaluating Kamaji for its [Managed Kubernetes platform](https://cloud.ionos.com/managed/kubernetes). |
| Vendor | OVHCloud | 2025 | [link](https://www.ovhcloud.com/) | OVHCloud is a European Cloud Provider that will use Kamaji for its Managed Kubernetes Service offer. |
| Vendor | WOBCOM GmbH | 2024 | [link](https://www.wobcom.de/) | WOBCOM provides an [**Open Digital Platform**](https://www.wobcom.de/geschaeftskunden/odp/) solution for Smart Cities, which is provided for customers in a Managed Kubernetes provided by Kamaji. |
| Vendor | Mistral AI | 2025 | [link](https://mistral.ai/products/mistral-compute) | Mistral provides a baremetal kubernetes service that uses Kamaji for control plane management. |
### Adopter Types

View File

@@ -131,12 +131,14 @@ webhook: controller-gen yq
crds: controller-gen yq
# kamaji chart
$(CONTROLLER_GEN) crd webhook paths="./..." output:stdout | $(YQ) 'select(documentIndex == 0)' > ./charts/kamaji/crds/kamaji.clastix.io_datastores.yaml
$(CONTROLLER_GEN) crd webhook paths="./..." output:stdout | $(YQ) 'select(documentIndex == 1)' > ./charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml
$(CONTROLLER_GEN) crd webhook paths="./..." output:stdout | $(YQ) 'select(documentIndex == 1)' > ./charts/kamaji/crds/kamaji.clastix.io_kubeconfiggenerators.yaml
$(CONTROLLER_GEN) crd webhook paths="./..." output:stdout | $(YQ) 'select(documentIndex == 2)' > ./charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml
$(YQ) -i '. *n load("./charts/kamaji/controller-gen/crd-conversion.yaml")' ./charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml
# kamaji-crds chart
cp ./charts/kamaji/controller-gen/crd-conversion.yaml ./charts/kamaji-crds/hack/crd-conversion.yaml
$(YQ) '.spec' ./charts/kamaji/crds/kamaji.clastix.io_datastores.yaml > ./charts/kamaji-crds/hack/kamaji.clastix.io_datastores_spec.yaml
$(YQ) '.spec' ./charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml > ./charts/kamaji-crds/hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml
$(YQ) '.spec' ./charts/kamaji/crds/kamaji.clastix.io_kubeconfiggenerators.yaml > ./charts/kamaji-crds/hack/kamaji.clastix.io_kubeconfiggenerators_spec.yaml
$(YQ) -i '.conversion.webhook.clientConfig.service.name = "{{ .Values.kamajiService }}"' ./charts/kamaji-crds/hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml
$(YQ) -i '.conversion.webhook.clientConfig.service.namespace = "{{ .Values.kamajiNamespace }}"' ./charts/kamaji-crds/hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml
@@ -238,6 +240,12 @@ 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 wait --for=condition=Established crd/gateways.gateway.networking.k8s.io --timeout=60s
load: kind
$(KIND) load docker-image --name kamaji ${CONTAINER_REPOSITORY}:${VERSION}
@@ -247,8 +255,11 @@ load: kind
env: kind
$(KIND) create cluster --name kamaji
cleanup: kind
$(KIND) delete cluster --name kamaji
.PHONY: e2e
e2e: env build load helm ginkgo cert-manager ## Create a KinD cluster, install Kamaji on it and run the test suite.
e2e: env build load helm ginkgo cert-manager gateway-api ## Create a KinD cluster, install Kamaji on it and run the test suite.
$(HELM) upgrade --debug --install kamaji-crds ./charts/kamaji-crds --create-namespace --namespace kamaji-system
$(HELM) repo add clastix https://clastix.github.io/charts
$(HELM) dependency build ./charts/kamaji

View File

@@ -7,6 +7,15 @@ plugins:
projectName: operator
repo: github.com/clastix/kamaji
resources:
- api:
crdVersion: v1
namespaced: false
controller: true
domain: clastix.io
group: kamaji
kind: KubeconfigGenerator
path: github.com/clastix/kamaji/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true

View File

@@ -123,6 +123,8 @@ Since Kamaji is just focusing on the Control Plane a [Kamaji's Cluster API Contr
- YouTube ▶️ [Rancher & Kamaji: solving multitenancy challenges in the Kubernetes world](https://www.youtube.com/watch?v=VXHNrMmlF8U)
- YouTube ▶️ [Enabling Self-Service Kubernetes clusters with Kamaji and Paralus](https://www.youtube.com/watch?v=JWA2LwZazM0)
- YouTube ▶️ [Hosted Control Plane on Kubernetes (HPC) with Kamaji and K0mostron by Hervé Leclerc, ALTER WAY](https://www.youtube.com/watch?v=vmRdE2ngn78)
- Medium 📖 [Set up Virtual Control Planes with Kamaji on Minikube, by Ben Soer](https://medium.com/@bensoer/set-up-virtual-control-planes-with-kamaji-on-minikube-a540be0275aa)
- Hands-On tutorial 📖 [How to build your own managed Kubernetes service on Hetzner Cloud, by Hans Jörg Wieland](https://wieland.tech/blog/kamaji-cluster-api-and-etcd)
### 🏷️ Versioning

View File

@@ -4,7 +4,6 @@
// Package v1alpha1 contains API Schema definitions for the kamaji v1alpha1 API group
// +kubebuilder:object:generate=true
// +groupName=kamaji.clastix.io
//nolint
package v1alpha1
import (

View File

@@ -0,0 +1,47 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
"context"
"fmt"
controllerruntime "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
)
const (
GatewayListenerNameKey = "spec.listeners.name"
)
type GatewayListener struct{}
func (g *GatewayListener) Object() client.Object {
return &gatewayv1.Gateway{}
}
func (g *GatewayListener) Field() string {
return GatewayListenerNameKey
}
func (g *GatewayListener) ExtractValue() client.IndexerFunc {
return func(object client.Object) []string {
gateway := object.(*gatewayv1.Gateway) //nolint:forcetypeassert
listenerNames := make([]string, 0, len(gateway.Spec.Listeners))
for _, listener := range gateway.Spec.Listeners {
// Create a composite key: namespace/gatewayName/listenerName
// This allows us to look up gateways by listener name while ensuring uniqueness
key := fmt.Sprintf("%s/%s/%s", gateway.Namespace, gateway.Name, listener.Name)
listenerNames = append(listenerNames, key)
}
return listenerNames
}
}
func (g *GatewayListener) SetupWithManager(ctx context.Context, mgr controllerruntime.Manager) error {
return mgr.GetFieldIndexer().IndexField(ctx, g.Object(), g.Field(), g.ExtractValue())
}

View File

@@ -0,0 +1,91 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var (
ManagedByLabel = "kamaji.clastix.io/managed-by"
ManagedForLabel = "kamaji.clastix.io/managed-for"
)
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+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"}
//+kubebuilder:resource:scope=Cluster,shortName=kc,categories=kamaji
// KubeconfigGenerator is the Schema for the kubeconfiggenerators API.
type KubeconfigGenerator struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec KubeconfigGeneratorSpec `json:"spec,omitempty"`
Status KubeconfigGeneratorStatus `json:"status,omitempty"`
}
// CompoundValue allows defining a static, or a dynamic value.
// Options are mutually exclusive, just one should be picked up.
// +kubebuilder:validation:XValidation:rule="(has(self.stringValue) || has(self.fromDefinition)) && !(has(self.stringValue) && has(self.fromDefinition))",message="Either stringValue or fromDefinition must be set, but not both."
type CompoundValue struct {
// StringValue is a static string value.
StringValue string `json:"stringValue,omitempty"`
// FromDefinition is used to generate a dynamic value,
// it uses the dot notation to access fields from the referenced TenantControlPlane object:
// e.g.: metadata.name
FromDefinition string `json:"fromDefinition,omitempty"`
}
type KubeconfigGeneratorSpec struct {
// NamespaceSelector is used to filter Namespaces from which the generator should extract TenantControlPlane objects.
NamespaceSelector metav1.LabelSelector `json:"namespaceSelector,omitempty"`
// TenantControlPlaneSelector is used to filter the TenantControlPlane objects that should be address by the generator.
TenantControlPlaneSelector metav1.LabelSelector `json:"tenantControlPlaneSelector,omitempty"`
// Groups is resolved a set of strings used to assign the x509 organisations field.
// It will be recognised by Kubernetes as user groups.
Groups []CompoundValue `json:"groups,omitempty"`
// User resolves to a string to identify the client, assigned to the x509 Common Name field.
User CompoundValue `json:"user"`
// ControlPlaneEndpointFrom is the key used to extract the Tenant Control Plane endpoint that must be used by the generator.
// The targeted Secret is the `${TCP}-admin-kubeconfig` one, default to `admin.svc`.
//+kubebuilder:default="admin.svc"
ControlPlaneEndpointFrom string `json:"controlPlaneEndpointFrom,omitempty"`
}
type KubeconfigGeneratorStatusError struct {
// Resource is the Namespaced name of the errored resource.
//+kubebuilder:validation:Required
Resource string `json:"resource"`
// Message is the error message recorded upon the last generator run.
//+kubebuilder:validation:Required
Message string `json:"message"`
}
// KubeconfigGeneratorStatus defines the observed state of KubeconfigGenerator.
type KubeconfigGeneratorStatus struct {
// Resources is the sum of targeted TenantControlPlane objects.
//+kubebuilder:default=0
Resources int `json:"resources"`
// AvailableResources is the sum of successfully generated resources.
// In case of a different value compared to Resources, check the field errors.
//+kubebuilder:default=0
AvailableResources int `json:"availableResources"`
// Errors is the list of failed kubeconfig generations.
Errors []KubeconfigGeneratorStatusError `json:"errors,omitempty"`
}
//+kubebuilder:object:root=true
// KubeconfigGeneratorList contains a list of TenantControlPlane.
type KubeconfigGeneratorList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []KubeconfigGenerator `json:"items"`
}
func init() {
SchemeBuilder.Register(&KubeconfigGenerator{}, &KubeconfigGeneratorList{})
}

View File

@@ -8,6 +8,7 @@ import (
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
)
// APIServerCertificatesStatus defines the observed state of ETCD Certificate for API server.
@@ -187,14 +188,17 @@ type KubernetesStatus struct {
Deployment KubernetesDeploymentStatus `json:"deployment,omitempty"`
Service KubernetesServiceStatus `json:"service,omitempty"`
Ingress *KubernetesIngressStatus `json:"ingress,omitempty"`
Gateway *KubernetesGatewayStatus `json:"gateway,omitempty"`
}
// +kubebuilder:validation:Enum=Provisioning;CertificateAuthorityRotating;Upgrading;Migrating;Ready;NotReady;Sleeping
// +kubebuilder:validation:Enum=Unknown;Provisioning;CertificateAuthorityRotating;Upgrading;Migrating;Ready;NotReady;Sleeping;WriteLimited
type KubernetesVersionStatus string
var (
VersionUnknown KubernetesVersionStatus = "Unknown"
VersionProvisioning KubernetesVersionStatus = "Provisioning"
VersionSleeping KubernetesVersionStatus = "Sleeping"
VersionWriteLimited KubernetesVersionStatus = "WriteLimited"
VersionCARotating KubernetesVersionStatus = "CertificateAuthorityRotating"
VersionUpgrading KubernetesVersionStatus = "Upgrading"
VersionMigrating KubernetesVersionStatus = "Migrating"
@@ -242,3 +246,25 @@ type KubernetesIngressStatus struct {
// The namespace which the Ingress for the given cluster is deployed.
Namespace string `json:"namespace"`
}
type GatewayAccessPoint struct {
Type *gatewayv1.AddressType `json:"type"`
Value string `json:"value"`
Port int32 `json:"port"`
URLs []string `json:"urls,omitempty"`
}
// +k8s:deepcopy-gen=false
type RouteStatus = gatewayv1.RouteStatus
// KubernetesGatewayStatus defines the status for the Tenant Control Plane Gateway in the management cluster.
type KubernetesGatewayStatus struct {
// The TLSRoute status as resported by the gateway controllers.
RouteStatus `json:",inline"`
// Reference to the route created for this tenant.
RouteRef corev1.LocalObjectReference `json:"routeRef,omitempty"`
// A list of valid access points that the route exposes.
AccessPoints []GatewayAccessPoint `json:"accessPoints,omitempty"`
}

View File

@@ -7,6 +7,8 @@ import (
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
)
// NetworkProfileSpec defines the desired state of NetworkProfile.
@@ -89,6 +91,32 @@ type KubernetesSpec struct {
AdmissionControllers AdmissionControllers `json:"admissionControllers,omitempty"`
}
type AdditionalPort struct {
// The name of this port within the Service created by Kamaji.
// This must be a DNS_LABEL, must have unique names, and cannot be `kube-apiserver`, or `konnectivity-server`.
Name string `json:"name"`
// The IP protocol for this port. Supports "TCP", "UDP", and "SCTP".
//+kubebuilder:validation:Enum=TCP;UDP;SCTP
//+kubebuilder:default=TCP
Protocol corev1.Protocol `json:"protocol,omitempty"`
// The application protocol for this port.
// This is used as a hint for implementations to offer richer behavior for protocols that they understand.
// This field follows standard Kubernetes label syntax.
// Valid values are either:
//
// * Un-prefixed protocol names - reserved for IANA standard service names (as per
// RFC-6335 and https://www.iana.org/assignments/service-names).
AppProtocol *string `json:"appProtocol,omitempty"`
// The port that will be exposed by this service.
Port int32 `json:"port"`
// Number or name of the port to access on the pods of the Tenant Control Plane.
// Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME.
// If this is a string, it will be looked up as a named port in the
// target Pod's container ports. If this is not specified, the value
// of the 'port' field is used (an identity map).
TargetPort intstr.IntOrString `json:"targetPort"`
}
// AdditionalMetadata defines which additional metadata, such as labels and annotations, must be attached to the created resource.
type AdditionalMetadata struct {
Labels map[string]string `json:"labels,omitempty"`
@@ -97,6 +125,7 @@ type AdditionalMetadata struct {
// ControlPlane defines how the Tenant Control Plane Kubernetes resources must be created in the Admin Cluster,
// such as the number of Pod replicas, the Service resource, or the Ingress.
// +kubebuilder:validation:XValidation:rule="!(has(self.ingress) && has(self.gateway))",message="using both ingress and gateway is not supported"
type ControlPlane struct {
// Defining the options for the deployed Tenant Control Plane as Deployment resource.
Deployment DeploymentSpec `json:"deployment,omitempty"`
@@ -104,6 +133,8 @@ type ControlPlane struct {
Service ServiceSpec `json:"service"`
// Defining the options for an Optional Ingress which will expose API Server of the Tenant Control Plane
Ingress *IngressSpec `json:"ingress,omitempty"`
// Defining the options for an Optional Gateway which will expose API Server of the Tenant Control Plane
Gateway *GatewaySpec `json:"gateway,omitempty"`
}
// IngressSpec defines the options for the ingress which will expose API Server of the Tenant Control Plane.
@@ -115,6 +146,16 @@ type IngressSpec struct {
Hostname string `json:"hostname,omitempty"`
}
// GatewaySpec defines the options for the Gateway which will expose API Server of the Tenant Control Plane.
type GatewaySpec struct {
// AdditionalMetadata to add Labels and Annotations support.
AdditionalMetadata AdditionalMetadata `json:"additionalMetadata,omitempty"`
// GatewayParentRefs is the class of the Gateway resource to use.
GatewayParentRefs []gatewayv1.ParentReference `json:"parentRefs,omitempty"`
// Hostname is an optional field which will be used as a route hostname.
Hostname gatewayv1.Hostname `json:"hostname,omitempty"`
}
type ControlPlaneComponentsResources struct {
APIServer *corev1.ResourceRequirements `json:"apiServer,omitempty"`
ControllerManager *corev1.ResourceRequirements `json:"controllerManager,omitempty"`
@@ -198,6 +239,9 @@ type ControlPlaneExtraArgs struct {
type ServiceSpec struct {
AdditionalMetadata AdditionalMetadata `json:"additionalMetadata,omitempty"`
// AdditionalPorts allows adding additional ports to the Service generated Kamaji
// which targets the Tenant Control Plane pods.
AdditionalPorts []AdditionalPort `json:"additionalPorts,omitempty"`
// ServiceType allows specifying how to expose the Tenant Control Plane.
ServiceType ServiceType `json:"serviceType"`
}
@@ -297,6 +341,20 @@ type AddonsSpec struct {
KubeProxy *AddonSpec `json:"kubeProxy,omitempty"`
}
type Permissions struct {
BlockCreate bool `json:"blockCreation,omitempty"`
BlockUpdate bool `json:"blockUpdate,omitempty"`
BlockDelete bool `json:"blockDeletion,omitempty"`
}
func (p *Permissions) HasAnyLimitation() bool {
if p.BlockCreate || p.BlockUpdate || p.BlockDelete {
return true
}
return false
}
// TenantControlPlaneSpec defines the desired state of TenantControlPlane.
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.dataStore) || has(self.dataStore)", message="unsetting the dataStore is not supported"
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.dataStoreSchema) || has(self.dataStoreSchema)", message="unsetting the dataStoreSchema is not supported"
@@ -306,6 +364,13 @@ type AddonsSpec struct {
// +kubebuilder:validation:XValidation:rule="self.controlPlane.service.serviceType != 'LoadBalancer' || (oldSelf.controlPlane.service.serviceType != 'LoadBalancer' && self.controlPlane.service.serviceType == 'LoadBalancer') || has(self.networkProfile.loadBalancerClass) == has(oldSelf.networkProfile.loadBalancerClass)",message="LoadBalancerClass cannot be set or unset at runtime"
type TenantControlPlaneSpec struct {
// WritePermissions allows to select which operations (create, delete, update) must be blocked:
// by default, all actions are allowed, and API Server can write to its Datastore.
//
// By blocking all actions, the Tenant Control Plane can enter in a Read Only mode:
// this phase can be used to prevent Datastore quota exhaustion or for your own business logic
// (e.g.: blocking creation and update, but allowing deletion to "clean up" space).
WritePermissions Permissions `json:"writePermissions,omitempty"`
// DataStore specifies the DataStore that should be used to store the Kubernetes data for the given Tenant Control Plane.
// When Kamaji runs with the default DataStore flag, all empty values will inherit the default value.
// By leaving it empty and running Kamaji with no default DataStore flag, it is possible to achieve automatic assignment to a specific DataStore object.

View File

@@ -8,8 +8,9 @@
package v1alpha1
import (
"k8s.io/api/core/v1"
corev1 "k8s.io/api/core/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/gateway-api/apis/v1"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
@@ -57,26 +58,47 @@ func (in *AdditionalMetadata) DeepCopy() *AdditionalMetadata {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdditionalPort) DeepCopyInto(out *AdditionalPort) {
*out = *in
if in.AppProtocol != nil {
in, out := &in.AppProtocol, &out.AppProtocol
*out = new(string)
**out = **in
}
out.TargetPort = in.TargetPort
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalPort.
func (in *AdditionalPort) DeepCopy() *AdditionalPort {
if in == nil {
return nil
}
out := new(AdditionalPort)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdditionalVolumeMounts) DeepCopyInto(out *AdditionalVolumeMounts) {
*out = *in
if in.APIServer != nil {
in, out := &in.APIServer, &out.APIServer
*out = make([]v1.VolumeMount, len(*in))
*out = make([]corev1.VolumeMount, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.ControllerManager != nil {
in, out := &in.ControllerManager, &out.ControllerManager
*out = make([]v1.VolumeMount, len(*in))
*out = make([]corev1.VolumeMount, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Scheduler != nil {
in, out := &in.Scheduler, &out.Scheduler
*out = make([]v1.VolumeMount, len(*in))
*out = make([]corev1.VolumeMount, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
@@ -289,6 +311,21 @@ func (in *ClientCertificate) DeepCopy() *ClientCertificate {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CompoundValue) DeepCopyInto(out *CompoundValue) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompoundValue.
func (in *CompoundValue) DeepCopy() *CompoundValue {
if in == nil {
return nil
}
out := new(CompoundValue)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ContentRef) DeepCopyInto(out *ContentRef) {
*out = *in
@@ -324,6 +361,11 @@ func (in *ControlPlane) DeepCopyInto(out *ControlPlane) {
*out = new(IngressSpec)
(*in).DeepCopyInto(*out)
}
if in.Gateway != nil {
in, out := &in.Gateway, &out.Gateway
*out = new(GatewaySpec)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlane.
@@ -341,22 +383,22 @@ func (in *ControlPlaneComponentsResources) DeepCopyInto(out *ControlPlaneCompone
*out = *in
if in.APIServer != nil {
in, out := &in.APIServer, &out.APIServer
*out = new(v1.ResourceRequirements)
*out = new(corev1.ResourceRequirements)
(*in).DeepCopyInto(*out)
}
if in.ControllerManager != nil {
in, out := &in.ControllerManager, &out.ControllerManager
*out = new(v1.ResourceRequirements)
*out = new(corev1.ResourceRequirements)
(*in).DeepCopyInto(*out)
}
if in.Scheduler != nil {
in, out := &in.Scheduler, &out.Scheduler
*out = new(v1.ResourceRequirements)
*out = new(corev1.ResourceRequirements)
(*in).DeepCopyInto(*out)
}
if in.Kine != nil {
in, out := &in.Kine, &out.Kine
*out = new(v1.ResourceRequirements)
*out = new(corev1.ResourceRequirements)
(*in).DeepCopyInto(*out)
}
}
@@ -596,19 +638,19 @@ func (in *DeploymentSpec) DeepCopyInto(out *DeploymentSpec) {
in.Strategy.DeepCopyInto(&out.Strategy)
if in.Tolerations != nil {
in, out := &in.Tolerations, &out.Tolerations
*out = make([]v1.Toleration, len(*in))
*out = make([]corev1.Toleration, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Affinity != nil {
in, out := &in.Affinity, &out.Affinity
*out = new(v1.Affinity)
*out = new(corev1.Affinity)
(*in).DeepCopyInto(*out)
}
if in.TopologySpreadConstraints != nil {
in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints
*out = make([]v1.TopologySpreadConstraint, len(*in))
*out = make([]corev1.TopologySpreadConstraint, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
@@ -627,21 +669,21 @@ func (in *DeploymentSpec) DeepCopyInto(out *DeploymentSpec) {
in.PodAdditionalMetadata.DeepCopyInto(&out.PodAdditionalMetadata)
if in.AdditionalInitContainers != nil {
in, out := &in.AdditionalInitContainers, &out.AdditionalInitContainers
*out = make([]v1.Container, len(*in))
*out = make([]corev1.Container, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.AdditionalContainers != nil {
in, out := &in.AdditionalContainers, &out.AdditionalContainers
*out = make([]v1.Container, len(*in))
*out = make([]corev1.Container, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.AdditionalVolumes != nil {
in, out := &in.AdditionalVolumes, &out.AdditionalVolumes
*out = make([]v1.Volume, len(*in))
*out = make([]corev1.Volume, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
@@ -750,6 +792,69 @@ func (in ExtraArgs) DeepCopy() ExtraArgs {
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GatewayAccessPoint) DeepCopyInto(out *GatewayAccessPoint) {
*out = *in
if in.Type != nil {
in, out := &in.Type, &out.Type
*out = new(v1.AddressType)
**out = **in
}
if in.URLs != nil {
in, out := &in.URLs, &out.URLs
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayAccessPoint.
func (in *GatewayAccessPoint) DeepCopy() *GatewayAccessPoint {
if in == nil {
return nil
}
out := new(GatewayAccessPoint)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GatewayListener) DeepCopyInto(out *GatewayListener) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayListener.
func (in *GatewayListener) DeepCopy() *GatewayListener {
if in == nil {
return nil
}
out := new(GatewayListener)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GatewaySpec) DeepCopyInto(out *GatewaySpec) {
*out = *in
in.AdditionalMetadata.DeepCopyInto(&out.AdditionalMetadata)
if in.GatewayParentRefs != nil {
in, out := &in.GatewayParentRefs, &out.GatewayParentRefs
*out = make([]v1.ParentReference, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewaySpec.
func (in *GatewaySpec) DeepCopy() *GatewaySpec {
if in == nil {
return nil
}
out := new(GatewaySpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ImageOverrideTrait) DeepCopyInto(out *ImageOverrideTrait) {
*out = *in
@@ -786,7 +891,7 @@ func (in *KonnectivityAgentSpec) DeepCopyInto(out *KonnectivityAgentSpec) {
*out = *in
if in.Tolerations != nil {
in, out := &in.Tolerations, &out.Tolerations
*out = make([]v1.Toleration, len(*in))
*out = make([]corev1.Toleration, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
@@ -844,7 +949,7 @@ func (in *KonnectivityServerSpec) DeepCopyInto(out *KonnectivityServerSpec) {
*out = *in
if in.Resources != nil {
in, out := &in.Resources, &out.Resources
*out = new(v1.ResourceRequirements)
*out = new(corev1.ResourceRequirements)
(*in).DeepCopyInto(*out)
}
if in.ExtraArgs != nil {
@@ -951,6 +1056,123 @@ func (in *KubeadmPhasesStatus) DeepCopy() *KubeadmPhasesStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubeconfigGenerator) DeepCopyInto(out *KubeconfigGenerator) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeconfigGenerator.
func (in *KubeconfigGenerator) DeepCopy() *KubeconfigGenerator {
if in == nil {
return nil
}
out := new(KubeconfigGenerator)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *KubeconfigGenerator) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubeconfigGeneratorList) DeepCopyInto(out *KubeconfigGeneratorList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]KubeconfigGenerator, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeconfigGeneratorList.
func (in *KubeconfigGeneratorList) DeepCopy() *KubeconfigGeneratorList {
if in == nil {
return nil
}
out := new(KubeconfigGeneratorList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *KubeconfigGeneratorList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubeconfigGeneratorSpec) DeepCopyInto(out *KubeconfigGeneratorSpec) {
*out = *in
in.NamespaceSelector.DeepCopyInto(&out.NamespaceSelector)
in.TenantControlPlaneSelector.DeepCopyInto(&out.TenantControlPlaneSelector)
if in.Groups != nil {
in, out := &in.Groups, &out.Groups
*out = make([]CompoundValue, len(*in))
copy(*out, *in)
}
out.User = in.User
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeconfigGeneratorSpec.
func (in *KubeconfigGeneratorSpec) DeepCopy() *KubeconfigGeneratorSpec {
if in == nil {
return nil
}
out := new(KubeconfigGeneratorSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubeconfigGeneratorStatus) DeepCopyInto(out *KubeconfigGeneratorStatus) {
*out = *in
if in.Errors != nil {
in, out := &in.Errors, &out.Errors
*out = make([]KubeconfigGeneratorStatusError, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeconfigGeneratorStatus.
func (in *KubeconfigGeneratorStatus) DeepCopy() *KubeconfigGeneratorStatus {
if in == nil {
return nil
}
out := new(KubeconfigGeneratorStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubeconfigGeneratorStatusError) DeepCopyInto(out *KubeconfigGeneratorStatusError) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeconfigGeneratorStatusError.
func (in *KubeconfigGeneratorStatusError) DeepCopy() *KubeconfigGeneratorStatusError {
if in == nil {
return nil
}
out := new(KubeconfigGeneratorStatusError)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubeconfigStatus) DeepCopyInto(out *KubeconfigStatus) {
*out = *in
@@ -1022,6 +1244,30 @@ func (in *KubernetesDeploymentStatus) DeepCopy() *KubernetesDeploymentStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubernetesGatewayStatus) DeepCopyInto(out *KubernetesGatewayStatus) {
*out = *in
in.RouteStatus.DeepCopyInto(&out.RouteStatus)
out.RouteRef = in.RouteRef
if in.AccessPoints != nil {
in, out := &in.AccessPoints, &out.AccessPoints
*out = make([]GatewayAccessPoint, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesGatewayStatus.
func (in *KubernetesGatewayStatus) DeepCopy() *KubernetesGatewayStatus {
if in == nil {
return nil
}
out := new(KubernetesGatewayStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubernetesIngressStatus) DeepCopyInto(out *KubernetesIngressStatus) {
*out = *in
@@ -1086,6 +1332,11 @@ func (in *KubernetesStatus) DeepCopyInto(out *KubernetesStatus) {
*out = new(KubernetesIngressStatus)
(*in).DeepCopyInto(*out)
}
if in.Gateway != nil {
in, out := &in.Gateway, &out.Gateway
*out = new(KubernetesGatewayStatus)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesStatus.
@@ -1153,6 +1404,21 @@ func (in *NetworkProfileSpec) DeepCopy() *NetworkProfileSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Permissions) DeepCopyInto(out *Permissions) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Permissions.
func (in *Permissions) DeepCopy() *Permissions {
if in == nil {
return nil
}
out := new(Permissions)
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
@@ -1204,6 +1470,13 @@ func (in *SecretReference) DeepCopy() *SecretReference {
func (in *ServiceSpec) DeepCopyInto(out *ServiceSpec) {
*out = *in
in.AdditionalMetadata.DeepCopyInto(&out.AdditionalMetadata)
if in.AdditionalPorts != nil {
in, out := &in.AdditionalPorts, &out.AdditionalPorts
*out = make([]AdditionalPort, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceSpec.
@@ -1317,6 +1590,7 @@ func (in *TenantControlPlaneList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantControlPlaneSpec) DeepCopyInto(out *TenantControlPlaneSpec) {
*out = *in
out.WritePermissions = in.WritePermissions
in.ControlPlane.DeepCopyInto(&out.ControlPlane)
in.Kubernetes.DeepCopyInto(&out.Kubernetes)
in.NetworkProfile.DeepCopyInto(&out.NetworkProfile)

View File

@@ -14,7 +14,7 @@ name: kamaji-crds
sources:
- https://github.com/clastix/kamaji
type: application
version: 0.0.0+edge
version: 0.0.0+latest
annotations:
artifacthub.io/crds: |
- kind: TenantControlPlane

View File

@@ -1,6 +1,6 @@
# kamaji-crds
![Version: 0.0.0+edge](https://img.shields.io/badge/Version-0.0.0+edge-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: latest](https://img.shields.io/badge/AppVersion-latest-informational?style=flat-square)
![Version: 0.0.0+latest](https://img.shields.io/badge/Version-0.0.0+latest-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: latest](https://img.shields.io/badge/AppVersion-latest-informational?style=flat-square)
Kamaji is the Hosted Control Plane Manager for Kubernetes.

View File

@@ -0,0 +1,214 @@
group: kamaji.clastix.io
names:
categories:
- kamaji
kind: KubeconfigGenerator
listKind: KubeconfigGeneratorList
plural: kubeconfiggenerators
shortNames:
- kc
singular: kubeconfiggenerator
scope: Cluster
versions:
- additionalPrinterColumns:
- description: Age
jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: KubeconfigGenerator is the Schema for the kubeconfiggenerators API.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
properties:
controlPlaneEndpointFrom:
default: admin.svc
description: |-
ControlPlaneEndpointFrom is the key used to extract the Tenant Control Plane endpoint that must be used by the generator.
The targeted Secret is the `${TCP}-admin-kubeconfig` one, default to `admin.svc`.
type: string
groups:
description: |-
Groups is resolved a set of strings used to assign the x509 organisations field.
It will be recognised by Kubernetes as user groups.
items:
description: |-
CompoundValue allows defining a static, or a dynamic value.
Options are mutually exclusive, just one should be picked up.
properties:
fromDefinition:
description: |-
FromDefinition is used to generate a dynamic value,
it uses the dot notation to access fields from the referenced TenantControlPlane object:
e.g.: metadata.name
type: string
stringValue:
description: StringValue is a static string value.
type: string
type: object
x-kubernetes-validations:
- message: Either stringValue or fromDefinition must be set, but not both.
rule: (has(self.stringValue) || has(self.fromDefinition)) && !(has(self.stringValue) && has(self.fromDefinition))
type: array
namespaceSelector:
description: NamespaceSelector is used to filter Namespaces from which the generator should extract TenantControlPlane objects.
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
tenantControlPlaneSelector:
description: TenantControlPlaneSelector is used to filter the TenantControlPlane objects that should be address by the generator.
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
user:
description: User resolves to a string to identify the client, assigned to the x509 Common Name field.
properties:
fromDefinition:
description: |-
FromDefinition is used to generate a dynamic value,
it uses the dot notation to access fields from the referenced TenantControlPlane object:
e.g.: metadata.name
type: string
stringValue:
description: StringValue is a static string value.
type: string
type: object
x-kubernetes-validations:
- message: Either stringValue or fromDefinition must be set, but not both.
rule: (has(self.stringValue) || has(self.fromDefinition)) && !(has(self.stringValue) && has(self.fromDefinition))
required:
- user
type: object
status:
description: KubeconfigGeneratorStatus defines the observed state of KubeconfigGenerator.
properties:
availableResources:
default: 0
description: |-
AvailableResources is the sum of successfully generated resources.
In case of a different value compared to Resources, check the field errors.
type: integer
errors:
description: Errors is the list of failed kubeconfig generations.
items:
properties:
message:
description: Message is the error message recorded upon the last generator run.
type: string
resource:
description: Resource is the Namespaced name of the errored resource.
type: string
required:
- message
- resource
type: object
type: array
resources:
default: 0
description: Resources is the sum of targeted TenantControlPlane objects.
type: integer
required:
- availableResources
- resources
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -6700,6 +6700,178 @@ versions:
type: object
type: array
type: object
gateway:
description: Defining the options for an Optional Gateway which will expose API Server of the Tenant Control Plane
properties:
additionalMetadata:
description: AdditionalMetadata to add Labels and Annotations support.
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
hostname:
description: Hostname is an optional field which will be used as a route hostname.
maxLength: 253
minLength: 1
pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
type: string
parentRefs:
description: GatewayParentRefs is the class of the Gateway resource to use.
items:
description: |-
ParentReference identifies an API object (usually a Gateway) that can be considered
a parent of this resource (usually a route). There are two kinds of parent resources
with "Core" support:
* Gateway (Gateway conformance profile)
* Service (Mesh conformance profile, ClusterIP Services only)
This API may be extended in the future to support additional kinds of parent
resources.
The API object must be valid in the cluster; the Group and Kind must
be registered in the cluster for this reference to be valid.
properties:
group:
default: gateway.networking.k8s.io
description: |-
Group is the group of the referent.
When unspecified, "gateway.networking.k8s.io" is inferred.
To set the core API group (such as for a "Service" kind referent),
Group must be explicitly set to "" (empty string).
Support: Core
maxLength: 253
pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
type: string
kind:
default: Gateway
description: |-
Kind is kind of the referent.
There are two kinds of parent resources with "Core" support:
* Gateway (Gateway conformance profile)
* Service (Mesh conformance profile, ClusterIP Services only)
Support for other resources is Implementation-Specific.
maxLength: 63
minLength: 1
pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
type: string
name:
description: |-
Name is the name of the referent.
Support: Core
maxLength: 253
minLength: 1
type: string
namespace:
description: |-
Namespace is the namespace of the referent. When unspecified, this refers
to the local namespace of the Route.
Note that there are specific rules for ParentRefs which cross namespace
boundaries. Cross-namespace references are only valid if they are explicitly
allowed by something in the namespace they are referring to. For example:
Gateway has the AllowedRoutes field, and ReferenceGrant provides a
generic way to enable any other kind of cross-namespace reference.
<gateway:experimental:description>
ParentRefs from a Route to a Service in the same namespace are "producer"
routes, which apply default routing rules to inbound connections from
any namespace to the Service.
ParentRefs from a Route to a Service in a different namespace are
"consumer" routes, and these routing rules are only applied to outbound
connections originating from the same namespace as the Route, for which
the intended destination of the connections are a Service targeted as a
ParentRef of the Route.
</gateway:experimental:description>
Support: Core
maxLength: 63
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
port:
description: |-
Port is the network port this Route targets. It can be interpreted
differently based on the type of parent resource.
When the parent resource is a Gateway, this targets all listeners
listening on the specified port that also support this kind of Route(and
select this Route). It's not recommended to set `Port` unless the
networking behaviors specified in a Route must apply to a specific port
as opposed to a listener(s) whose port(s) may be changed. When both Port
and SectionName are specified, the name and port of the selected listener
must match both specified values.
<gateway:experimental:description>
When the parent resource is a Service, this targets a specific port in the
Service spec. When both Port (experimental) and SectionName are specified,
the name and port of the selected port must match both specified values.
</gateway:experimental:description>
Implementations MAY choose to support other parent resources.
Implementations supporting other types of parent resources MUST clearly
document how/if Port is interpreted.
For the purpose of status, an attachment is considered successful as
long as the parent resource accepts it partially. For example, Gateway
listeners can restrict which Routes can attach to them by Route kind,
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
from the referencing Route, the Route MUST be considered successfully
attached. If no Gateway listeners accept attachment from this Route,
the Route MUST be considered detached from the Gateway.
Support: Extended
format: int32
maximum: 65535
minimum: 1
type: integer
sectionName:
description: |-
SectionName is the name of a section within the target resource. In the
following resources, SectionName is interpreted as the following:
* Gateway: Listener name. When both Port (experimental) and SectionName
are specified, the name and port of the selected listener must match
both specified values.
* Service: Port name. When both Port (experimental) and SectionName
are specified, the name and port of the selected listener must match
both specified values.
Implementations MAY choose to support attaching Routes to other resources.
If that is the case, they MUST clearly document how SectionName is
interpreted.
When unspecified (empty string), this will reference the entire resource.
For the purpose of status, an attachment is considered successful if at
least one section in the parent resource accepts it. For example, Gateway
listeners can restrict which Routes can attach to them by Route kind,
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
the referencing Route, the Route MUST be considered successfully
attached. If no Gateway listeners accept attachment from this Route, the
Route MUST be considered detached from the Gateway.
Support: Core
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
type: string
required:
- name
type: object
type: array
type: object
ingress:
description: Defining the options for an Optional Ingress which will expose API Server of the Tenant Control Plane
properties:
@@ -6738,6 +6910,56 @@ versions:
type: string
type: object
type: object
additionalPorts:
description: |-
AdditionalPorts allows adding additional ports to the Service generated Kamaji
which targets the Tenant Control Plane pods.
items:
properties:
appProtocol:
description: |-
The application protocol for this port.
This is used as a hint for implementations to offer richer behavior for protocols that they understand.
This field follows standard Kubernetes label syntax.
Valid values are either:
* Un-prefixed protocol names - reserved for IANA standard service names (as per
RFC-6335 and https://www.iana.org/assignments/service-names).
type: string
name:
description: |-
The name of this port within the Service created by Kamaji.
This must be a DNS_LABEL, must have unique names, and cannot be `kube-apiserver`, or `konnectivity-server`.
type: string
port:
description: The port that will be exposed by this service.
format: int32
type: integer
protocol:
default: TCP
description: The IP protocol for this port. Supports "TCP", "UDP", and "SCTP".
enum:
- TCP
- UDP
- SCTP
type: string
targetPort:
anyOf:
- type: integer
- type: string
description: |-
Number or name of the port to access on the pods of the Tenant Control Plane.
Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME.
If this is a string, it will be looked up as a named port in the
target Pod's container ports. If this is not specified, the value
of the 'port' field is used (an identity map).
x-kubernetes-int-or-string: true
required:
- name
- port
- targetPort
type: object
type: array
serviceType:
description: ServiceType allows specifying how to expose the Tenant Control Plane.
enum:
@@ -6751,6 +6973,9 @@ versions:
required:
- service
type: object
x-kubernetes-validations:
- message: using both ingress and gateway is not supported
rule: '!(has(self.ingress) && has(self.gateway))'
dataStore:
description: |-
DataStore specifies the DataStore that should be used to store the Kubernetes data for the given Tenant Control Plane.
@@ -6955,6 +7180,22 @@ versions:
description: 'CIDR for Kubernetes Services: if empty, defaulted to 10.96.0.0/16.'
type: string
type: object
writePermissions:
description: |-
WritePermissions allows to select which operations (create, delete, update) must be blocked:
by default, all actions are allowed, and API Server can write to its Datastore.
By blocking all actions, the Tenant Control Plane can enter in a Read Only mode:
this phase can be used to prevent Datastore quota exhaustion or for your own business logic
(e.g.: blocking creation and update, but allowing deletion to "clean up" space).
properties:
blockCreation:
type: boolean
blockDeletion:
type: boolean
blockUpdate:
type: boolean
type: object
required:
- controlPlane
- kubernetes
@@ -7485,6 +7726,383 @@ versions:
- namespace
- selector
type: object
gateway:
description: KubernetesGatewayStatus defines the status for the Tenant Control Plane Gateway in the management cluster.
properties:
accessPoints:
description: A list of valid access points that the route exposes.
items:
properties:
port:
format: int32
type: integer
type:
description: |-
AddressType defines how a network address is represented as a text string.
This may take two possible forms:
* A predefined CamelCase string identifier (currently limited to `IPAddress` or `Hostname`)
* A domain-prefixed string identifier (like `acme.io/CustomAddressType`)
Values `IPAddress` and `Hostname` have Extended support.
The `NamedAddress` value has been deprecated in favor of implementation
specific domain-prefixed strings.
All other values, including domain-prefixed values have Implementation-specific support,
which are used in implementation-specific behaviors. Support for additional
predefined CamelCase identifiers may be added in future releases.
maxLength: 253
minLength: 1
pattern: ^Hostname|IPAddress|NamedAddress|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
type: string
urls:
items:
type: string
type: array
value:
type: string
required:
- port
- type
- value
type: object
type: array
parents:
description: |-
Parents is a list of parent resources (usually Gateways) that are
associated with the route, and the status of the route with respect to
each parent. When this route attaches to a parent, the controller that
manages the parent must add an entry to this list when the controller
first sees the route and should update the entry as appropriate when the
route or gateway is modified.
Note that parent references that cannot be resolved by an implementation
of this API will not be added to this list. Implementations of this API
can only populate Route status for the Gateways/parent resources they are
responsible for.
A maximum of 32 Gateways will be represented in this list. An empty list
means the route has not been attached to any Gateway.
<gateway:util:excludeFromCRD>
Notes for implementors:
While parents is not a listType `map`, this is due to the fact that the
list key is not scalar, and Kubernetes is unable to represent this.
Parent status MUST be considered to be namespaced by the combination of
the parentRef and controllerName fields, and implementations should keep
the following rules in mind when updating this status:
* Implementations MUST update only entries that have a matching value of
`controllerName` for that implementation.
* Implementations MUST NOT update entries with non-matching `controllerName`
fields.
* Implementations MUST treat each `parentRef`` in the Route separately and
update its status based on the relationship with that parent.
* Implementations MUST perform a read-modify-write cycle on this field
before modifying it. That is, when modifying this field, implementations
must be confident they have fetched the most recent version of this field,
and ensure that changes they make are on that recent version.
</gateway:util:excludeFromCRD>
items:
description: |-
RouteParentStatus describes the status of a route with respect to an
associated Parent.
properties:
conditions:
description: |-
Conditions describes the status of the route with respect to the Gateway.
Note that the route's availability is also subject to the Gateway's own
status conditions and listener status.
If the Route's ParentRef specifies an existing Gateway that supports
Routes of this kind AND that Gateway's controller has sufficient access,
then that Gateway's controller MUST set the "Accepted" condition on the
Route, to indicate whether the route has been accepted or rejected by the
Gateway, and why.
A Route MUST be considered "Accepted" if at least one of the Route's
rules is implemented by the Gateway.
There are a number of cases where the "Accepted" condition may not be set
due to lack of controller visibility, that includes when:
* The Route refers to a nonexistent parent.
* The Route is of a type that the controller does not support.
* The Route is in a namespace the controller does not have access to.
<gateway:util:excludeFromCRD>
Notes for implementors:
Conditions are a listType `map`, which means that they function like a
map with a key of the `type` field _in the k8s apiserver_.
This means that implementations must obey some rules when updating this
section.
* Implementations MUST perform a read-modify-write cycle on this field
before modifying it. That is, when modifying this field, implementations
must be confident they have fetched the most recent version of this field,
and ensure that changes they make are on that recent version.
* Implementations MUST NOT remove or reorder Conditions that they are not
directly responsible for. For example, if an implementation sees a Condition
with type `special.io/SomeField`, it MUST NOT remove, change or update that
Condition.
* Implementations MUST always _merge_ changes into Conditions of the same Type,
rather than creating more than one Condition of the same Type.
* Implementations MUST always update the `observedGeneration` field of the
Condition to the `metadata.generation` of the Gateway at the time of update creation.
* If the `observedGeneration` of a Condition is _greater than_ the value the
implementation knows about, then it MUST NOT perform the update on that Condition,
but must wait for a future reconciliation and status update. (The assumption is that
the implementation's copy of the object is stale and an update will be re-triggered
if relevant.)
</gateway:util:excludeFromCRD>
items:
description: Condition contains details for one aspect of the current state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
maxItems: 8
minItems: 1
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
controllerName:
description: |-
ControllerName is a domain/path string that indicates the name of the
controller that wrote this status. This corresponds with the
controllerName field on GatewayClass.
Example: "example.net/gateway-controller".
The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are
valid Kubernetes names
(https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).
Controllers MUST populate this field when writing status. Controllers should ensure that
entries to status populated with their ControllerName are cleaned up when they are no
longer necessary.
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
type: string
parentRef:
description: |-
ParentRef corresponds with a ParentRef in the spec that this
RouteParentStatus struct describes the status of.
properties:
group:
default: gateway.networking.k8s.io
description: |-
Group is the group of the referent.
When unspecified, "gateway.networking.k8s.io" is inferred.
To set the core API group (such as for a "Service" kind referent),
Group must be explicitly set to "" (empty string).
Support: Core
maxLength: 253
pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
type: string
kind:
default: Gateway
description: |-
Kind is kind of the referent.
There are two kinds of parent resources with "Core" support:
* Gateway (Gateway conformance profile)
* Service (Mesh conformance profile, ClusterIP Services only)
Support for other resources is Implementation-Specific.
maxLength: 63
minLength: 1
pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
type: string
name:
description: |-
Name is the name of the referent.
Support: Core
maxLength: 253
minLength: 1
type: string
namespace:
description: |-
Namespace is the namespace of the referent. When unspecified, this refers
to the local namespace of the Route.
Note that there are specific rules for ParentRefs which cross namespace
boundaries. Cross-namespace references are only valid if they are explicitly
allowed by something in the namespace they are referring to. For example:
Gateway has the AllowedRoutes field, and ReferenceGrant provides a
generic way to enable any other kind of cross-namespace reference.
<gateway:experimental:description>
ParentRefs from a Route to a Service in the same namespace are "producer"
routes, which apply default routing rules to inbound connections from
any namespace to the Service.
ParentRefs from a Route to a Service in a different namespace are
"consumer" routes, and these routing rules are only applied to outbound
connections originating from the same namespace as the Route, for which
the intended destination of the connections are a Service targeted as a
ParentRef of the Route.
</gateway:experimental:description>
Support: Core
maxLength: 63
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
port:
description: |-
Port is the network port this Route targets. It can be interpreted
differently based on the type of parent resource.
When the parent resource is a Gateway, this targets all listeners
listening on the specified port that also support this kind of Route(and
select this Route). It's not recommended to set `Port` unless the
networking behaviors specified in a Route must apply to a specific port
as opposed to a listener(s) whose port(s) may be changed. When both Port
and SectionName are specified, the name and port of the selected listener
must match both specified values.
<gateway:experimental:description>
When the parent resource is a Service, this targets a specific port in the
Service spec. When both Port (experimental) and SectionName are specified,
the name and port of the selected port must match both specified values.
</gateway:experimental:description>
Implementations MAY choose to support other parent resources.
Implementations supporting other types of parent resources MUST clearly
document how/if Port is interpreted.
For the purpose of status, an attachment is considered successful as
long as the parent resource accepts it partially. For example, Gateway
listeners can restrict which Routes can attach to them by Route kind,
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
from the referencing Route, the Route MUST be considered successfully
attached. If no Gateway listeners accept attachment from this Route,
the Route MUST be considered detached from the Gateway.
Support: Extended
format: int32
maximum: 65535
minimum: 1
type: integer
sectionName:
description: |-
SectionName is the name of a section within the target resource. In the
following resources, SectionName is interpreted as the following:
* Gateway: Listener name. When both Port (experimental) and SectionName
are specified, the name and port of the selected listener must match
both specified values.
* Service: Port name. When both Port (experimental) and SectionName
are specified, the name and port of the selected listener must match
both specified values.
Implementations MAY choose to support attaching Routes to other resources.
If that is the case, they MUST clearly document how SectionName is
interpreted.
When unspecified (empty string), this will reference the entire resource.
For the purpose of status, an attachment is considered successful if at
least one section in the parent resource accepts it. For example, Gateway
listeners can restrict which Routes can attach to them by Route kind,
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
the referencing Route, the Route MUST be considered successfully
attached. If no Gateway listeners accept attachment from this Route, the
Route MUST be considered detached from the Gateway.
Support: Core
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
type: string
required:
- name
type: object
required:
- conditions
- controllerName
- parentRef
type: object
maxItems: 32
type: array
x-kubernetes-list-type: atomic
routeRef:
description: Reference to the route created for this tenant.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
required:
- parents
type: object
ingress:
description: KubernetesIngressStatus defines the status for the Tenant Control Plane Ingress in the management cluster.
properties:
@@ -7703,6 +8321,7 @@ versions:
default: Provisioning
description: Status returns the current status of the Kubernetes version, such as its provisioning state, or completed upgrade.
enum:
- Unknown
- Provisioning
- CertificateAuthorityRotating
- Upgrading
@@ -7710,6 +8329,7 @@ versions:
- Ready
- NotReady
- Sleeping
- WriteLimited
type: string
version:
description: Version is the running Kubernetes version of the Tenant Control Plane.

View File

@@ -0,0 +1,10 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
cert-manager.io/inject-ca-from: {{ include "kamaji-crds.certManagerAnnotation" . }}
labels:
{{- include "kamaji-crds.labels" . | nindent 4 }}
name: kubeconfiggenerators.kamaji.clastix.io
spec:
{{ tpl (.Files.Get "hack/kamaji.clastix.io_kubeconfiggenerators_spec.yaml") . | nindent 2 }}

View File

@@ -83,6 +83,24 @@ Here the values you can override:
| image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. |
| imagePullSecrets | list | `[]` | |
| kamaji-etcd | object | `{"clusterDomain":"cluster.local","datastore":{"enabled":true,"name":"default"},"deploy":true,"fullnameOverride":"kamaji-etcd"}` | Subchart: See https://github.com/clastix/kamaji-etcd/blob/master/charts/kamaji-etcd/values.yaml |
| kubeconfigGenerator.affinity | object | `{}` | Kubernetes affinity rules to apply to Kubeconfig Generator controller pods |
| kubeconfigGenerator.enableLeaderElect | bool | `true` | Enables the leader election. |
| kubeconfigGenerator.enabled | bool | `false` | Toggle to deploy the Kubeconfig Generator Deployment. |
| kubeconfigGenerator.extraArgs | list | `[]` | A list of extra arguments to add to the Kubeconfig Generator controller default ones. |
| kubeconfigGenerator.fullnameOverride | string | `""` | |
| kubeconfigGenerator.healthProbeBindAddress | string | `":8081"` | The address the probe endpoint binds to. |
| kubeconfigGenerator.loggingDevel.enable | bool | `false` | Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error) |
| kubeconfigGenerator.nodeSelector | object | `{}` | Kubernetes node selector rules to schedule Kubeconfig Generator controller |
| kubeconfigGenerator.podAnnotations | object | `{}` | The annotations to apply to the Kubeconfig Generator controller pods. |
| kubeconfigGenerator.podSecurityContext | object | `{"runAsNonRoot":true}` | The securityContext to apply to the Kubeconfig Generator controller pods. |
| kubeconfigGenerator.replicaCount | int | `2` | The number of the pod replicas for the Kubeconfig Generator controller. |
| kubeconfigGenerator.resources.limits.cpu | string | `"200m"` | |
| kubeconfigGenerator.resources.limits.memory | string | `"512Mi"` | |
| kubeconfigGenerator.resources.requests.cpu | string | `"200m"` | |
| kubeconfigGenerator.resources.requests.memory | string | `"512Mi"` | |
| kubeconfigGenerator.securityContext | object | `{"allowPrivilegeEscalation":false}` | The securityContext to apply to the Kubeconfig Generator controller container only. |
| kubeconfigGenerator.serviceAccountOverride | string | `""` | The name of the service account to use. If not set, the root Kamaji one will be used. |
| kubeconfigGenerator.tolerations | list | `[]` | Kubernetes node taints that the Kubeconfig Generator controller pods would tolerate |
| livenessProbe | object | `{"httpGet":{"path":"/healthz","port":"healthcheck"},"initialDelaySeconds":15,"periodSeconds":20}` | The livenessProbe for the controller container |
| loggingDevel.enable | bool | `false` | Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error) (default false) |
| metricsBindAddress | string | `":8080"` | The address the metric endpoint binds to. (default ":8080") |

View File

@@ -1,3 +1,11 @@
- apiGroups:
- ""
resources:
- namespaces
verbs:
- get
- list
- watch
- apiGroups:
- apps
resources:
@@ -34,6 +42,28 @@
- patch
- update
- watch
- apiGroups:
- gateway.networking.k8s.io
resources:
- gateways
verbs:
- get
- list
- watch
- apiGroups:
- gateway.networking.k8s.io
resources:
- grpcroutes
- httproutes
- tlsroutes
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- kamaji.clastix.io
resources:
@@ -51,6 +81,7 @@
- kamaji.clastix.io
resources:
- datastores/status
- kubeconfiggenerators/status
- tenantcontrolplanes/status
verbs:
- get
@@ -59,6 +90,18 @@
- apiGroups:
- kamaji.clastix.io
resources:
- kubeconfiggenerators
verbs:
- create
- get
- list
- patch
- update
- watch
- apiGroups:
- kamaji.clastix.io
resources:
- kubeconfiggenerators/finalizers
- tenantcontrolplanes/finalizers
verbs:
- update

View File

@@ -0,0 +1,222 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
cert-manager.io/inject-ca-from: kamaji-system/kamaji-serving-cert
controller-gen.kubebuilder.io/version: v0.16.1
name: kubeconfiggenerators.kamaji.clastix.io
spec:
group: kamaji.clastix.io
names:
categories:
- kamaji
kind: KubeconfigGenerator
listKind: KubeconfigGeneratorList
plural: kubeconfiggenerators
shortNames:
- kc
singular: kubeconfiggenerator
scope: Cluster
versions:
- additionalPrinterColumns:
- description: Age
jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: KubeconfigGenerator is the Schema for the kubeconfiggenerators API.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
properties:
controlPlaneEndpointFrom:
default: admin.svc
description: |-
ControlPlaneEndpointFrom is the key used to extract the Tenant Control Plane endpoint that must be used by the generator.
The targeted Secret is the `${TCP}-admin-kubeconfig` one, default to `admin.svc`.
type: string
groups:
description: |-
Groups is resolved a set of strings used to assign the x509 organisations field.
It will be recognised by Kubernetes as user groups.
items:
description: |-
CompoundValue allows defining a static, or a dynamic value.
Options are mutually exclusive, just one should be picked up.
properties:
fromDefinition:
description: |-
FromDefinition is used to generate a dynamic value,
it uses the dot notation to access fields from the referenced TenantControlPlane object:
e.g.: metadata.name
type: string
stringValue:
description: StringValue is a static string value.
type: string
type: object
x-kubernetes-validations:
- message: Either stringValue or fromDefinition must be set, but not both.
rule: (has(self.stringValue) || has(self.fromDefinition)) && !(has(self.stringValue) && has(self.fromDefinition))
type: array
namespaceSelector:
description: NamespaceSelector is used to filter Namespaces from which the generator should extract TenantControlPlane objects.
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
tenantControlPlaneSelector:
description: TenantControlPlaneSelector is used to filter the TenantControlPlane objects that should be address by the generator.
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
user:
description: User resolves to a string to identify the client, assigned to the x509 Common Name field.
properties:
fromDefinition:
description: |-
FromDefinition is used to generate a dynamic value,
it uses the dot notation to access fields from the referenced TenantControlPlane object:
e.g.: metadata.name
type: string
stringValue:
description: StringValue is a static string value.
type: string
type: object
x-kubernetes-validations:
- message: Either stringValue or fromDefinition must be set, but not both.
rule: (has(self.stringValue) || has(self.fromDefinition)) && !(has(self.stringValue) && has(self.fromDefinition))
required:
- user
type: object
status:
description: KubeconfigGeneratorStatus defines the observed state of KubeconfigGenerator.
properties:
availableResources:
default: 0
description: |-
AvailableResources is the sum of successfully generated resources.
In case of a different value compared to Resources, check the field errors.
type: integer
errors:
description: Errors is the list of failed kubeconfig generations.
items:
properties:
message:
description: Message is the error message recorded upon the last generator run.
type: string
resource:
description: Resource is the Namespaced name of the errored resource.
type: string
required:
- message
- resource
type: object
type: array
resources:
default: 0
description: Resources is the sum of targeted TenantControlPlane objects.
type: integer
required:
- availableResources
- resources
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -6708,6 +6708,178 @@ spec:
type: object
type: array
type: object
gateway:
description: Defining the options for an Optional Gateway which will expose API Server of the Tenant Control Plane
properties:
additionalMetadata:
description: AdditionalMetadata to add Labels and Annotations support.
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
hostname:
description: Hostname is an optional field which will be used as a route hostname.
maxLength: 253
minLength: 1
pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
type: string
parentRefs:
description: GatewayParentRefs is the class of the Gateway resource to use.
items:
description: |-
ParentReference identifies an API object (usually a Gateway) that can be considered
a parent of this resource (usually a route). There are two kinds of parent resources
with "Core" support:
* Gateway (Gateway conformance profile)
* Service (Mesh conformance profile, ClusterIP Services only)
This API may be extended in the future to support additional kinds of parent
resources.
The API object must be valid in the cluster; the Group and Kind must
be registered in the cluster for this reference to be valid.
properties:
group:
default: gateway.networking.k8s.io
description: |-
Group is the group of the referent.
When unspecified, "gateway.networking.k8s.io" is inferred.
To set the core API group (such as for a "Service" kind referent),
Group must be explicitly set to "" (empty string).
Support: Core
maxLength: 253
pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
type: string
kind:
default: Gateway
description: |-
Kind is kind of the referent.
There are two kinds of parent resources with "Core" support:
* Gateway (Gateway conformance profile)
* Service (Mesh conformance profile, ClusterIP Services only)
Support for other resources is Implementation-Specific.
maxLength: 63
minLength: 1
pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
type: string
name:
description: |-
Name is the name of the referent.
Support: Core
maxLength: 253
minLength: 1
type: string
namespace:
description: |-
Namespace is the namespace of the referent. When unspecified, this refers
to the local namespace of the Route.
Note that there are specific rules for ParentRefs which cross namespace
boundaries. Cross-namespace references are only valid if they are explicitly
allowed by something in the namespace they are referring to. For example:
Gateway has the AllowedRoutes field, and ReferenceGrant provides a
generic way to enable any other kind of cross-namespace reference.
<gateway:experimental:description>
ParentRefs from a Route to a Service in the same namespace are "producer"
routes, which apply default routing rules to inbound connections from
any namespace to the Service.
ParentRefs from a Route to a Service in a different namespace are
"consumer" routes, and these routing rules are only applied to outbound
connections originating from the same namespace as the Route, for which
the intended destination of the connections are a Service targeted as a
ParentRef of the Route.
</gateway:experimental:description>
Support: Core
maxLength: 63
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
port:
description: |-
Port is the network port this Route targets. It can be interpreted
differently based on the type of parent resource.
When the parent resource is a Gateway, this targets all listeners
listening on the specified port that also support this kind of Route(and
select this Route). It's not recommended to set `Port` unless the
networking behaviors specified in a Route must apply to a specific port
as opposed to a listener(s) whose port(s) may be changed. When both Port
and SectionName are specified, the name and port of the selected listener
must match both specified values.
<gateway:experimental:description>
When the parent resource is a Service, this targets a specific port in the
Service spec. When both Port (experimental) and SectionName are specified,
the name and port of the selected port must match both specified values.
</gateway:experimental:description>
Implementations MAY choose to support other parent resources.
Implementations supporting other types of parent resources MUST clearly
document how/if Port is interpreted.
For the purpose of status, an attachment is considered successful as
long as the parent resource accepts it partially. For example, Gateway
listeners can restrict which Routes can attach to them by Route kind,
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
from the referencing Route, the Route MUST be considered successfully
attached. If no Gateway listeners accept attachment from this Route,
the Route MUST be considered detached from the Gateway.
Support: Extended
format: int32
maximum: 65535
minimum: 1
type: integer
sectionName:
description: |-
SectionName is the name of a section within the target resource. In the
following resources, SectionName is interpreted as the following:
* Gateway: Listener name. When both Port (experimental) and SectionName
are specified, the name and port of the selected listener must match
both specified values.
* Service: Port name. When both Port (experimental) and SectionName
are specified, the name and port of the selected listener must match
both specified values.
Implementations MAY choose to support attaching Routes to other resources.
If that is the case, they MUST clearly document how SectionName is
interpreted.
When unspecified (empty string), this will reference the entire resource.
For the purpose of status, an attachment is considered successful if at
least one section in the parent resource accepts it. For example, Gateway
listeners can restrict which Routes can attach to them by Route kind,
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
the referencing Route, the Route MUST be considered successfully
attached. If no Gateway listeners accept attachment from this Route, the
Route MUST be considered detached from the Gateway.
Support: Core
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
type: string
required:
- name
type: object
type: array
type: object
ingress:
description: Defining the options for an Optional Ingress which will expose API Server of the Tenant Control Plane
properties:
@@ -6746,6 +6918,56 @@ spec:
type: string
type: object
type: object
additionalPorts:
description: |-
AdditionalPorts allows adding additional ports to the Service generated Kamaji
which targets the Tenant Control Plane pods.
items:
properties:
appProtocol:
description: |-
The application protocol for this port.
This is used as a hint for implementations to offer richer behavior for protocols that they understand.
This field follows standard Kubernetes label syntax.
Valid values are either:
* Un-prefixed protocol names - reserved for IANA standard service names (as per
RFC-6335 and https://www.iana.org/assignments/service-names).
type: string
name:
description: |-
The name of this port within the Service created by Kamaji.
This must be a DNS_LABEL, must have unique names, and cannot be `kube-apiserver`, or `konnectivity-server`.
type: string
port:
description: The port that will be exposed by this service.
format: int32
type: integer
protocol:
default: TCP
description: The IP protocol for this port. Supports "TCP", "UDP", and "SCTP".
enum:
- TCP
- UDP
- SCTP
type: string
targetPort:
anyOf:
- type: integer
- type: string
description: |-
Number or name of the port to access on the pods of the Tenant Control Plane.
Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME.
If this is a string, it will be looked up as a named port in the
target Pod's container ports. If this is not specified, the value
of the 'port' field is used (an identity map).
x-kubernetes-int-or-string: true
required:
- name
- port
- targetPort
type: object
type: array
serviceType:
description: ServiceType allows specifying how to expose the Tenant Control Plane.
enum:
@@ -6759,6 +6981,9 @@ spec:
required:
- service
type: object
x-kubernetes-validations:
- message: using both ingress and gateway is not supported
rule: '!(has(self.ingress) && has(self.gateway))'
dataStore:
description: |-
DataStore specifies the DataStore that should be used to store the Kubernetes data for the given Tenant Control Plane.
@@ -6963,6 +7188,22 @@ spec:
description: 'CIDR for Kubernetes Services: if empty, defaulted to 10.96.0.0/16.'
type: string
type: object
writePermissions:
description: |-
WritePermissions allows to select which operations (create, delete, update) must be blocked:
by default, all actions are allowed, and API Server can write to its Datastore.
By blocking all actions, the Tenant Control Plane can enter in a Read Only mode:
this phase can be used to prevent Datastore quota exhaustion or for your own business logic
(e.g.: blocking creation and update, but allowing deletion to "clean up" space).
properties:
blockCreation:
type: boolean
blockDeletion:
type: boolean
blockUpdate:
type: boolean
type: object
required:
- controlPlane
- kubernetes
@@ -7493,6 +7734,383 @@ spec:
- namespace
- selector
type: object
gateway:
description: KubernetesGatewayStatus defines the status for the Tenant Control Plane Gateway in the management cluster.
properties:
accessPoints:
description: A list of valid access points that the route exposes.
items:
properties:
port:
format: int32
type: integer
type:
description: |-
AddressType defines how a network address is represented as a text string.
This may take two possible forms:
* A predefined CamelCase string identifier (currently limited to `IPAddress` or `Hostname`)
* A domain-prefixed string identifier (like `acme.io/CustomAddressType`)
Values `IPAddress` and `Hostname` have Extended support.
The `NamedAddress` value has been deprecated in favor of implementation
specific domain-prefixed strings.
All other values, including domain-prefixed values have Implementation-specific support,
which are used in implementation-specific behaviors. Support for additional
predefined CamelCase identifiers may be added in future releases.
maxLength: 253
minLength: 1
pattern: ^Hostname|IPAddress|NamedAddress|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
type: string
urls:
items:
type: string
type: array
value:
type: string
required:
- port
- type
- value
type: object
type: array
parents:
description: |-
Parents is a list of parent resources (usually Gateways) that are
associated with the route, and the status of the route with respect to
each parent. When this route attaches to a parent, the controller that
manages the parent must add an entry to this list when the controller
first sees the route and should update the entry as appropriate when the
route or gateway is modified.
Note that parent references that cannot be resolved by an implementation
of this API will not be added to this list. Implementations of this API
can only populate Route status for the Gateways/parent resources they are
responsible for.
A maximum of 32 Gateways will be represented in this list. An empty list
means the route has not been attached to any Gateway.
<gateway:util:excludeFromCRD>
Notes for implementors:
While parents is not a listType `map`, this is due to the fact that the
list key is not scalar, and Kubernetes is unable to represent this.
Parent status MUST be considered to be namespaced by the combination of
the parentRef and controllerName fields, and implementations should keep
the following rules in mind when updating this status:
* Implementations MUST update only entries that have a matching value of
`controllerName` for that implementation.
* Implementations MUST NOT update entries with non-matching `controllerName`
fields.
* Implementations MUST treat each `parentRef`` in the Route separately and
update its status based on the relationship with that parent.
* Implementations MUST perform a read-modify-write cycle on this field
before modifying it. That is, when modifying this field, implementations
must be confident they have fetched the most recent version of this field,
and ensure that changes they make are on that recent version.
</gateway:util:excludeFromCRD>
items:
description: |-
RouteParentStatus describes the status of a route with respect to an
associated Parent.
properties:
conditions:
description: |-
Conditions describes the status of the route with respect to the Gateway.
Note that the route's availability is also subject to the Gateway's own
status conditions and listener status.
If the Route's ParentRef specifies an existing Gateway that supports
Routes of this kind AND that Gateway's controller has sufficient access,
then that Gateway's controller MUST set the "Accepted" condition on the
Route, to indicate whether the route has been accepted or rejected by the
Gateway, and why.
A Route MUST be considered "Accepted" if at least one of the Route's
rules is implemented by the Gateway.
There are a number of cases where the "Accepted" condition may not be set
due to lack of controller visibility, that includes when:
* The Route refers to a nonexistent parent.
* The Route is of a type that the controller does not support.
* The Route is in a namespace the controller does not have access to.
<gateway:util:excludeFromCRD>
Notes for implementors:
Conditions are a listType `map`, which means that they function like a
map with a key of the `type` field _in the k8s apiserver_.
This means that implementations must obey some rules when updating this
section.
* Implementations MUST perform a read-modify-write cycle on this field
before modifying it. That is, when modifying this field, implementations
must be confident they have fetched the most recent version of this field,
and ensure that changes they make are on that recent version.
* Implementations MUST NOT remove or reorder Conditions that they are not
directly responsible for. For example, if an implementation sees a Condition
with type `special.io/SomeField`, it MUST NOT remove, change or update that
Condition.
* Implementations MUST always _merge_ changes into Conditions of the same Type,
rather than creating more than one Condition of the same Type.
* Implementations MUST always update the `observedGeneration` field of the
Condition to the `metadata.generation` of the Gateway at the time of update creation.
* If the `observedGeneration` of a Condition is _greater than_ the value the
implementation knows about, then it MUST NOT perform the update on that Condition,
but must wait for a future reconciliation and status update. (The assumption is that
the implementation's copy of the object is stale and an update will be re-triggered
if relevant.)
</gateway:util:excludeFromCRD>
items:
description: Condition contains details for one aspect of the current state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
maxItems: 8
minItems: 1
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
controllerName:
description: |-
ControllerName is a domain/path string that indicates the name of the
controller that wrote this status. This corresponds with the
controllerName field on GatewayClass.
Example: "example.net/gateway-controller".
The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are
valid Kubernetes names
(https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).
Controllers MUST populate this field when writing status. Controllers should ensure that
entries to status populated with their ControllerName are cleaned up when they are no
longer necessary.
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
type: string
parentRef:
description: |-
ParentRef corresponds with a ParentRef in the spec that this
RouteParentStatus struct describes the status of.
properties:
group:
default: gateway.networking.k8s.io
description: |-
Group is the group of the referent.
When unspecified, "gateway.networking.k8s.io" is inferred.
To set the core API group (such as for a "Service" kind referent),
Group must be explicitly set to "" (empty string).
Support: Core
maxLength: 253
pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
type: string
kind:
default: Gateway
description: |-
Kind is kind of the referent.
There are two kinds of parent resources with "Core" support:
* Gateway (Gateway conformance profile)
* Service (Mesh conformance profile, ClusterIP Services only)
Support for other resources is Implementation-Specific.
maxLength: 63
minLength: 1
pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
type: string
name:
description: |-
Name is the name of the referent.
Support: Core
maxLength: 253
minLength: 1
type: string
namespace:
description: |-
Namespace is the namespace of the referent. When unspecified, this refers
to the local namespace of the Route.
Note that there are specific rules for ParentRefs which cross namespace
boundaries. Cross-namespace references are only valid if they are explicitly
allowed by something in the namespace they are referring to. For example:
Gateway has the AllowedRoutes field, and ReferenceGrant provides a
generic way to enable any other kind of cross-namespace reference.
<gateway:experimental:description>
ParentRefs from a Route to a Service in the same namespace are "producer"
routes, which apply default routing rules to inbound connections from
any namespace to the Service.
ParentRefs from a Route to a Service in a different namespace are
"consumer" routes, and these routing rules are only applied to outbound
connections originating from the same namespace as the Route, for which
the intended destination of the connections are a Service targeted as a
ParentRef of the Route.
</gateway:experimental:description>
Support: Core
maxLength: 63
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
port:
description: |-
Port is the network port this Route targets. It can be interpreted
differently based on the type of parent resource.
When the parent resource is a Gateway, this targets all listeners
listening on the specified port that also support this kind of Route(and
select this Route). It's not recommended to set `Port` unless the
networking behaviors specified in a Route must apply to a specific port
as opposed to a listener(s) whose port(s) may be changed. When both Port
and SectionName are specified, the name and port of the selected listener
must match both specified values.
<gateway:experimental:description>
When the parent resource is a Service, this targets a specific port in the
Service spec. When both Port (experimental) and SectionName are specified,
the name and port of the selected port must match both specified values.
</gateway:experimental:description>
Implementations MAY choose to support other parent resources.
Implementations supporting other types of parent resources MUST clearly
document how/if Port is interpreted.
For the purpose of status, an attachment is considered successful as
long as the parent resource accepts it partially. For example, Gateway
listeners can restrict which Routes can attach to them by Route kind,
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
from the referencing Route, the Route MUST be considered successfully
attached. If no Gateway listeners accept attachment from this Route,
the Route MUST be considered detached from the Gateway.
Support: Extended
format: int32
maximum: 65535
minimum: 1
type: integer
sectionName:
description: |-
SectionName is the name of a section within the target resource. In the
following resources, SectionName is interpreted as the following:
* Gateway: Listener name. When both Port (experimental) and SectionName
are specified, the name and port of the selected listener must match
both specified values.
* Service: Port name. When both Port (experimental) and SectionName
are specified, the name and port of the selected listener must match
both specified values.
Implementations MAY choose to support attaching Routes to other resources.
If that is the case, they MUST clearly document how SectionName is
interpreted.
When unspecified (empty string), this will reference the entire resource.
For the purpose of status, an attachment is considered successful if at
least one section in the parent resource accepts it. For example, Gateway
listeners can restrict which Routes can attach to them by Route kind,
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
the referencing Route, the Route MUST be considered successfully
attached. If no Gateway listeners accept attachment from this Route, the
Route MUST be considered detached from the Gateway.
Support: Core
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
type: string
required:
- name
type: object
required:
- conditions
- controllerName
- parentRef
type: object
maxItems: 32
type: array
x-kubernetes-list-type: atomic
routeRef:
description: Reference to the route created for this tenant.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
required:
- parents
type: object
ingress:
description: KubernetesIngressStatus defines the status for the Tenant Control Plane Ingress in the management cluster.
properties:
@@ -7711,6 +8329,7 @@ spec:
default: Provisioning
description: Status returns the current status of the Kubernetes version, such as its provisioning state, or completed upgrade.
enum:
- Unknown
- Provisioning
- CertificateAuthorityRotating
- Upgrading
@@ -7718,6 +8337,7 @@ spec:
- Ready
- NotReady
- Sleeping
- WriteLimited
type: string
version:
description: Version is the running Kubernetes version of the Tenant Control Plane.

View File

@@ -89,3 +89,15 @@ Create the name of the cert-manager Certificate
{{- define "kamaji.certificateName" -}}
{{- printf "%s-serving-cert" (include "kamaji.fullname" .) }}
{{- end }}
{{/*
Kubeconfig Generator Deployment name.
*/}}
{{- define "kamaji.kubeconfigGeneratorName" -}}
{{- if .Values.kubeconfigGenerator.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name "kubeconfig-generator" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,54 @@
{{- if .Values.kubeconfigGenerator.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
{{- include "kamaji.labels" . | nindent 4 }}
name: {{ include "kamaji.kubeconfigGeneratorName" . }}
namespace: {{ .Release.Namespace }}
spec:
replicas: {{ .Values.kubeconfigGenerator.replicaCount }}
selector:
matchLabels:
{{- include "kamaji.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.kubeconfigGenerator.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "kamaji.selectorLabels" . | nindent 8 }}
spec:
securityContext:
{{- toYaml .Values.kubeconfigGenerator.podSecurityContext | nindent 8 }}
serviceAccountName: {{ default .Values.kubeconfigGenerator.serviceAccountOverride (include "kamaji.serviceAccountName" .) }}
containers:
- args:
- kubeconfig-generator
- --health-probe-bind-address={{ .Values.kubeconfigGenerator.healthProbeBindAddress }}
- --leader-elect={{ .Values.kubeconfigGenerator.enableLeaderElect }}
{{- if .Values.kubeconfigGenerator.loggingDevel.enable }}- --zap-devel{{- end }}
{{- with .Values.kubeconfigGenerator.extraArgs }}
{{- toYaml . | nindent 10 }}
{{- end }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
name: controller
resources:
{{- toYaml .Values.kubeconfigGenerator.resources | nindent 12 }}
securityContext:
{{- toYaml .Values.kubeconfigGenerator.securityContext | nindent 12 }}
{{- with .Values.kubeconfigGenerator.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.kubeconfigGenerator.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.kubeconfigGenerator.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}

View File

@@ -111,4 +111,48 @@ kamaji-etcd:
# -- Disable the analytics traces collection
telemetry:
disabled: false
kubeconfigGenerator:
# -- Toggle to deploy the Kubeconfig Generator Deployment.
enabled: false
fullnameOverride: ""
# -- The number of the pod replicas for the Kubeconfig Generator controller.
replicaCount: 2
# -- The annotations to apply to the Kubeconfig Generator controller pods.
podAnnotations: {}
# -- The securityContext to apply to the Kubeconfig Generator controller pods.
podSecurityContext:
runAsNonRoot: true
# -- The name of the service account to use. If not set, the root Kamaji one will be used.
serviceAccountOverride: ""
# -- The address the probe endpoint binds to.
healthProbeBindAddress: ":8081"
# -- Enables the leader election.
enableLeaderElect: true
loggingDevel:
# -- Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error)
enable: false
# -- A list of extra arguments to add to the Kubeconfig Generator controller default ones.
extraArgs: []
resources:
limits:
cpu: 200m
memory: 512Mi
requests:
cpu: 200m
memory: 512Mi
# -- The securityContext to apply to the Kubeconfig Generator controller container only.
securityContext:
allowPrivilegeEscalation: false
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
# -- Kubernetes node selector rules to schedule Kubeconfig Generator controller
nodeSelector: {}
# -- Kubernetes node taints that the Kubeconfig Generator controller pods would tolerate
tolerations: []
# -- Kubernetes affinity rules to apply to Kubeconfig Generator controller pods
affinity: {}

View File

@@ -0,0 +1,167 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package kubeconfiggenerator
import (
"flag"
"fmt"
"io"
"os"
goRuntime "runtime"
"time"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"github.com/clastix/kamaji/controllers"
"github.com/clastix/kamaji/internal"
)
func NewCmd(scheme *runtime.Scheme) *cobra.Command {
// CLI flags
var (
metricsBindAddress string
healthProbeBindAddress string
leaderElect bool
controllerReconcileTimeout time.Duration
cacheResyncPeriod time.Duration
managerNamespace string
certificateExpirationDeadline time.Duration
)
cmd := &cobra.Command{
Use: "kubeconfig-generator",
Short: "Start the Kubeconfig Generator manager",
SilenceErrors: false,
SilenceUsage: true,
PreRunE: func(*cobra.Command, []string) error {
// Avoid polluting stdout with useless details by the underlying klog implementations
klog.SetOutput(io.Discard)
klog.LogToStderr(false)
if certificateExpirationDeadline < 24*time.Hour {
return fmt.Errorf("certificate expiration deadline must be at least 24 hours")
}
return nil
},
RunE: func(*cobra.Command, []string) error {
ctx := ctrl.SetupSignalHandler()
setupLog := ctrl.Log.WithName("kubeconfig-generator")
setupLog.Info(fmt.Sprintf("Kamaji version %s %s%s", internal.GitTag, internal.GitCommit, internal.GitDirty))
setupLog.Info(fmt.Sprintf("Build from: %s", internal.GitRepo))
setupLog.Info(fmt.Sprintf("Build date: %s", internal.BuildTime))
setupLog.Info(fmt.Sprintf("Go Version: %s", goRuntime.Version()))
setupLog.Info(fmt.Sprintf("Go OS/Arch: %s/%s", goRuntime.GOOS, goRuntime.GOARCH))
ctrlOpts := ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{
BindAddress: metricsBindAddress,
},
HealthProbeBindAddress: healthProbeBindAddress,
LeaderElection: leaderElect,
LeaderElectionNamespace: managerNamespace,
LeaderElectionID: "kubeconfiggenerator.kamaji.clastix.io",
NewCache: func(config *rest.Config, opts cache.Options) (cache.Cache, error) {
opts.SyncPeriod = &cacheResyncPeriod
return cache.New(config, opts)
},
}
triggerChan := make(chan event.GenericEvent)
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrlOpts)
if err != nil {
setupLog.Error(err, "unable to start manager")
return err
}
setupLog.Info("setting probes")
{
if err = mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up health check")
return err
}
if err = mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up ready check")
return err
}
}
certController := &controllers.CertificateLifecycle{Channel: triggerChan, Deadline: certificateExpirationDeadline}
certController.EnqueueFn = certController.EnqueueForKubeconfigGenerator
if err = certController.SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "CertificateLifecycle")
return err
}
if err = (&controllers.KubeconfigGeneratorWatcher{
Client: mgr.GetClient(),
GeneratorChan: triggerChan,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "KubeconfigGeneratorWatcher")
return err
}
if err = (&controllers.KubeconfigGeneratorReconciler{
Client: mgr.GetClient(),
NotValidThreshold: certificateExpirationDeadline,
CertificateChan: triggerChan,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "KubeconfigGenerator")
return err
}
setupLog.Info("starting manager")
if err = mgr.Start(ctx); err != nil {
setupLog.Error(err, "problem running manager")
return err
}
return nil
},
}
// Setting zap logger
zapfs := flag.NewFlagSet("zap", flag.ExitOnError)
opts := zap.Options{
Development: true,
}
opts.BindFlags(zapfs)
cmd.Flags().AddGoFlagSet(zapfs)
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
// Setting CLI flags
cmd.Flags().StringVar(&metricsBindAddress, "metrics-bind-address", ":8090", "The address the metric endpoint binds to.")
cmd.Flags().StringVar(&healthProbeBindAddress, "health-probe-bind-address", ":8091", "The address the probe endpoint binds to.")
cmd.Flags().BoolVar(&leaderElect, "leader-elect", true, "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.")
cmd.Flags().DurationVar(&controllerReconcileTimeout, "controller-reconcile-timeout", 30*time.Second, "The reconciliation request timeout before the controller withdraw the external resource calls, such as dealing with the Datastore, or the Tenant Control Plane API endpoint.")
cmd.Flags().DurationVar(&cacheResyncPeriod, "cache-resync-period", 10*time.Hour, "The controller-runtime.Manager cache resync period.")
cmd.Flags().StringVar(&managerNamespace, "pod-namespace", os.Getenv("POD_NAMESPACE"), "The Kubernetes Namespace on which the Operator is running in, required for the TenantControlPlane migration jobs.")
cmd.Flags().DurationVar(&certificateExpirationDeadline, "certificate-expiration-deadline", 24*time.Hour, "Define the deadline upon certificate expiration to start the renewal process, cannot be less than a 24 hours.")
cobra.OnInitialize(func() {
viper.AutomaticEnv()
})
return cmd
}

View File

@@ -4,6 +4,7 @@
package manager
import (
"context"
"flag"
"fmt"
"io"
@@ -16,6 +17,7 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/discovery"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
ctrl "sigs.k8s.io/controller-runtime"
@@ -33,6 +35,7 @@ import (
"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"
"github.com/clastix/kamaji/internal/webhook/routes"
@@ -62,8 +65,6 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
webhookCAPath string
)
ctx := ctrl.SetupSignalHandler()
cmd := &cobra.Command{
Use: "manager",
Short: "Start the Kamaji Kubernetes Operator",
@@ -86,7 +87,7 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
return fmt.Errorf("unable to read webhook CA: %w", err)
}
if err = datastoreutils.CheckExists(ctx, scheme, datastore); err != nil {
if err = datastoreutils.CheckExists(context.Background(), scheme, datastore); err != nil {
return err
}
@@ -97,6 +98,8 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
return nil
},
RunE: func(*cobra.Command, []string) error {
ctx := ctrl.SetupSignalHandler()
setupLog := ctrl.Log.WithName("setup")
setupLog.Info(fmt.Sprintf("Kamaji version %s %s%s", internal.GitTag, internal.GitCommit, internal.GitDirty))
@@ -145,6 +148,13 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
return err
}
discoveryClient, err := discovery.NewDiscoveryClientForConfig(mgr.GetConfig())
if err != nil {
setupLog.Error(err, "unable to create discovery client")
return err
}
reconciler := &controllers.TenantControlPlaneReconciler{
Client: mgr.GetClient(),
APIReader: mgr.GetAPIReader(),
@@ -162,9 +172,10 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
KamajiService: managerServiceName,
KamajiMigrateImage: migrateJobImage,
MaxConcurrentReconciles: maxConcurrentReconciles,
DiscoveryClient: discoveryClient,
}
if err = reconciler.SetupWithManager(mgr); err != nil {
if err = reconciler.SetupWithManager(ctx, mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Namespace")
return err
@@ -193,7 +204,10 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
}
}
if err = (&controllers.CertificateLifecycle{Channel: certChannel, Deadline: certificateExpirationDeadline}).SetupWithManager(mgr); err != nil {
certController := &controllers.CertificateLifecycle{Channel: certChannel, Deadline: certificateExpirationDeadline}
certController.EnqueueFn = certController.EnqueueForTenantControlPlane
if err = certController.SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "CertificateLifecycle")
return err
@@ -211,10 +225,22 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
return err
}
// Only requires to look for the core api group.
if utilities.AreGatewayResourcesAvailable(ctx, mgr.GetClient(), discoveryClient) {
if err = (&kamajiv1alpha1.GatewayListener{}).SetupWithManager(ctx, mgr); err != nil {
setupLog.Error(err, "unable to create indexer", "indexer", "GatewayListener")
return err
}
}
err = webhook.Register(mgr, map[routes.Route][]handlers.Handler{
routes.TenantControlPlaneMigrate{}: {
handlers.Freeze{},
},
routes.TenantControlPlaneWritePermission{}: {
handlers.WritePermission{},
},
routes.TenantControlPlaneDefaults{}: {
handlers.TenantControlPlaneDefaults{
DefaultDatastore: datastore,
@@ -237,6 +263,10 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
},
handlers.TenantControlPlaneServiceCIDR{},
handlers.TenantControlPlaneLoadBalancerSourceRanges{},
handlers.TenantControlPlaneGatewayValidation{
Client: mgr.GetClient(),
DiscoveryClient: discoveryClient,
},
},
routes.TenantControlPlaneTelemetry{}: {
handlers.TenantControlPlaneTelemetry{

View File

@@ -10,6 +10,8 @@ import (
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
appsv1 "k8s.io/kubernetes/pkg/apis/apps/v1"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
)
@@ -22,6 +24,10 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(kamajiv1alpha1.AddToScheme(scheme))
utilruntime.Must(appsv1.RegisterDefaults(scheme))
// NOTE: This will succeed even if Gateway API is not installed in the cluster.
// Only registers the go types.
utilruntime.Must(gatewayv1.Install(scheme))
utilruntime.Must(gatewayv1alpha2.Install(scheme))
},
}
}

View File

@@ -0,0 +1,21 @@
apiVersion: kamaji.clastix.io/v1alpha1
kind: KubeconfigGenerator
metadata:
name: tenant
spec:
controlPlaneEndpointFrom: admin.conf
groups:
- stringValue: custom.group.tld
- fromDefinition: metadata.namespace
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: Exists
values: []
tenantControlPlaneSelector:
matchExpressions:
- key: tenant.clastix.io
operator: Exists
values: []
user:
fromDefinition: metadata.name

View File

@@ -31,8 +31,9 @@ import (
)
type CertificateLifecycle struct {
Channel chan event.GenericEvent
Deadline time.Duration
Channel chan event.GenericEvent
Deadline time.Duration
EnqueueFn func(secret *corev1.Secret)
client client.Client
}
@@ -91,12 +92,7 @@ func (s *CertificateLifecycle) Reconcile(ctx context.Context, request reconcile.
if deadline.After(crt.NotAfter) {
logger.Info("certificate near expiration, must be rotated")
s.Channel <- event.GenericEvent{Object: &kamajiv1alpha1.TenantControlPlane{
ObjectMeta: metav1.ObjectMeta{
Name: secret.GetOwnerReferences()[0].Name,
Namespace: secret.Namespace,
},
}}
s.EnqueueFn(&secret)
logger.Info("certificate rotation triggered")
@@ -110,6 +106,35 @@ func (s *CertificateLifecycle) Reconcile(ctx context.Context, request reconcile.
return reconcile.Result{RequeueAfter: after}, nil
}
func (s *CertificateLifecycle) EnqueueForTenantControlPlane(secret *corev1.Secret) {
for _, or := range secret.GetOwnerReferences() {
if or.Kind != "TenantControlPlane" {
continue
}
s.Channel <- event.GenericEvent{Object: &kamajiv1alpha1.TenantControlPlane{
ObjectMeta: metav1.ObjectMeta{
Name: or.Name,
Namespace: secret.Namespace,
},
}}
}
}
func (s *CertificateLifecycle) EnqueueForKubeconfigGenerator(secret *corev1.Secret) {
for _, or := range secret.GetOwnerReferences() {
if or.Kind != "KubeconfigGenerator" {
continue
}
s.Channel <- event.GenericEvent{Object: &kamajiv1alpha1.TenantControlPlane{
ObjectMeta: metav1.ObjectMeta{
Name: or.Name,
},
}}
}
}
func (s *CertificateLifecycle) extractCertificateFromBareSecret(secret corev1.Secret) (*x509.Certificate, error) {
var crt *x509.Certificate
var err error

View File

@@ -0,0 +1,444 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package controllers
import (
"bytes"
"context"
"crypto/x509"
"fmt"
"sort"
"strings"
"time"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
clientcmdapiv1 "k8s.io/client-go/tools/clientcmd/api/v1"
certutil "k8s.io/client-go/util/cert"
"k8s.io/client-go/util/keyutil"
"k8s.io/client-go/util/workqueue"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/cmd/kubeadm/app/util"
"k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil"
ctrl "sigs.k8s.io/controller-runtime"
"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/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/controllers/utils"
"github.com/clastix/kamaji/internal/constants"
"github.com/clastix/kamaji/internal/crypto"
"github.com/clastix/kamaji/internal/resources"
"github.com/clastix/kamaji/internal/utilities"
)
type KubeconfigGeneratorReconciler struct {
Client client.Client
NotValidThreshold time.Duration
CertificateChan chan event.GenericEvent
}
//+kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch
//+kubebuilder:rbac:groups=kamaji.clastix.io,resources=kubeconfiggenerators,verbs=get;list;watch;create;update;patch
//+kubebuilder:rbac:groups=kamaji.clastix.io,resources=kubeconfiggenerators/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=kamaji.clastix.io,resources=kubeconfiggenerators/finalizers,verbs=update
func (r *KubeconfigGeneratorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
logger.Info("reconciling resource")
var generator kamajiv1alpha1.KubeconfigGenerator
if err := r.Client.Get(ctx, req.NamespacedName, &generator); err != nil {
if apierrors.IsNotFound(err) {
logger.Info("resource may have been deleted, skipping")
return ctrl.Result{}, nil
}
logger.Error(err, "cannot retrieve the required resource")
return ctrl.Result{}, err
}
if utils.IsPaused(&generator) {
logger.Info("paused reconciliation, no further actions")
return ctrl.Result{}, nil
}
status, err := r.handle(ctx, &generator)
if err != nil {
logger.Error(err, "cannot handle the request")
return ctrl.Result{}, err
}
generator.Status = status
if statusErr := r.Client.Status().Update(ctx, &generator); statusErr != nil {
logger.Error(statusErr, "cannot update resource status")
return ctrl.Result{}, statusErr
}
logger.Info("reconciling completed")
return ctrl.Result{}, nil
}
func (r *KubeconfigGeneratorReconciler) handle(ctx context.Context, generator *kamajiv1alpha1.KubeconfigGenerator) (kamajiv1alpha1.KubeconfigGeneratorStatus, error) {
nsSelector, nsErr := metav1.LabelSelectorAsSelector(&generator.Spec.NamespaceSelector)
if nsErr != nil {
return kamajiv1alpha1.KubeconfigGeneratorStatus{}, errors.Wrap(nsErr, "NamespaceSelector contains an error")
}
var namespaceList corev1.NamespaceList
if err := r.Client.List(ctx, &namespaceList, &client.ListOptions{LabelSelector: nsSelector}); err != nil {
return kamajiv1alpha1.KubeconfigGeneratorStatus{}, errors.Wrap(err, "cannot filter Namespace objects using provided selector")
}
var targets []kamajiv1alpha1.TenantControlPlane
for _, ns := range namespaceList.Items {
tcpSelector, tcpErr := metav1.LabelSelectorAsSelector(&generator.Spec.TenantControlPlaneSelector)
if tcpErr != nil {
return kamajiv1alpha1.KubeconfigGeneratorStatus{}, errors.Wrap(tcpErr, "TenantControlPlaneSelector contains an error")
}
var tcpList kamajiv1alpha1.TenantControlPlaneList
if err := r.Client.List(ctx, &tcpList, &client.ListOptions{Namespace: ns.GetName(), LabelSelector: tcpSelector}); err != nil {
return kamajiv1alpha1.KubeconfigGeneratorStatus{}, errors.Wrap(err, "cannot filter TenantControlPlane objects using provided selector")
}
targets = append(targets, tcpList.Items...)
}
sort.Slice(targets, func(i, j int) bool {
return client.ObjectKeyFromObject(&targets[i]).String() < client.ObjectKeyFromObject(&targets[j]).String()
})
status := kamajiv1alpha1.KubeconfigGeneratorStatus{
Resources: len(targets),
AvailableResources: len(targets),
}
for _, tcp := range targets {
if err := r.process(ctx, generator, tcp); err != nil {
status.Errors = append(status.Errors, *err)
status.AvailableResources--
}
}
return status, nil
}
func (r *KubeconfigGeneratorReconciler) process(ctx context.Context, generator *kamajiv1alpha1.KubeconfigGenerator, tcp kamajiv1alpha1.TenantControlPlane) *kamajiv1alpha1.KubeconfigGeneratorStatusError {
statusErr := kamajiv1alpha1.KubeconfigGeneratorStatusError{
Resource: client.ObjectKeyFromObject(&tcp).String(),
}
var adminSecret corev1.Secret
if tcp.Status.KubeConfig.Admin.SecretName == "" {
statusErr.Message = "the admin kubeconfig is not yet generated"
return &statusErr
}
if err := r.Client.Get(ctx, types.NamespacedName{Name: tcp.Status.KubeConfig.Admin.SecretName, Namespace: tcp.GetNamespace()}, &adminSecret); err != nil {
statusErr.Message = fmt.Sprintf("an error occurred retrieving the admin Kubeconfig: %s", err.Error())
return &statusErr
}
kubeconfigTmpl, kcErr := utilities.DecodeKubeconfig(adminSecret, generator.Spec.ControlPlaneEndpointFrom)
if kcErr != nil {
statusErr.Message = fmt.Sprintf("unable to decode Kubeconfig template: %s", kcErr.Error())
return &statusErr
}
uMap, uErr := runtime.DefaultUnstructuredConverter.ToUnstructured(&tcp)
if uErr != nil {
statusErr.Message = fmt.Sprintf("cannot convert the resource to a map: %s", uErr)
return &statusErr
}
var user string
groups := sets.New[string]()
for _, group := range generator.Spec.Groups {
switch {
case group.StringValue != "":
groups.Insert(group.StringValue)
case group.FromDefinition != "":
v, ok, vErr := unstructured.NestedString(uMap, strings.Split(group.FromDefinition, ".")...)
switch {
case vErr != nil:
statusErr.Message = fmt.Sprintf("cannot run NestedString %q due to an error: %s", group.FromDefinition, vErr.Error())
return &statusErr
case !ok:
statusErr.Message = fmt.Sprintf("provided dot notation %q is not found", group.FromDefinition)
return &statusErr
default:
groups.Insert(v)
}
default:
statusErr.Message = "at least a StringValue or FromDefinition Group value must be provided"
return &statusErr
}
}
switch {
case generator.Spec.User.StringValue != "":
user = generator.Spec.User.StringValue
case generator.Spec.User.FromDefinition != "":
v, ok, vErr := unstructured.NestedString(uMap, strings.Split(generator.Spec.User.FromDefinition, ".")...)
switch {
case vErr != nil:
statusErr.Message = fmt.Sprintf("cannot run NestedString %q due to an error: %s", generator.Spec.User.FromDefinition, vErr.Error())
return &statusErr
case !ok:
statusErr.Message = fmt.Sprintf("provided dot notation %q is not found", generator.Spec.User.FromDefinition)
return &statusErr
default:
user = v
}
default:
statusErr.Message = "at least a StringValue or FromDefinition for the user field must be provided"
return &statusErr
}
var resultSecret corev1.Secret
resultSecret.SetName(tcp.Name + "-" + generator.Name)
resultSecret.SetNamespace(tcp.Namespace)
objectKey := client.ObjectKeyFromObject(&resultSecret)
if err := r.Client.Get(ctx, objectKey, &resultSecret); err != nil {
if !apierrors.IsNotFound(err) {
statusErr.Message = fmt.Sprintf("the secret %q cannot be generated", objectKey.String())
return &statusErr
}
if generateErr := r.generate(ctx, generator, &resultSecret, kubeconfigTmpl, &tcp, groups, user); generateErr != nil {
statusErr.Message = fmt.Sprintf("an error occurred generating the %q Secret: %s", objectKey.String(), generateErr.Error())
return &statusErr
}
return nil
}
isValid, validateErr := r.isValid(&resultSecret, kubeconfigTmpl, groups, user)
switch {
case !isValid:
if generateErr := r.generate(ctx, generator, &resultSecret, kubeconfigTmpl, &tcp, groups, user); generateErr != nil {
statusErr.Message = fmt.Sprintf("an error occurred regenerating the %q Secret: %s", objectKey.String(), generateErr.Error())
return &statusErr
}
return nil
case validateErr != nil:
statusErr.Message = fmt.Sprintf("an error occurred checking validation for %q Secret: %s", objectKey.String(), validateErr.Error())
return &statusErr
default:
return nil
}
}
func (r *KubeconfigGeneratorReconciler) generate(ctx context.Context, generator *kamajiv1alpha1.KubeconfigGenerator, secret *corev1.Secret, tmpl *clientcmdapiv1.Config, tcp *kamajiv1alpha1.TenantControlPlane, groups sets.Set[string], user string) error {
_, config, err := resources.GetKubeadmManifestDeps(ctx, r.Client, tcp)
if err != nil {
return err
}
clientCertConfig := pkiutil.CertConfig{
Config: certutil.Config{
CommonName: user,
Organization: groups.UnsortedList(),
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
},
NotAfter: util.StartTimeUTC().Add(kubeadmconstants.CertificateValidityPeriod),
EncryptionAlgorithm: config.InitConfiguration.ClusterConfiguration.EncryptionAlgorithmType(),
}
var caSecret corev1.Secret
if caErr := r.Client.Get(ctx, types.NamespacedName{Namespace: tcp.Namespace, Name: tcp.Status.Certificates.CA.SecretName}, &caSecret); caErr != nil {
return errors.Wrap(caErr, "cannot retrieve Certificate Authority")
}
caCert, crtErr := crypto.ParseCertificateBytes(caSecret.Data[kubeadmconstants.CACertName])
if crtErr != nil {
return errors.Wrap(crtErr, "cannot parse Certificate Authority certificate")
}
caKey, keyErr := crypto.ParsePrivateKeyBytes(caSecret.Data[kubeadmconstants.CAKeyName])
if keyErr != nil {
return errors.Wrap(keyErr, "cannot parse Certificate Authority key")
}
clientCert, clientKey, err := pkiutil.NewCertAndKey(caCert, caKey, &clientCertConfig)
contextUserName := generator.Name
for name := range tmpl.AuthInfos {
tmpl.AuthInfos[name].Name = contextUserName
tmpl.AuthInfos[name].AuthInfo.ClientCertificateData = pkiutil.EncodeCertPEM(clientCert)
tmpl.AuthInfos[name].AuthInfo.ClientKeyData, err = keyutil.MarshalPrivateKeyToPEM(clientKey)
if err != nil {
return errors.Wrap(err, "cannot marshal private key to PEM")
}
}
for name := range tmpl.Contexts {
tmpl.Contexts[name].Name = contextUserName
tmpl.Contexts[name].Context.AuthInfo = contextUserName
}
tmpl.CurrentContext = contextUserName
_, err = utilities.CreateOrUpdateWithConflict(ctx, r.Client, secret, func() error {
labels := secret.GetLabels()
if labels == nil {
labels = map[string]string{}
}
labels[kamajiv1alpha1.ManagedByLabel] = generator.Name
labels[kamajiv1alpha1.ManagedForLabel] = tcp.Name
labels[constants.ControllerLabelResource] = utilities.CertificateKubeconfigLabel
secret.SetLabels(labels)
if secret.Data == nil {
secret.Data = make(map[string][]byte)
}
secret.Data["value"], err = utilities.EncodeToYaml(tmpl)
if err != nil {
return errors.Wrap(err, "cannot encode generated Kubeconfig to YAML")
}
if utilities.IsRotationRequested(secret) {
utilities.SetLastRotationTimestamp(secret)
}
if orErr := controllerutil.SetOwnerReference(tcp, secret, r.Client.Scheme()); orErr != nil {
return orErr
}
return ctrl.SetControllerReference(tcp, secret, r.Client.Scheme())
})
if err != nil {
return errors.Wrap(err, "cannot create or update generated Kubeconfig")
}
return nil
}
func (r *KubeconfigGeneratorReconciler) isValid(secret *corev1.Secret, tmpl *clientcmdapiv1.Config, groups sets.Set[string], user string) (bool, error) {
if utilities.IsRotationRequested(secret) {
return false, nil
}
concrete, decodeErr := utilities.DecodeKubeconfig(*secret, "value")
if decodeErr != nil {
return false, decodeErr
}
// Checking Certificate Authority validity
switch {
case len(concrete.Clusters) != len(tmpl.Clusters):
return false, nil
default:
for i := range tmpl.Clusters {
if !bytes.Equal(tmpl.Clusters[i].Cluster.CertificateAuthorityData, concrete.Clusters[i].Cluster.CertificateAuthorityData) {
return false, nil
}
if tmpl.Clusters[i].Cluster.Server != concrete.Clusters[i].Cluster.Server {
return false, nil
}
}
}
for _, auth := range concrete.AuthInfos {
valid, vErr := crypto.IsValidCertificateKeyPairBytes(auth.AuthInfo.ClientCertificateData, auth.AuthInfo.ClientKeyData, r.NotValidThreshold)
if vErr != nil {
return false, vErr
}
if !valid {
return false, nil
}
crt, crtErr := crypto.ParseCertificateBytes(auth.AuthInfo.ClientCertificateData)
if crtErr != nil {
return false, crtErr
}
if crt.Subject.CommonName != user {
return false, nil
}
if !sets.New[string](crt.Subject.Organization...).Equal(groups) {
return false, nil
}
}
return true, nil
}
func (r *KubeconfigGeneratorReconciler) SetupWithManager(mgr manager.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&kamajiv1alpha1.KubeconfigGenerator{}).
WatchesRawSource(source.Channel(r.CertificateChan, handler.Funcs{GenericFunc: func(_ context.Context, genericEvent event.TypedGenericEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
w.AddRateLimited(ctrl.Request{
NamespacedName: types.NamespacedName{
Name: genericEvent.Object.GetName(),
},
})
}})).
Watches(&corev1.Secret{}, handler.TypedEnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []ctrl.Request {
if object.GetLabels() == nil {
return nil
}
v, found := object.GetLabels()[kamajiv1alpha1.ManagedByLabel]
if !found {
return nil
}
return []ctrl.Request{
{
NamespacedName: types.NamespacedName{
Name: v,
},
},
}
})).
Complete(r)
}

View File

@@ -0,0 +1,75 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package controllers
import (
"context"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/manager"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
)
type KubeconfigGeneratorWatcher struct {
Client client.Client
GeneratorChan chan event.GenericEvent
}
func (r *KubeconfigGeneratorWatcher) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
logger.Info("reconciling resource")
var tcp kamajiv1alpha1.TenantControlPlane
if err := r.Client.Get(ctx, req.NamespacedName, &tcp); err != nil {
if apierrors.IsNotFound(err) {
logger.Info("resource may have been deleted, skipping")
return ctrl.Result{}, nil
}
logger.Error(err, "cannot retrieve the required resource")
return ctrl.Result{}, err
}
var generators kamajiv1alpha1.KubeconfigGeneratorList
if err := r.Client.List(ctx, &generators); err != nil {
logger.Error(err, "cannot list generators")
return ctrl.Result{}, err
}
for _, generator := range generators.Items {
sel, err := metav1.LabelSelectorAsSelector(&generator.Spec.TenantControlPlaneSelector)
if err != nil {
logger.Error(err, "cannot validate Selector", "generator", generator.Name)
return ctrl.Result{}, err
}
if sel.Matches(labels.Set(tcp.Labels)) {
logger.Info("pushing Generator", "generator", generator.Name)
r.GeneratorChan <- event.GenericEvent{
Object: &generator,
}
}
}
return ctrl.Result{}, nil
}
func (r *KubeconfigGeneratorWatcher) SetupWithManager(mgr manager.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&kamajiv1alpha1.TenantControlPlane{}).
Complete(r)
}

View File

@@ -4,12 +4,14 @@
package controllers
import (
"context"
"fmt"
"time"
"github.com/go-logr/logr"
"github.com/google/uuid"
k8stypes "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/discovery"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -20,6 +22,7 @@ import (
"github.com/clastix/kamaji/internal/resources"
ds "github.com/clastix/kamaji/internal/resources/datastore"
"github.com/clastix/kamaji/internal/resources/konnectivity"
"github.com/clastix/kamaji/internal/utilities"
)
type GroupResourceBuilderConfiguration struct {
@@ -34,6 +37,7 @@ type GroupResourceBuilderConfiguration struct {
KamajiServiceAccount string
KamajiService string
KamajiMigrateImage string
DiscoveryClient discovery.DiscoveryInterface
}
type GroupDeletableResourceBuilderConfiguration struct {
@@ -48,8 +52,28 @@ type GroupDeletableResourceBuilderConfiguration struct {
// GetResources returns a list of resources that will be used to provide tenant control planes
// Currently there is only a default approach
// TODO: the idea of this function is to become a factory to return the group of resources according to the given configuration.
func GetResources(config GroupResourceBuilderConfiguration) []resources.Resource {
return getDefaultResources(config)
func GetResources(ctx context.Context, config GroupResourceBuilderConfiguration) []resources.Resource {
resources := []resources.Resource{}
resources = append(resources, getDataStoreMigratingResources(config.client, config.KamajiNamespace, config.KamajiMigrateImage, config.KamajiServiceAccount, config.KamajiService)...)
resources = append(resources, getUpgradeResources(config.client)...)
resources = append(resources, getKubernetesServiceResources(config.client)...)
resources = append(resources, getKubeadmConfigResources(config.client, getTmpDirectory(config.tcpReconcilerConfig.TmpBaseDirectory, config.tenantControlPlane), config.DataStore)...)
resources = append(resources, getKubernetesCertificatesResources(config.client, config.tcpReconcilerConfig, config.tenantControlPlane)...)
resources = append(resources, getKubeconfigResources(config.client, config.tcpReconcilerConfig, config.tenantControlPlane)...)
resources = append(resources, getKubernetesStorageResources(config.client, config.Connection, config.DataStore, config.ExpirationThreshold)...)
resources = append(resources, getKonnectivityServerRequirementsResources(config.client, config.ExpirationThreshold)...)
resources = append(resources, getKubernetesDeploymentResources(config.client, config.tcpReconcilerConfig, config.DataStore)...)
resources = append(resources, getKonnectivityServerPatchResources(config.client)...)
resources = append(resources, getDataStoreMigratingCleanup(config.client, config.KamajiNamespace)...)
resources = append(resources, getKubernetesIngressResources(config.client)...)
// Conditionally add Gateway resources
if utilities.AreGatewayResourcesAvailable(ctx, config.client, config.DiscoveryClient) {
resources = append(resources, getKubernetesGatewayResources(config.client)...)
}
return resources
}
// GetDeletableResources returns a list of resources that have to be deleted when tenant control planes are deleted
@@ -73,23 +97,6 @@ func GetDeletableResources(tcp *kamajiv1alpha1.TenantControlPlane, config GroupD
return res
}
func getDefaultResources(config GroupResourceBuilderConfiguration) []resources.Resource {
resources := getDataStoreMigratingResources(config.client, config.KamajiNamespace, config.KamajiMigrateImage, config.KamajiServiceAccount, config.KamajiService)
resources = append(resources, getUpgradeResources(config.client)...)
resources = append(resources, getKubernetesServiceResources(config.client)...)
resources = append(resources, getKubeadmConfigResources(config.client, getTmpDirectory(config.tcpReconcilerConfig.TmpBaseDirectory, config.tenantControlPlane), config.DataStore)...)
resources = append(resources, getKubernetesCertificatesResources(config.client, config.tcpReconcilerConfig, config.tenantControlPlane)...)
resources = append(resources, getKubeconfigResources(config.client, config.tcpReconcilerConfig, config.tenantControlPlane)...)
resources = append(resources, getKubernetesStorageResources(config.client, config.Connection, config.DataStore, config.ExpirationThreshold)...)
resources = append(resources, getKonnectivityServerRequirementsResources(config.client, config.ExpirationThreshold)...)
resources = append(resources, getKubernetesDeploymentResources(config.client, config.tcpReconcilerConfig, config.DataStore)...)
resources = append(resources, getKonnectivityServerPatchResources(config.client)...)
resources = append(resources, getDataStoreMigratingCleanup(config.client, config.KamajiNamespace)...)
resources = append(resources, getKubernetesIngressResources(config.client)...)
return resources
}
func getDataStoreMigratingCleanup(c client.Client, kamajiNamespace string) []resources.Resource {
return []resources.Resource{
&ds.Migrate{
@@ -128,6 +135,14 @@ func getKubernetesServiceResources(c client.Client) []resources.Resource {
}
}
func getKubernetesGatewayResources(c client.Client) []resources.Resource {
return []resources.Resource{
&resources.KubernetesGatewayResource{
Client: c,
},
}
}
func getKubeadmConfigResources(c client.Client, tmpDirectory string, dataStore kamajiv1alpha1.DataStore) []resources.Resource {
var endpoints []string

View File

@@ -0,0 +1,207 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package controllers
import (
"context"
"fmt"
"time"
"github.com/go-logr/logr"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kubernetes/cmd/kubeadm/app/util/errors"
"k8s.io/utils/ptr"
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"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
sooterrors "github.com/clastix/kamaji/controllers/soot/controllers/errors"
"github.com/clastix/kamaji/controllers/utils"
"github.com/clastix/kamaji/internal/utilities"
)
type WritePermissions struct {
Logger logr.Logger
Client client.Client
GetTenantControlPlaneFunc utils.TenantControlPlaneRetrievalFn
WebhookNamespace string
WebhookServiceName string
WebhookCABundle []byte
TriggerChannel chan event.GenericEvent
}
func (r *WritePermissions) Reconcile(ctx context.Context, _ reconcile.Request) (reconcile.Result, error) {
tcp, err := r.GetTenantControlPlaneFunc()
if err != nil {
if errors.Is(err, sooterrors.ErrPausedReconciliation) {
r.Logger.Info(err.Error())
return reconcile.Result{}, nil
}
return reconcile.Result{}, err
}
// Cannot detect the status of the TenantControlPlane, enqueuing back
if tcp.Status.Kubernetes.Version.Status == nil {
return reconcile.Result{RequeueAfter: time.Second}, nil
}
switch {
case ptr.Deref(tcp.Status.Kubernetes.Version.Status, kamajiv1alpha1.VersionUnknown) == kamajiv1alpha1.VersionWriteLimited &&
tcp.Spec.WritePermissions.HasAnyLimitation():
err = r.createOrUpdate(ctx, tcp.Spec.WritePermissions)
default:
err = r.cleanup(ctx)
}
if err != nil {
r.Logger.Error(err, "reconciliation failed")
return reconcile.Result{}, err
}
return reconcile.Result{}, nil
}
func (r *WritePermissions) createOrUpdate(ctx context.Context, writePermissions kamajiv1alpha1.Permissions) error {
obj := r.object().DeepCopy()
_, err := utilities.CreateOrUpdateWithConflict(ctx, r.Client, obj, func() error {
obj.Webhooks = []admissionregistrationv1.ValidatingWebhook{
{
Name: "leases.write-permissions.kamaji.clastix.io",
ClientConfig: admissionregistrationv1.WebhookClientConfig{
URL: ptr.To(fmt.Sprintf("https://%s.%s.svc:443/write-permission", r.WebhookServiceName, r.WebhookNamespace)),
CABundle: r.WebhookCABundle,
},
Rules: []admissionregistrationv1.RuleWithOperations{
{
Operations: []admissionregistrationv1.OperationType{
admissionregistrationv1.Create,
admissionregistrationv1.Delete,
},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"*"},
APIVersions: []string{"*"},
Resources: []string{"*"},
Scope: ptr.To(admissionregistrationv1.NamespacedScope),
},
},
},
FailurePolicy: ptr.To(admissionregistrationv1.Fail),
MatchPolicy: ptr.To(admissionregistrationv1.Equivalent),
NamespaceSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "kubernetes.io/metadata.name",
Operator: metav1.LabelSelectorOpIn,
Values: []string{
"kube-node-lease",
},
},
},
},
SideEffects: ptr.To(admissionregistrationv1.SideEffectClassNoneOnDryRun),
AdmissionReviewVersions: []string{"v1"},
},
{
Name: "catchall.write-permissions.kamaji.clastix.io",
ClientConfig: admissionregistrationv1.WebhookClientConfig{
URL: ptr.To(fmt.Sprintf("https://%s.%s.svc:443/write-permission", r.WebhookServiceName, r.WebhookNamespace)),
CABundle: r.WebhookCABundle,
},
Rules: []admissionregistrationv1.RuleWithOperations{
{
Operations: func() []admissionregistrationv1.OperationType {
var ops []admissionregistrationv1.OperationType
if writePermissions.BlockCreate {
ops = append(ops, admissionregistrationv1.Create)
}
if writePermissions.BlockUpdate {
ops = append(ops, admissionregistrationv1.Update)
}
if writePermissions.BlockDelete {
ops = append(ops, admissionregistrationv1.Delete)
}
return ops
}(),
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"*"},
APIVersions: []string{"*"},
Resources: []string{"*"},
Scope: ptr.To(admissionregistrationv1.AllScopes),
},
},
},
FailurePolicy: ptr.To(admissionregistrationv1.Fail),
MatchPolicy: ptr.To(admissionregistrationv1.Equivalent),
NamespaceSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "kubernetes.io/metadata.name",
Operator: metav1.LabelSelectorOpNotIn,
Values: []string{
"kube-system",
"kube-node-lease",
},
},
},
},
SideEffects: ptr.To(admissionregistrationv1.SideEffectClassNoneOnDryRun),
TimeoutSeconds: nil,
AdmissionReviewVersions: []string{"v1"},
},
}
return nil
})
return err
}
func (r *WritePermissions) cleanup(ctx context.Context) error {
if err := r.Client.Delete(ctx, r.object()); err != nil {
if apierrors.IsNotFound(err) {
return nil
}
return fmt.Errorf("unable to clean-up ValidationWebhook required for write permissions: %w", err)
}
return nil
}
func (r *WritePermissions) SetupWithManager(mgr manager.Manager) error {
r.TriggerChannel = make(chan event.GenericEvent)
return controllerruntime.NewControllerManagedBy(mgr).
WithOptions(controller.TypedOptions[reconcile.Request]{SkipNameValidation: ptr.To(true)}).
For(&admissionregistrationv1.ValidatingWebhookConfiguration{}, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
return object.GetName() == r.object().GetName()
}))).
WatchesRawSource(source.Channel(r.TriggerChannel, &handler.EnqueueRequestForObject{})).
Complete(r)
}
func (r *WritePermissions) object() *admissionregistrationv1.ValidatingWebhookConfiguration {
return &admissionregistrationv1.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: "kamaji-write-permissions",
},
}
}

View File

@@ -253,6 +253,19 @@ func (m *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
//
// Register all the controllers of the soot here:
//
writePermissions := &controllers.WritePermissions{
Logger: mgr.GetLogger().WithName("writePermissions"),
Client: mgr.GetClient(),
GetTenantControlPlaneFunc: m.retrieveTenantControlPlane(tcpCtx, request),
WebhookNamespace: m.MigrateServiceNamespace,
WebhookServiceName: m.MigrateServiceName,
WebhookCABundle: m.MigrateCABundle,
TriggerChannel: nil,
}
if err = writePermissions.SetupWithManager(mgr); err != nil {
return reconcile.Result{}, err
}
migrate := &controllers.Migrate{
WebhookNamespace: m.MigrateServiceNamespace,
WebhookServiceName: m.MigrateServiceName,
@@ -370,6 +383,7 @@ func (m *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
m.sootMap[request.NamespacedName.String()] = sootItem{
triggers: []chan event.GenericEvent{
writePermissions.TriggerChannel,
migrate.TriggerChannel,
konnectivityAgent.TriggerChannel,
kubeProxy.TriggerChannel,

View File

@@ -17,6 +17,7 @@ import (
networkingv1 "k8s.io/api/networking/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
k8stypes "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/discovery"
"k8s.io/client-go/util/workqueue"
"k8s.io/utils/clock"
ctrl "sigs.k8s.io/controller-runtime"
@@ -30,6 +31,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/controllers/finalizers"
@@ -37,6 +40,7 @@ import (
"github.com/clastix/kamaji/internal/datastore"
kamajierrors "github.com/clastix/kamaji/internal/errors"
"github.com/clastix/kamaji/internal/resources"
"github.com/clastix/kamaji/internal/utilities"
)
// TenantControlPlaneReconciler reconciles a TenantControlPlane object.
@@ -51,6 +55,7 @@ type TenantControlPlaneReconciler struct {
KamajiMigrateImage string
MaxConcurrentReconciles int
ReconcileTimeout time.Duration
DiscoveryClient discovery.DiscoveryInterface
// CertificateChan is the channel used by the CertificateLifecycleController that is checking for
// certificates and kubeconfig user certs validity: a generic event for the given TCP will be triggered
// once the validity threshold for the given certificate is reached.
@@ -76,6 +81,10 @@ type TenantControlPlaneReconcilerConfig struct {
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;delete
//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=httproutes,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=grpcroutes,verbs=get;list;watch;create;update;patch;delete
//+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
func (r *TenantControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
@@ -184,8 +193,9 @@ func (r *TenantControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R
KamajiServiceAccount: r.KamajiServiceAccount,
KamajiService: r.KamajiService,
KamajiMigrateImage: r.KamajiMigrateImage,
DiscoveryClient: r.DiscoveryClient,
}
registeredResources := GetResources(groupResourceBuilderConfiguration)
registeredResources := GetResources(ctx, groupResourceBuilderConfiguration)
for _, resource := range registeredResources {
result, err := resources.Handle(ctx, resource, tenantControlPlane)
@@ -242,10 +252,10 @@ func (r *TenantControlPlaneReconciler) mutexSpec(obj client.Object) mutex.Spec {
}
// SetupWithManager sets up the controller with the Manager.
func (r *TenantControlPlaneReconciler) SetupWithManager(mgr ctrl.Manager) error {
func (r *TenantControlPlaneReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
r.clock = clock.RealClock{}
return ctrl.NewControllerManagedBy(mgr).
controllerBuilder := ctrl.NewControllerManagedBy(mgr).
WatchesRawSource(source.Channel(r.CertificateChan, handler.Funcs{GenericFunc: func(_ context.Context, genericEvent event.TypedGenericEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
w.AddRateLimited(ctrl.Request{
NamespacedName: k8stypes.NamespacedName{
@@ -295,7 +305,20 @@ func (r *TenantControlPlaneReconciler) SetupWithManager(mgr ctrl.Manager) error
v, ok := labels["kamaji.clastix.io/component"]
return ok && v == "migrate"
}))).
})))
// Conditionally add Gateway API ownership if available
if utilities.AreGatewayResourcesAvailable(ctx, r.Client, r.DiscoveryClient) {
controllerBuilder = controllerBuilder.
Owns(&gatewayv1.HTTPRoute{}).
Owns(&gatewayv1.GRPCRoute{}).
Owns(&gatewayv1alpha2.TLSRoute{}).
Watches(&gatewayv1.Gateway{}, handler.EnqueueRequestsFromMapFunc(func(_ context.Context, object client.Object) []reconcile.Request {
return nil
}))
}
return controllerBuilder.
WithOptions(controller.Options{
MaxConcurrentReconciles: r.MaxConcurrentReconciles,
}).

View File

@@ -1,24 +1,24 @@
# Cluster Autoscaler
The [Cluster Autoscaler](https://github.com/kubernetes/autoscaler) is a tool that automatically adjusts the size of a Kubernetes cluster so that all pods have a place to run and there are no unneeded nodes.
The [Cluster Autoscaler](https://github.com/kubernetes/autoscaler) is a tool that automatically adjusts the size of a Kubernetes cluster so that all pods have a place to run and no unneeded nodes remain.
When pods are unschedulable because there are not enough resources, Cluster Autoscaler scales up the cluster. When nodes are underutilized, Cluster Autoscaler scales down the cluster.
When pods are unschedulable because there are not enough resources, the Cluster Autoscaler scales up the cluster. When nodes are underutilized, the Cluster Autoscaler scales the cluster down.
Cluster API supports the Cluster Autoscaler. See the [Cluster Autoscaler on Cluster API](https://cluster-api.sigs.k8s.io/tasks/automated-machine-management/autoscaling) for more information.
## Getting started with the Cluster Autoscaler on Kamaji
Kamaji supports the Cluster Autoscaler through Cluster API. There are several way to run the Cluster autoscaler with Cluster API. In this guide, we're leveraging the unique features of Kamaji to run the Cluster Autoscaler as part of Hosted Control Plane.
Kamaji supports the Cluster Autoscaler through Cluster API. There are several ways to run the Cluster Autoscaler with Cluster API. In this guide, we leverage the unique features of Kamaji to run the Cluster Autoscaler as part of the Hosted Control Plane.
In other words, the Cluster Autoscaler is running as a pod in the Kamaji Management Cluster, side by side with the Tenant Control Plane pods, and connecting directly to the apiserver of the workload cluster, hiding sensitive data and information from the tenant: this can be done by mounting the kubeconfig of the tenant cluster in the Cluster Autoscaler pod.
In other words, the Cluster Autoscaler runs as a pod in the Kamaji Management Cluster, alongside the Tenant Control Plane pods, and connects directly to the API server of the workload cluster. This approach hides sensitive data from the tenant. It works by mounting the kubeconfig of the tenant cluster into the Cluster Autoscaler pod.
### Create the workload cluster
Create a workload cluster using the Kamaji Control Plane Provider and the Infrastructure Provider of choice. The following example creates a workload cluster using the vSphere Infrastructure Provider:
Create a workload cluster using the Kamaji Control Plane Provider and the Infrastructure Provider of your choice. The following example creates a workload cluster using the vSphere Infrastructure Provider.
The template file [`capi-kamaji-vsphere-autoscaler-template.yaml`](https://raw.githubusercontent.com/clastix/cluster-api-control-plane-provider-kamaji/master/templates/vsphere/capi-kamaji-vsphere-autoscaler-template.yaml) provides a full example of a cluster with autoscaler enabled. You can generate the cluster manifest using `clusterctl`.
The template file [`capi-kamaji-vsphere-autoscaler-template.yaml`](https://raw.githubusercontent.com/clastix/cluster-api-control-plane-provider-kamaji/master/templates/vsphere/capi-kamaji-vsphere-autoscaler-template.yaml) provides a full example of a cluster with the autoscaler enabled. You can generate the cluster manifest using `clusterctl`.
Before you need to list all the variables in the template file:
Before doing so, list all the variables in the template file:
```bash
cat capi-kamaji-vsphere-autoscaler-template.yaml | clusterctl generate yaml --list-variables
@@ -40,10 +40,10 @@ kubectl apply -f capi-kamaji-vsphere-cluster.yaml
### Install the Cluster Autoscaler
Install the Cluster Autoscaler via Helm in the Management Cluster, in the same namespace where workload cluster is deployed.
Install the Cluster Autoscaler via Helm in the Management Cluster, in the same namespace where the workload cluster is deployed.
!!! info "Options for install Cluster Autoscaler"
Cluster Autoscaler works on a single cluster: it means every cluster must have its own Cluster Autoscaler instance. This could be solved by leveraging on Project Sveltos automations, by deploying a Cluster Autoscaler instance for each Kamaji Cluster API instance.
!!! info "Options for installing the Cluster Autoscaler"
The Cluster Autoscaler works on a single cluster, meaning every cluster must have its own Cluster Autoscaler instance. This can be addressed by leveraging Project Sveltos automations to deploy a Cluster Autoscaler instance for each Kamaji Cluster API instance.
```bash
helm repo add autoscaler https://kubernetes.github.io/autoscaler
@@ -56,9 +56,9 @@ helm upgrade --install ${CLUSTER_NAME}-autoscaler autoscaler/cluster-autoscaler
--set clusterAPIMode=kubeconfig-incluster
```
The `autoDiscovery.labels` values are used to pick dynamically clusters to autoscale.
The `autoDiscovery.labels` values are used to dynamically select clusters to autoscale.
Such labels must be set on the workload cluster, in the `Cluster` and `MachineDeployment` resources.
These labels must be set on the workload cluster, specifically in the `Cluster` and `MachineDeployment` resources.
```yaml
apiVersion: cluster.x-k8s.io/v1beta1
@@ -79,6 +79,9 @@ metadata:
# Cluster Autoscaler annotations
cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size: "0"
cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size: "6"
capacity.cluster-autoscaler.kubernetes.io/cpu: "2" # YMMV
capacity.cluster-autoscaler.kubernetes.io/memory: 4Gi # YMMV
capacity.cluster-autoscaler.kubernetes.io/maxPods: "110" # YMMV
labels:
cluster.x-k8s.io/cluster-name: sample
# Cluster Autoscaler labels
@@ -90,10 +93,9 @@ metadata:
# other Cluster API resources omitted for brevity
```
### Verify the Cluster Autoscaler
To verify the Cluster Autoscaler is working as expected, you can deploy a workload in the Tenant cluster with some CPU requirements in order to simulate workload requiring resources.
To verify that the Cluster Autoscaler is working as expected, deploy a workload in the Tenant cluster with specific CPU requirements to simulate resource demand.
```yaml
apiVersion: apps/v1
@@ -122,7 +124,51 @@ spec:
cpu: 500m
```
Apply the workload to the Tenant cluster and simulate the load spike by increasing the replicas. The Cluster Autoscaler should scale up the cluster to accommodate the workload. Cooldown time must be configured properly on a cluster basis.
Apply the workload to the Tenant cluster and simulate a load spike by increasing the number of replicas. The Cluster Autoscaler should scale up the cluster to accommodate the workload. Cooldown times must be configured correctly on a per-cluster basis.
!!! warning "Possible Resource Wasting"
With Cluster Autoscaler, new machines are automatically created in a very short time, ending up with some up-provisioning and potentially wasting resources. The official Cluster Autosclaler documentation must be understood to provide correct values according to the infrastructure and provisioning times.
!!! warning "Possible Resource Wastage"
With the Cluster Autoscaler, new machines may be created very quickly, which can lead to over-provisioning and potentially wasted resources. The official Cluster Autoscaler documentation should be consulted to configure appropriate values based on your infrastructure and provisioning times.
## `ProvisioningRequest` support
The [ProvisioningRequest](https://github.com/kubernetes/autoscaler/blob/cluster-autoscaler-1.34.1/cluster-autoscaler/proposals/provisioning-request.md) introduces a Kubernetes-native way for Cluster Autoscaler to request new capacity without talking directly to cloud provider APIs.
Instead of embedding provider-specific logic, the autoscaler simply describes the capacity it needs, and an external provisioner decides how to create the required nodes.
This makes scaling portable across clouds, on-prem platforms, and custom provisioning systems, while greatly reducing complexity inside the autoscaler.
Once the cluster has been provisioned, install the `ProvisioningRequest` definition.
```
kubectl kamaji kubeconfig get capi-quickstart-kubevirt > /tmp/capi-quickstart-kubevirt
KUBECONFIG=/tmp/capi-quickstart-kubevirt kubectl apply -f https://raw.githubusercontent.com/kubernetes/autoscaler/refs/tags/cluster-autoscaler-1.34.1/cluster-autoscaler/apis/config/crd/autoscaling.x-k8s.io_provisioningrequests.yaml
```
Proceed with the installation of Cluster Autoscaler by enabling some additional parameters: YMMV.
```yaml
cloudProvider: clusterapi
autoDiscovery:
namespace: default
labels:
- autoscaling.x-k8s.io: enabled
clusterAPIKubeconfigSecret: capi-quickstart-kubeconfig
clusterAPIMode: kubeconfig-incluster
extraArgs:
enable-provisioning-requests: true
kube-api-content-type: "application/json"
cloud-config: /etc/kubernetes/management/kubeconfig
extraVolumeSecrets:
# Mount the management kubeconfig to talk with the management cluster:
# the in-rest configuration doesn't work
management-kubeconfig:
name: management-kubeconfig
mountPath: /etc/kubernetes/management
items:
- key: kubeconfig
path: kubeconfig
```
The Cluster Autoscaler should be up and running, enabled to connect to the management and tenant cluster API Server:
follow the [official example](https://github.com/kubernetes/autoscaler/blob/cluster-autoscaler-1.34.1/cluster-autoscaler/FAQ.md#example-usage) from the repository to assess the `ProvisioningRequest` feature.

View File

@@ -14,15 +14,18 @@ To use the Proxmox Cluster API provider, you must connect and authenticate to a
```bash
# The Proxmox VE host
export PROXMOX_URL: "https://pve.example:8006"
export PROXMOX_URL="https://pve.example:8006"
# The Proxmox VE TokenID for authentication
export PROXMOX_TOKEN: "clastix@pam!capi"
export PROXMOX_TOKEN='clastix@pam!capi'
# The secret associated with the TokenID
export PROXMOX_SECRET: "REDACTED"
export PROXMOX_SECRET="REDACTED"
```
!!! warning "Env escaping "
Pay attention to escape special characters, such as `\` and `!`
Install the Infrastructure Provider:
```bash
@@ -72,6 +75,14 @@ Set the following environment variables to configure the workload machines:
# Node Configuration
export SSH_USER="clastix"
export SSH_AUTHORIZED_KEY="ssh-rsa AAAAB3Nz ..."
export NODE_LABELS="datacenter=us-west,instance-type=large"
export NODE_TAINTS="environment=production:PreferNoSchedule"
# You can add additional cloud-init configuration to further customize
# the worker nodes by setting the CLOUD_INIT_CONFIG environment variable:
export CLOUD_INIT_CONFIG="#cloud-config package_update: true packages: - net-tools"
# Number of worker nodes
export NODE_REPLICAS=2
# Resource Configuration
@@ -85,6 +96,7 @@ export BOOT_VOLUME_DEVICE="scsi0"
export BOOT_VOLUME_SIZE=20
export FILE_STORAGE_FORMAT="qcow2"
export STORAGE_NODE="local"
export POOL_NAME="sample-pool"
```
Use the following command to generate a cluster manifest based on the [`capi-kamaji-proxmox-template.yaml`](https://raw.githubusercontent.com/clastix/cluster-api-control-plane-provider-kamaji/master/templates/proxmox/capi-kamaji-proxmox-template.yaml) template file:
@@ -95,38 +107,8 @@ clusterctl generate cluster $CLUSTER_NAME \
> capi-kamaji-proxmox-cluster.yaml
```
### Additional cloud-init configuration
Cluster API requires machine templates based on `cloud-init`. You can add additional `cloud-init` configuration to further customize the worker nodes by including an additional `cloud-init` file in the `KubeadmConfigTemplate`:
```yaml
kind: KubeadmConfigTemplate
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
metadata:
name: ${CLUSTER_NAME}-md-0
spec:
template:
spec:
files:
- path: "/etc/cloud/cloud.cfg.d/99-custom.cfg"
content: "${CLOUD_INIT_CONFIG:-}"
owner: "root:root"
permissions: "0644"
```
You can then set the `CLOUD_INIT_CONFIG` environment variable to include the additional configuration:
```bash
export CLOUD_INIT_CONFIG="#cloud-config package_update: true packages: - net-tools"
```
and include it in the `clusterctl generate cluster` command:
```bash
clusterctl generate cluster $CLUSTER_NAME \
--from capi-kamaji-proxmox-template.yaml \
> capi-kamaji-proxmox-cluster.yaml
```
!!! warning "Customize the Template"
Before to generate cluster manifest, review and edit the template `capi-kamaji-proxmox-template.yaml` to customize.
### Apply the Cluster Manifest

View File

@@ -27,7 +27,7 @@ Kamaji is an open source Kubernetes Operator that transforms any Kubernetes clus
Kamaji leverages Kubernetes Custom Resource Definitions (CRDs) to provide a fully declarative approach to managing control planes, datastores, and other resources.
- **CNCF Compliance:**
Kamaji uses upstream, unmodified Kubernetes components and kubeadm for control plane setup, ensuring that all Tenant Clusters are [CNCF Certified Kubernetes](https://www.cncf.io/certification/software-conformance/) and compatible with standard Kubernetes tooling.
Kamaji uses upstream, unmodified Kubernetes components and kubeadm for control plane setup, ensuring that all Tenant Clusters follow [CNCF Certified Kubernetes Software Conformance](https://www.cncf.io/certification/software-conformance/) and are compatible with standard Kubernetes tooling.
## Extensibility and Integrations
@@ -54,4 +54,4 @@ Explore the following concepts to understand how Kamaji works under the hood:
- [Tenant Worker Nodes](tenant-worker-nodes.md)
- [Konnectivity](konnectivity.md)
Kamajis architecture is designed for flexibility, scalability, and operational simplicity, making it an ideal solution for organizations managing multiple Kubernetes clusters at scale.
Kamajis architecture is designed for flexibility, scalability, and operational simplicity, making it an ideal solution for organizations managing multiple Kubernetes clusters at scale.

View File

@@ -166,6 +166,17 @@ helm repo update
helm install kamaji clastix/kamaji -n kamaji-system --create-namespace --version 0.0.0+latest
```
!!! note "Alternative: Two-Step Installation"
The kamaji chart includes CRDs. For separate CRD management (GitOps, policy requirements), install CRDs first:
```bash
# Step 1: Install CRDs
helm install kamaji-crds clastix/kamaji-crds -n kamaji-system --create-namespace --version 0.0.0+latest
# Step 2: Install Kamaji operator
helm install kamaji clastix/kamaji -n kamaji-system --version 0.0.0+latest
```
## Create Tenant Cluster
Now that our management cluster is up and running, we can create a Tenant Cluster. A Tenant Cluster is a Kubernetes cluster that is managed by Kamaji.

View File

@@ -86,6 +86,17 @@ helm install kamaji clastix/kamaji \
--set image.tag=latest
```
!!! note "Alternative: Two-Step Installation"
The kamaji chart includes CRDs. For separate CRD management (GitOps, policy requirements), install CRDs first
```bash
# Step 1: Install CRDs
helm install kamaji-crds clastix/kamaji-crds -n kamaji-system --create-namespace --version 0.0.0+latest
# Step 2: Install Kamaji operator
helm install kamaji clastix/kamaji -n kamaji-system --version 0.0.0+latest
```
After installation, verify that Kamaji and its components are running:
```bash

View File

@@ -4,6 +4,57 @@ This guide describe a declarative way to deploy Kubernetes add-ons across multip
This way the tenant resources can be ensured from a single pane of glass, from the *Management Cluster*.
## Installing Kamaji with GitOps
For GitOps workflows using tools like FluxCD or ArgoCD, the kamaji-crds chart enables separate CRD management:
```yaml
# Step 1: HelmRelease for CRDs
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
name: kamaji-crds
namespace: kamaji-system
spec:
interval: 10m
chart:
spec:
chart: kamaji-crds
version: 0.0.0+latest
sourceRef:
kind: HelmRepository
name: clastix
namespace: flux-system
---
# Step 2: HelmRelease for operator
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
name: kamaji
namespace: kamaji-system
spec:
interval: 10m
dependsOn:
- name: kamaji-crds
chart:
spec:
chart: kamaji
version: 0.0.0+latest
sourceRef:
kind: HelmRepository
name: clastix
namespace: flux-system
```
!!! note "CRD Management Options"
The kamaji chart includes CRDs in its `crds/` directory. Using the separate kamaji-crds chart is optional and beneficial for:
- GitOps workflows requiring separate CRD lifecycle management
- Organizations with policies requiring separate CRD approval
- Scenarios where CRD updates must precede operator updates
For standard installations, using the kamaji chart alone is sufficient.
## Flux as the GitOps operator
As GitOps ensures a constant reconciliation to a Git-versioned desired state, [Flux](https://fluxcd.io) can satisfy the requirement of those scenarios. In particular, the controllers that reconcile [resources](https://fluxcd.io/flux/concepts/#reconciliation) support communicating to external clusters.

View File

@@ -0,0 +1,114 @@
# Kubeconfig Generator
The **Kubeconfig Generator** is a Kamaji extension that simplifies the distribution of Kubeconfig files for tenant clusters managed through Kamaji.
Instead of manually exporting and editing credentials, the generator automates the creation of kubeconfigs aligned with your organizational policies.
## Motivation
When managing multiple Tenant Control Planes (TCPs), cluster administrators often face two challenges:
1. **Consistency**: ensuring kubeconfigs are generated with the correct user identity, groups, and endpoints
2. **Scalability**: distributing kubeconfigs to users across potentially dozens of tenant clusters without manual steps
The `KubeconfigGenerator` resource addresses these problems by:
- Selecting which TCPs to target via label selectors.
- Defining how to build user and group identities in kubeconfigs.
- Automatically maintaining kubeconfigs as new tenant clusters are created or updated.
This provides a single, declarative way to manage kubeconfig lifecycle across all your tenants,
especially convenient if those cases where an Identity Provider can't be used to delegate access to Tenant Control Plane clusters.
## How it Works
### Selection
- `namespaceSelector` filters the namespaces from which Tenant Control Planes are discovered.
- `tenantControlPlaneSelector` further refines which TCPs to include.
### Identity Definition
The `user` and `groups` fields use compound values, which can be either:
- A static string (e.g., `developer`)
- A dynamic reference resolved from the TCP object (e.g., `metadata.name`)
This allows kubeconfigs to be tailored to the clusters context or a fixed organizational pattern.
### Endpoint Resolution
The generator pulls the API server endpoint from the TCPs `admin` kubeconfig.
By default it uses the `admin.svc` template, but this can be overridden with the `controlPlaneEndpointFrom` field.
### Status and Errors
The resource keeps track of how many kubeconfigs were attempted, how many succeeded,
and provides detailed error reports for failed generations.
## Typical Use Cases
- **Platform Operators**: automatically distribute kubeconfigs to developers as new tenant clusters are provisioned.
- **Multi-team Environments**: ensure each team gets kubeconfigs with the correct groups for RBAC authorization.
- **Least Privilege Principle**: avoid distributing `cluster-admin` credentials with a fine-grained RBAC
- **Dynamic Access**: use `fromDefinition` references to bind kubeconfig identities directly to tenant metadata
(e.g., prefixing users with the TCP's name).
## Example Scenario
A SaaS provider runs multiple Tenant Control Planes, each corresponding to a different customer.
Instead of manually managing kubeconfigs for every customer environment, the operator defines a single `KubeconfigGenerator`:
```yaml
apiVersion: kamaji.clastix.io/v1alpha1
kind: KubeconfigGenerator
metadata:
name: tenant
spec:
# Select only Tenant Control Planes living in namespaces
# labeled as production environments
namespaceSelector:
matchLabels:
environment: production
# Match all Tenant Control Planes in those namespaces
tenantControlPlaneSelector: {}
# Assign a static group "customer-admins"
groups:
- stringValue: "customer-admins"
# Derive the user identity dynamically from the TenantControlPlane metadata
user:
fromDefinition: "metadata.name"
# Use the public admin endpoint from the TCPs kubeconfig
controlPlaneEndpointFrom: "admin.conf"
```
- Matches all TCPs in namespaces labeled `environment=production`.
- Generates kubeconfigs with group `customer-admins`.
- Derives the user identity from the TCPs `metadata.name`.
As new tenants are created, their kubeconfigs are generated automatically and kept up to date.
```
$: kubectl get secret --all-namespaces -l kamaji.clastix.io/managed-by=tenant
NAMESPACE NAME TYPE DATA AGE
alpha-tnt env-133-tenant Opaque 1 12h
alpha-tnt env-130-tenant Opaque 1 2d
bravo-tnt prod-tenant Opaque 1 2h
charlie-tnt stable-tenant Opaque 1 1d
```
## Observability
The generator exposes its status directly in the CRD:
- `resources`: total number of TCPs targeted.
- `availableResources`: successfully generated kubeconfigs.
- `errors`: list of failed kubeconfig generations, including the affected resource and error message.
This allows quick debugging and operational awareness.
## Deployment
The _Kubeconfig Generator_ is **not** enabled by default since it's still in experimental state.
It can be enabled using the Helm value `kubeconfigGenerator.enabled=true` which is defaulted to `false`.

View File

@@ -0,0 +1,115 @@
# Write Permissions
Using the _Write Permissions_ section, operators can limit write operations for a given Tenant Control Plane,
where no further write actions can be made by its tenants.
This feature ensures consistency during maintenance, migrations, incident recovery, quote enforcement,
or when freezing workloads for auditing and compliance purposes.
Write Operations can limit the following actions:
- Create
- Update
- Delete
By default, all write operations are allowed.
## Enabling a Read-Only mode
You can enable ReadOnly mode by setting all the boolean fields of `TenantControlPlane.spec.writePermissions` to `true`.
```yaml
apiVersion: kamaji.clastix.io/v1alpha1
kind: TenantControlPlane
metadata:
name: my-control-plane
spec:
writePermissions:
blockCreate: true
blockUpdate: true
blockDelete: true
```
Once applied, the Tenant Control Plane will switch into `WriteLimited` status.
## Enforcing a quota mode
If your Tenant Control Plane has a Datastore quota, this feature allows freezing write and update operations,
but still allowing its tenants to perform a clean-up by deleting exceeding resources.
```yaml
apiVersion: kamaji.clastix.io/v1alpha1
kind: TenantControlPlane
metadata:
name: my-control-plane
spec:
writePermissions:
blockCreate: true
blockUpdate: true
blockDelete: false
```
!!! note "Datastore quota"
Kamaji does **not** enforce storage quota for a given Tenant Control Plane:
you have to implement it according to your business logic.
## Monitoring the status
You can verify the status of your Tenant Control Plane with `kubectl get tcp`:
```json
$: kubectl get tcp k8s-133
NAME VERSION INSTALLED VERSION STATUS CONTROL-PLANE ENDPOINT KUBECONFIG DATASTORE AGE
k8s-133 v1.33.0 v1.33.0 WriteLimited 172.18.255.100:6443 k8s-133-admin-kubeconfig default 50d
```
The `STATUS` field will display `WriteLimited` when write permissions are limited.
## How it works
When a Tenant Control Plane write status is _limited_, Kamaji creates a `ValidatingWebhookConfiguration` in the Tenant Cluster:
```
$: kubectl get validatingwebhookconfigurations
NAME WEBHOOKS AGE
kamaji-write-permissions 2 59m
```
The webhook intercepts all API requests to the Tenant Control Plane and programmatically denies any attempts to modify resources.
As a result, all changes initiated by tenants (such as `kubectl apply`, `kubectl delete`, or CRD updates) could be blocked.
!!! warning "Operators and Controller"
When the write status is limited, all actions are intercepted by the webhook.
If a Pod must be rescheduled, the webhook will deny it.
## Behaviour with limited write operations
If a tenant user tries to perform non-allowed write operations, such as:
- creating resources when `TenantControlPlane.spec.writePermissions.blockCreate` is set to `true`
- updating resources when `TenantControlPlane.spec.writePermissions.blockUpdate` is set to `true`
- deleting resources when `TenantControlPlane.spec.writePermissions.blockDelete` is set to `true`
the following error is returned:
```
Error from server (Forbidden): admission webhook "catchall.write-permissions.kamaji.clastix.io" denied the request:
the current Control Plane has limited write permissions, current changes are blocked:
removing the webhook may lead to an inconsistent state upon its completion
```
This guarantees the cluster remains in a frozen, consistent state, preventing partial updates or drift.
## Use Cases
Typical scenarios where ReadOnly mode is useful:
- **Planned Maintenance**: freeze workloads before performing upgrades or infrastructure changes.
- **Disaster Recovery**: lock the Tenant Control Plane to prevent accidental modifications during incident handling.
- **Auditing & Compliance**: ensure workloads cannot be altered during a compliance check or certification process.
- **Quota Enforcement**: preventing Datastore quote over commit in terms of storage size.
!!! info "Migrating the DataStore"
In a similar manner, when migrating a Tenant Control Plane to a different store, similar enforcement is put in place.
This is managed automatically by Kamaji: there's no need to toggle on and off the ReadOnly mode.

File diff suppressed because it is too large Load Diff

View File

@@ -74,9 +74,11 @@ nav:
- guides/backup-and-restore.md
- guides/certs-lifecycle.md
- guides/pausing.md
- guides/write-permissions.md
- guides/datastore-migration.md
- guides/gitops.md
- guides/console.md
- guides/kubeconfig-generator.md
- guides/upgrade.md
- guides/monitoring.md
- guides/terraform.md

View File

@@ -15,6 +15,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
)
@@ -56,6 +58,12 @@ var _ = BeforeSuite(func() {
err = kamajiv1alpha1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
err = gatewayv1.Install(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
err = gatewayv1alpha2.Install(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
//+kubebuilder:scaffold:scheme
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).NotTo(HaveOccurred())

View File

@@ -0,0 +1,102 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package e2e
import (
"context"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
pointer "k8s.io/utils/ptr"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
)
var _ = Describe("Deploy a TenantControlPlane with Gateway API", func() {
var tcp *kamajiv1alpha1.TenantControlPlane
JustBeforeEach(func() {
tcp = &kamajiv1alpha1.TenantControlPlane{
ObjectMeta: metav1.ObjectMeta{
Name: "tcp-gateway",
Namespace: "default",
},
Spec: kamajiv1alpha1.TenantControlPlaneSpec{
ControlPlane: kamajiv1alpha1.ControlPlane{
Deployment: kamajiv1alpha1.DeploymentSpec{
Replicas: pointer.To(int32(1)),
},
Service: kamajiv1alpha1.ServiceSpec{
ServiceType: "ClusterIP",
},
Gateway: &kamajiv1alpha1.GatewaySpec{
Hostname: gatewayv1.Hostname("tcp-gateway.example.com"),
AdditionalMetadata: kamajiv1alpha1.AdditionalMetadata{
Labels: map[string]string{
"test.kamaji.io/gateway": "true",
},
Annotations: map[string]string{
"test.kamaji.io/created-by": "e2e-test",
},
},
GatewayParentRefs: []gatewayv1.ParentReference{
{
Name: "test-gateway",
},
},
},
},
NetworkProfile: kamajiv1alpha1.NetworkProfileSpec{
Address: "172.18.0.3",
},
Kubernetes: kamajiv1alpha1.KubernetesSpec{
Version: "v1.23.6",
Kubelet: kamajiv1alpha1.KubeletSpec{
CGroupFS: "cgroupfs",
},
AdmissionControllers: kamajiv1alpha1.AdmissionControllers{
"LimitRanger",
"ResourceQuota",
},
},
Addons: kamajiv1alpha1.AddonsSpec{},
},
}
Expect(k8sClient.Create(context.Background(), tcp)).NotTo(HaveOccurred())
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.Background(), tcp)).Should(Succeed())
// Wait for the object to be completely deleted
Eventually(func() bool {
err := k8sClient.Get(context.Background(), types.NamespacedName{
Name: tcp.Name,
Namespace: tcp.Namespace,
}, &kamajiv1alpha1.TenantControlPlane{})
return err != nil // Returns true when object is not found (deleted)
}).WithTimeout(time.Minute).Should(BeTrue())
})
It("Should be Ready", func() {
StatusMustEqualTo(tcp, kamajiv1alpha1.VersionReady)
})
It("Should create TLSRoute resource", func() {
Eventually(func() error {
route := &gatewayv1alpha2.TLSRoute{}
// TODO: Check ownership.
return k8sClient.Get(context.Background(), types.NamespacedName{
Name: tcp.Name,
Namespace: tcp.Namespace,
}, route)
}).WithTimeout(time.Minute).Should(Succeed())
})
})

View File

@@ -23,14 +23,14 @@ var _ = Describe("using an unsupported TenantControlPlane Kubernetes version", f
v, err := semver.Make(upgrade.KubeadmVersion[1:])
Expect(err).ToNot(HaveOccurred())
unsupported, err := semver.Make(fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch+1))
unsupported, err := semver.Make(fmt.Sprintf("%d.%d.%d", v.Major, v.Minor+1, 0))
Expect(err).ToNot(HaveOccurred())
It("should be blocked on creation", func() {
Consistently(func() error {
tcp := kamajiv1alpha1.TenantControlPlane{
ObjectMeta: metav1.ObjectMeta{
Name: "non-linear-update",
Name: "unsupported-version",
Namespace: "default",
},
Spec: kamajiv1alpha1.TenantControlPlaneSpec{

86
go.mod
View File

@@ -6,7 +6,7 @@ require (
github.com/JamesStewy/go-mysqldump v0.2.2
github.com/blang/semver v3.5.1+incompatible
github.com/clastix/kamaji-telemetry v1.0.0
github.com/docker/docker v28.4.0+incompatible
github.com/docker/docker v28.5.2+incompatible
github.com/go-logr/logr v1.4.3
github.com/go-pg/pg/v10 v10.15.0
github.com/go-sql-driver/mysql v1.9.3
@@ -14,29 +14,30 @@ 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.45.0
github.com/onsi/ginkgo/v2 v2.25.3
github.com/nats-io/nats.go v1.47.0
github.com/onsi/ginkgo/v2 v2.27.2
github.com/onsi/gomega v1.38.2
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.23.2
github.com/spf13/cobra v1.10.1
github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.21.0
github.com/testcontainers/testcontainers-go v0.39.0
go.etcd.io/etcd/api/v3 v3.6.4
go.etcd.io/etcd/client/v3 v3.6.4
github.com/testcontainers/testcontainers-go v0.40.0
go.etcd.io/etcd/api/v3 v3.6.6
go.etcd.io/etcd/client/v3 v3.6.6
go.uber.org/automaxprocs v1.6.0
gomodules.xyz/jsonpatch/v2 v2.5.0
k8s.io/api v0.34.0
k8s.io/apimachinery v0.34.0
k8s.io/apiserver v0.34.0
k8s.io/client-go v0.34.0
k8s.io/api v0.34.1
k8s.io/apimachinery v0.34.1
k8s.io/apiserver v0.34.1
k8s.io/client-go v0.34.1
k8s.io/cluster-bootstrap v0.0.0
k8s.io/klog/v2 v2.130.1
k8s.io/kubelet v0.0.0
k8s.io/kubernetes v1.34.1
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
sigs.k8s.io/controller-runtime v0.22.1
k8s.io/kubernetes v1.34.2
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d
sigs.k8s.io/controller-runtime v0.22.4
sigs.k8s.io/gateway-api v1.4.0
)
require (
@@ -67,8 +68,8 @@ require (
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
@@ -77,9 +78,9 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/jsonpointer v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-pg/zerochecker v0.2.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
@@ -103,7 +104,7 @@ require (
github.com/lithammer/dedent v1.1.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.1.0 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
@@ -126,7 +127,7 @@ require (
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
@@ -146,55 +147,56 @@ 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.4 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.6 // 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.58.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/term v0.34.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.36.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/grpc v1.72.1 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/term v0.36.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect
google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.34.0 // indirect
k8s.io/apiextensions-apiserver v0.34.1 // indirect
k8s.io/cli-runtime v0.0.0 // indirect
k8s.io/cloud-provider v0.0.0 // indirect
k8s.io/component-base v0.34.0 // indirect
k8s.io/component-base v0.34.1 // indirect
k8s.io/component-helpers v0.34.0 // indirect
k8s.io/controller-manager v0.34.0 // indirect
k8s.io/cri-api v0.34.0 // indirect
k8s.io/cri-client v0.0.0 // indirect
k8s.io/kms v0.34.0 // indirect
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 // indirect
k8s.io/kube-proxy v0.0.0 // indirect
k8s.io/system-validators v1.10.1 // indirect
k8s.io/system-validators v1.10.2 // indirect
mellium.im/sasl v0.3.1 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/kustomize/api v0.20.1 // indirect
sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect

184
go.sum
View File

@@ -49,7 +49,6 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -58,8 +57,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk=
github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -68,10 +67,10 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI=
github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -83,6 +82,12 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE=
github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -94,14 +99,12 @@ github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA=
github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-pg/pg/v10 v10.15.0 h1:6DQwbaxJz/e4wvgzbxBkBLiL/Uuk87MGgHhkURtzx24=
github.com/go-pg/pg/v10 v10.15.0/go.mod h1:FIn/x04hahOf9ywQ1p68rXqaDVbTRLYlu4MQR0lhoB8=
github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU=
@@ -112,6 +115,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
@@ -158,6 +163,8 @@ github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbd
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c h1:3UvYABOQRhJAApj9MdCN+Ydv841ETSoy6xLzdmmr/9A=
@@ -186,7 +193,6 @@ github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCy
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -203,8 +209,12 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
@@ -233,8 +243,8 @@ 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.45.0 h1:/wGPbnYXDM0pLKFjZTX+2JOw9TQPoIgTFrUaH97giwA=
github.com/nats-io/nats.go v1.45.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.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/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -243,8 +253,8 @@ github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw=
github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -270,10 +280,10 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
@@ -316,8 +326,16 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts=
github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8=
github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
@@ -349,12 +367,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.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
go.etcd.io/etcd/api/v3 v3.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo=
go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk=
go.etcd.io/etcd/client/pkg/v3 v3.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0=
go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI=
go.etcd.io/etcd/client/v3 v3.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A=
go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo=
go.etcd.io/etcd/api/v3 v3.6.6 h1:mcaMp3+7JawWv69p6QShYWS8cIWUOl32bFLb6qf8pOQ=
go.etcd.io/etcd/api/v3 v3.6.6/go.mod h1:f/om26iXl2wSkcTA1zGQv8reJRSLVdoEBsi4JdfMrx4=
go.etcd.io/etcd/client/pkg/v3 v3.6.6 h1:uoqgzSOv2H9KlIF5O1Lsd8sW+eMLuV6wzE3q5GJGQNs=
go.etcd.io/etcd/client/pkg/v3 v3.6.6/go.mod h1:YngfUVmvsvOJ2rRgStIyHsKtOt9SZI2aBJrZiWJhCbI=
go.etcd.io/etcd/client/v3 v3.6.6 h1:G5z1wMf5B9SNexoxOHUGBaULurOZPIgGPsW6CN492ec=
go.etcd.io/etcd/client/v3 v3.6.6/go.mod h1:36Qv6baQ07znPR3+n7t+Rk5VHEzVYPvFfGmfF4wBHV8=
go.etcd.io/etcd/pkg/v3 v3.6.4 h1:fy8bmXIec1Q35/jRZ0KOes8vuFxbvdN0aAFqmEfJZWA=
go.etcd.io/etcd/pkg/v3 v3.6.4/go.mod h1:kKcYWP8gHuBRcteyv6MXWSN0+bVMnfgqiHueIZnKMtE=
go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU=
@@ -367,22 +385,22 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
@@ -400,28 +418,30 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -435,44 +455,46 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs=
gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
@@ -519,26 +541,28 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kms v0.34.0 h1:u+/rcxQ3Jr7gC9AY5nXuEnBcGEB7ZOIJ9cdLdyHyEjQ=
k8s.io/kms v0.34.0/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 h1:liMHz39T5dJO1aOKHLvwaCjDbf07wVh6yaUlTpunnkE=
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
k8s.io/kube-proxy v0.34.0 h1:gU7MVbJHiXyPX8bXnod4bANtSC7rZSKkkLmM8gUqwT4=
k8s.io/kube-proxy v0.34.0/go.mod h1:tfwI8dCKm5Q0r+aVIbrq/aC36Kk936w2LZu8/rvJzWI=
k8s.io/kubelet v0.34.0 h1:1nZt1Q6Kfx7xCaTS9vnqR9sjZDxf3cRSQkAFCczULmc=
k8s.io/kubelet v0.34.0/go.mod h1:NqbF8ViVettlZbf9hw9DJhubaWn7rGvDDTcLMDm6tQ0=
k8s.io/kubernetes v1.34.1 h1:F3p8dtpv+i8zQoebZeK5zBqM1g9x1aIdnA5vthvcuUk=
k8s.io/kubernetes v1.34.1/go.mod h1:iu+FhII+Oc/1gGWLJcer6wpyih441aNFHl7Pvm8yPto=
k8s.io/system-validators v1.10.1 h1:bIO3YRgxJkh/W3ghcd5ViXNPGmjwQKlHk/ySPdw6K00=
k8s.io/system-validators v1.10.1/go.mod h1:awfSS706v9R12VC7u7K89FKfqVy44G+E0L1A0FX9Wmw=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/kubernetes v1.34.2 h1:WQdDvYJazkmkwSncgNwGvVtaCt4TYXIU3wSMRgvp3MI=
k8s.io/kubernetes v1.34.2/go.mod h1:m6pZk6a179pRo2wsTiCPORJ86iOEQmfIzUvtyEF8BwA=
k8s.io/system-validators v1.10.2 h1:7rC7VdrQCaM55E08Pw3I1v1Op9ObLxdKAu5Ff5hIPwY=
k8s.io/system-validators v1.10.2/go.mod h1:awfSS706v9R12VC7u7K89FKfqVy44G+E0L1A0FX9Wmw=
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0=
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo=
mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg=
sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
sigs.k8s.io/gateway-api v1.4.0 h1:ZwlNM6zOHq0h3WUX2gfByPs2yAEsy/EenYJB78jpQfQ=
sigs.k8s.io/gateway-api v1.4.0/go.mod h1:AR5RSqciWP98OPckEjOjh2XJhAe2Na4LHyXD2FUY7Qk=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I=
sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM=
sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78=

View File

@@ -117,6 +117,7 @@ func (r *APIServerCertificate) mutate(ctx context.Context, tenantControlPlane *k
}
r.resource.SetLabels(utilities.MergeMaps(
r.resource.GetLabels(),
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
map[string]string{
constants.ControllerLabelResource: utilities.CertificateX509Label,

View File

@@ -104,6 +104,7 @@ func (r *APIServerKubeletClientCertificate) mutate(ctx context.Context, tenantCo
}
r.resource.SetLabels(utilities.MergeMaps(
r.resource.GetLabels(),
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
map[string]string{
constants.ControllerLabelResource: utilities.CertificateX509Label,

View File

@@ -153,7 +153,7 @@ func (r *CACertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1
corev1.TLSPrivateKeyKey: ca.PrivateKey,
}
r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
utilities.SetObjectChecksum(r.resource, r.resource.Data)

View File

@@ -106,6 +106,7 @@ func (r *Certificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1al
r.resource.Data["ca.crt"] = ca
r.resource.SetLabels(utilities.MergeMaps(
r.resource.GetLabels(),
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
map[string]string{
constants.ControllerLabelResource: utilities.CertificateX509Label,

View File

@@ -190,7 +190,7 @@ func (r *Config) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.
utilities.SetObjectChecksum(r.resource, r.resource.Data)
r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
}

View File

@@ -104,6 +104,7 @@ func (r *FrontProxyClientCertificate) mutate(ctx context.Context, tenantControlP
}
r.resource.SetLabels(utilities.MergeMaps(
r.resource.GetLabels(),
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
map[string]string{
constants.ControllerLabelResource: utilities.CertificateX509Label,

View File

@@ -126,7 +126,7 @@ func (r *FrontProxyCACertificate) mutate(ctx context.Context, tenantControlPlane
kubeadmconstants.FrontProxyCAKeyName: ca.PrivateKey,
}
r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
if isRotationRequested {
utilities.SetLastRotationTimestamp(r.resource)

View File

@@ -36,7 +36,9 @@ func (r *KubernetesDeploymentResource) isStatusEqual(tenantControlPlane *kamajiv
}
func (r *KubernetesDeploymentResource) ShouldStatusBeUpdated(_ context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
return !r.isStatusEqual(tenantControlPlane) || tenantControlPlane.Spec.Kubernetes.Version != tenantControlPlane.Status.Kubernetes.Version.Version
return !r.isStatusEqual(tenantControlPlane) ||
tenantControlPlane.Spec.Kubernetes.Version != tenantControlPlane.Status.Kubernetes.Version.Version ||
*r.computeStatus(tenantControlPlane) != ptr.Deref(tenantControlPlane.Status.Kubernetes.Version.Status, kamajiv1alpha1.VersionUnknown)
}
func (r *KubernetesDeploymentResource) ShouldCleanup(*kamajiv1alpha1.TenantControlPlane) bool {
@@ -78,19 +80,29 @@ func (r *KubernetesDeploymentResource) GetName() string {
return "deployment"
}
func (r *KubernetesDeploymentResource) UpdateTenantControlPlaneStatus(_ context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
func (r *KubernetesDeploymentResource) computeStatus(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) *kamajiv1alpha1.KubernetesVersionStatus {
switch {
case ptr.Deref(tenantControlPlane.Spec.ControlPlane.Deployment.Replicas, 2) == 0:
tenantControlPlane.Status.Kubernetes.Version.Status = &kamajiv1alpha1.VersionSleeping
case !r.isProgressingUpgrade():
tenantControlPlane.Status.Kubernetes.Version.Status = &kamajiv1alpha1.VersionReady
tenantControlPlane.Status.Kubernetes.Version.Version = tenantControlPlane.Spec.Kubernetes.Version
case r.isUpgrading(tenantControlPlane):
tenantControlPlane.Status.Kubernetes.Version.Status = &kamajiv1alpha1.VersionUpgrading
case r.isProvisioning(tenantControlPlane):
tenantControlPlane.Status.Kubernetes.Version.Status = &kamajiv1alpha1.VersionProvisioning
return &kamajiv1alpha1.VersionSleeping
case r.isNotReady():
tenantControlPlane.Status.Kubernetes.Version.Status = &kamajiv1alpha1.VersionNotReady
return &kamajiv1alpha1.VersionNotReady
case tenantControlPlane.Spec.WritePermissions.HasAnyLimitation():
return &kamajiv1alpha1.VersionWriteLimited
case !r.isProgressingUpgrade():
return &kamajiv1alpha1.VersionReady
case r.isUpgrading(tenantControlPlane):
return &kamajiv1alpha1.VersionUpgrading
case r.isProvisioning(tenantControlPlane):
return &kamajiv1alpha1.VersionProvisioning
default:
return &kamajiv1alpha1.VersionUnknown
}
}
func (r *KubernetesDeploymentResource) UpdateTenantControlPlaneStatus(_ context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
tenantControlPlane.Status.Kubernetes.Version.Status = r.computeStatus(tenantControlPlane)
if *tenantControlPlane.Status.Kubernetes.Version.Status == kamajiv1alpha1.VersionReady {
tenantControlPlane.Status.Kubernetes.Version.Version = tenantControlPlane.Spec.Kubernetes.Version
}
tenantControlPlane.Status.Kubernetes.Deployment = kamajiv1alpha1.KubernetesDeploymentStatus{

View File

@@ -0,0 +1,384 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"context"
"fmt"
"net/url"
"github.com/prometheus/client_golang/prometheus"
v1 "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"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/utilities"
)
type KubernetesGatewayResource struct {
resource *gatewayv1alpha2.TLSRoute
Client client.Client
}
func (r *KubernetesGatewayResource) GetHistogram() prometheus.Histogram {
gatewayCollector = LazyLoadHistogramFromResource(gatewayCollector, r)
return gatewayCollector
}
func (r *KubernetesGatewayResource) ShouldStatusBeUpdated(_ context.Context, tcp *kamajiv1alpha1.TenantControlPlane) bool {
switch {
case tcp.Spec.ControlPlane.Gateway == nil && tcp.Status.Kubernetes.Gateway == nil:
return false
case tcp.Spec.ControlPlane.Gateway != nil && tcp.Status.Kubernetes.Gateway == nil:
return true
case tcp.Spec.ControlPlane.Gateway == nil && tcp.Status.Kubernetes.Gateway != nil:
return true
// Could be an alias for default here since the other cases are covered.
case tcp.Spec.ControlPlane.Gateway != nil && tcp.Status.Kubernetes.Gateway != nil:
return r.gatewayStatusNeedsUpdate(tcp)
}
return false
}
// gatewayStatusNeedsUpdate compares the current gateway resource status with the stored status.
func (r *KubernetesGatewayResource) gatewayStatusNeedsUpdate(tcp *kamajiv1alpha1.TenantControlPlane) bool {
currentStatus := tcp.Status.Kubernetes.Gateway
// Check if route reference has changed
if currentStatus.RouteRef.Name != r.resource.Name {
return true
}
// Compare RouteStatus - check if number of parents changed
if len(currentStatus.RouteStatus.Parents) != len(r.resource.Status.RouteStatus.Parents) {
return true
}
// Compare individual parent statuses
// NOTE: Multiple Parent References are assumed.
for i, currentParent := range currentStatus.RouteStatus.Parents {
if i >= len(r.resource.Status.RouteStatus.Parents) {
return true
}
resourceParent := r.resource.Status.RouteStatus.Parents[i]
// Compare parent references
if currentParent.ParentRef.Name != resourceParent.ParentRef.Name ||
(currentParent.ParentRef.Namespace == nil) != (resourceParent.ParentRef.Namespace == nil) ||
(currentParent.ParentRef.Namespace != nil && resourceParent.ParentRef.Namespace != nil &&
*currentParent.ParentRef.Namespace != *resourceParent.ParentRef.Namespace) ||
(currentParent.ParentRef.SectionName == nil) != (resourceParent.ParentRef.SectionName == nil) ||
(currentParent.ParentRef.SectionName != nil && resourceParent.ParentRef.SectionName != nil &&
*currentParent.ParentRef.SectionName != *resourceParent.ParentRef.SectionName) {
return true
}
if len(currentParent.Conditions) != len(resourceParent.Conditions) {
return true
}
// Compare each condition
for j, currentCondition := range currentParent.Conditions {
if j >= len(resourceParent.Conditions) {
return true
}
resourceCondition := resourceParent.Conditions[j]
if currentCondition.Type != resourceCondition.Type ||
currentCondition.Status != resourceCondition.Status ||
currentCondition.Reason != resourceCondition.Reason ||
currentCondition.Message != resourceCondition.Message ||
!currentCondition.LastTransitionTime.Equal(&resourceCondition.LastTransitionTime) {
return true
}
}
}
// Since access points are derived from route status and gateway conditions,
// and we've already compared the route status above, we can assume that
// if the route status hasn't changed, the access points calculation
// will produce the same result. This avoids the need for complex
// gateway fetching in the status comparison.
//
// If there are edge cases where gateway state changes but route status doesn't,
// those will be caught in the next reconciliation cycle anyway.
return false
}
func (r *KubernetesGatewayResource) ShouldCleanup(tcp *kamajiv1alpha1.TenantControlPlane) bool {
return tcp.Spec.ControlPlane.Gateway == nil && tcp.Status.Kubernetes.Gateway != nil
}
func (r *KubernetesGatewayResource) CleanUp(ctx context.Context, tcp *kamajiv1alpha1.TenantControlPlane) (bool, error) {
logger := log.FromContext(ctx, "resource", r.GetName())
route := gatewayv1alpha2.TLSRoute{}
if err := r.Client.Get(ctx, client.ObjectKey{
Namespace: r.resource.GetNamespace(),
Name: r.resource.GetName(),
}, &route); err != nil {
if !k8serrors.IsNotFound(err) {
logger.Error(err, "failed to get TLSRoute before cleanup")
return false, err
}
return false, nil
}
if !metav1.IsControlledBy(&route, tcp) {
logger.Info("skipping cleanup: route is not managed by Kamaji", "name", route.Name, "namespace", route.Namespace)
return false, nil
}
if err := r.Client.Delete(ctx, &route); err != nil {
if !k8serrors.IsNotFound(err) {
// TODO: Is that an error? Wanted to delete the resource anyways.
logger.Error(err, "cannot cleanup tcp route")
return false, err
}
return false, nil
}
logger.V(1).Info("tcp route cleaned up successfully")
return true, nil
}
// fetchGatewayByListener uses the indexer to efficiently find a gateway with a specific listener.
// This avoids the need to iterate through all listeners in a gateway.
func (r *KubernetesGatewayResource) fetchGatewayByListener(ctx context.Context, ref gatewayv1.ParentReference) (*gatewayv1.Gateway, error) {
if ref.Namespace == nil {
return nil, fmt.Errorf("missing namespace")
}
if ref.SectionName == nil {
return nil, fmt.Errorf("missing sectionName")
}
// Build the composite key that matches our indexer format: namespace/gatewayName/listenerName
listenerKey := fmt.Sprintf("%s/%s/%s", *ref.Namespace, ref.Name, *ref.SectionName)
// Query gateways using the indexer
gatewayList := &gatewayv1.GatewayList{}
if err := r.Client.List(ctx, gatewayList, client.MatchingFieldsSelector{
Selector: fields.OneTermEqualSelector(kamajiv1alpha1.GatewayListenerNameKey, listenerKey),
}); err != nil {
return nil, fmt.Errorf("failed to list gateways by listener: %w", err)
}
if len(gatewayList.Items) == 0 {
return nil, fmt.Errorf("no gateway found with listener '%s'", *ref.SectionName)
}
// Since we're using a composite key with namespace/name/listener, we should get exactly one result
if len(gatewayList.Items) > 1 {
return nil, fmt.Errorf("found multiple gateways with listener '%s', expected exactly one", *ref.SectionName)
}
return &gatewayList.Items[0], nil
}
func FindMatchingListener(listeners []gatewayv1.Listener, ref gatewayv1.ParentReference) (gatewayv1.Listener, error) {
if ref.SectionName == nil {
return gatewayv1.Listener{}, fmt.Errorf("missing sectionName")
}
name := *ref.SectionName
for _, listener := range listeners {
if listener.Name == name {
return listener, nil
}
}
// TODO: Handle the cases according to the spec:
// - When both Port (experimental) and SectionName are
// specified, the name and port of the selected listener
// must match both specified values.
// - When unspecified (empty string) this will reference
// the entire resource [...] an attachment is considered
// successful if at least one section in the parent resource accepts it
return gatewayv1.Listener{}, fmt.Errorf("could not find listener '%s'", name)
}
func (r *KubernetesGatewayResource) UpdateTenantControlPlaneStatus(ctx context.Context, tcp *kamajiv1alpha1.TenantControlPlane) error {
logger := log.FromContext(ctx, "resource", r.GetName())
// Clean up status if Gateway routes are no longer configured
if tcp.Spec.ControlPlane.Gateway == nil {
tcp.Status.Kubernetes.Gateway = nil
return nil
}
tcp.Status.Kubernetes.Gateway = &kamajiv1alpha1.KubernetesGatewayStatus{
RouteStatus: r.resource.Status.RouteStatus,
RouteRef: v1.LocalObjectReference{
Name: r.resource.Name,
},
}
routeStatuses := tcp.Status.Kubernetes.Gateway.RouteStatus
// TODO: Investigate the implications of having multiple parents / hostnames
// TODO: Use condition to report?
if len(routeStatuses.Parents) == 0 {
return fmt.Errorf("no gateway attached to the route")
}
if len(routeStatuses.Parents) > 1 {
return fmt.Errorf("too many gateway attached to the route")
}
if len(r.resource.Spec.Hostnames) == 0 {
return fmt.Errorf("no hostname in the route")
}
if len(r.resource.Spec.Hostnames) > 1 {
return fmt.Errorf("too many hostnames in the route")
}
logger.V(1).Info("updating TenantControlPlane status for Gateway routes")
accessPoints := []kamajiv1alpha1.GatewayAccessPoint{}
for _, routeStatus := range routeStatuses.Parents {
routeAccepted := meta.IsStatusConditionTrue(
routeStatus.Conditions,
string(gatewayv1.RouteConditionAccepted),
)
if !routeAccepted {
continue
}
// Use the indexer to efficiently find the gateway with the specific listener
gateway, err := r.fetchGatewayByListener(ctx, routeStatus.ParentRef)
if err != nil {
return fmt.Errorf("could not fetch gateway with listener '%v': %w",
routeStatus.ParentRef.SectionName, err)
}
gatewayProgrammed := meta.IsStatusConditionTrue(
gateway.Status.Conditions,
string(gatewayv1.GatewayConditionProgrammed),
)
if !gatewayProgrammed {
continue
}
// Since we fetched the gateway using the indexer, we know the listener exists
// but we still need to get its details from the gateway spec
listener, err := FindMatchingListener(
gateway.Spec.Listeners, routeStatus.ParentRef,
)
if err != nil {
return fmt.Errorf("failed to match listener: %w", err)
}
for _, hostname := range r.resource.Spec.Hostnames {
rawURL := fmt.Sprintf("https://%s:%d", hostname, listener.Port)
url, err := url.Parse(rawURL)
if err != nil {
return fmt.Errorf("invalid url: %w", err)
}
hostnameAddressType := gatewayv1.HostnameAddressType
accessPoints = append(accessPoints, kamajiv1alpha1.GatewayAccessPoint{
Type: &hostnameAddressType,
Value: url.String(),
Port: listener.Port,
})
}
}
tcp.Status.Kubernetes.Gateway.AccessPoints = accessPoints
return nil
}
func (r *KubernetesGatewayResource) Define(_ context.Context, tcp *kamajiv1alpha1.TenantControlPlane) error {
r.resource = &gatewayv1alpha2.TLSRoute{
ObjectMeta: metav1.ObjectMeta{
Name: tcp.GetName(),
Namespace: tcp.GetNamespace(),
},
}
return nil
}
func (r *KubernetesGatewayResource) mutate(tcp *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn {
return func() error {
labels := utilities.MergeMaps(
r.resource.GetLabels(),
utilities.KamajiLabels(tcp.GetName(), r.GetName()),
tcp.Spec.ControlPlane.Gateway.AdditionalMetadata.Labels,
)
r.resource.SetLabels(labels)
annotations := utilities.MergeMaps(
r.resource.GetAnnotations(),
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
if serviceName == "" || servicePort == 0 {
return fmt.Errorf("service not ready, cannot create TLSRoute")
}
rule := gatewayv1alpha2.TLSRouteRule{
BackendRefs: []gatewayv1alpha2.BackendRef{
{
BackendObjectReference: gatewayv1alpha2.BackendObjectReference{
Name: serviceName,
Port: &servicePort,
},
},
},
}
r.resource.Spec.Hostnames = []gatewayv1.Hostname{tcp.Spec.ControlPlane.Gateway.Hostname}
r.resource.Spec.Rules = []gatewayv1alpha2.TLSRouteRule{rule}
return controllerutil.SetControllerReference(tcp, r.resource, r.Client.Scheme())
}
}
func (r *KubernetesGatewayResource) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
logger := log.FromContext(ctx, "resource", r.GetName())
if tenantControlPlane.Spec.ControlPlane.Gateway == nil {
return controllerutil.OperationResultNone, nil
}
if len(tenantControlPlane.Spec.ControlPlane.Gateway.Hostname) == 0 {
return controllerutil.OperationResultNone, fmt.Errorf("missing hostname to expose the Tenant Control Plane using a Gateway resource")
}
logger.V(1).Info("creating or updating resource gateway routes")
result, err := utilities.CreateOrUpdateWithConflict(ctx, r.Client, r.resource, r.mutate(tenantControlPlane))
if err != nil {
return result, err
}
return result, nil
}
func (r *KubernetesGatewayResource) GetName() string {
return "gateway_routes"
}

View File

@@ -0,0 +1,238 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources_test
import (
"context"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/resources"
)
func TestGatewayResource(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Gateway Resource Suite")
}
var runtimeScheme *runtime.Scheme
var _ = BeforeSuite(func() {
runtimeScheme = runtime.NewScheme()
Expect(scheme.AddToScheme(runtimeScheme)).To(Succeed())
Expect(kamajiv1alpha1.AddToScheme(runtimeScheme)).To(Succeed())
Expect(gatewayv1alpha2.Install(runtimeScheme)).To(Succeed())
})
var _ = Describe("KubernetesGatewayResource", func() {
var (
tcp *kamajiv1alpha1.TenantControlPlane
resource *resources.KubernetesGatewayResource
ctx context.Context
)
BeforeEach(func() {
ctx = context.Background()
fakeClient := fake.NewClientBuilder().
WithScheme(runtimeScheme).
Build()
resource = &resources.KubernetesGatewayResource{
Client: fakeClient,
}
tcp = &kamajiv1alpha1.TenantControlPlane{
ObjectMeta: metav1.ObjectMeta{
Name: "test-tcp",
Namespace: "default",
},
Spec: kamajiv1alpha1.TenantControlPlaneSpec{
ControlPlane: kamajiv1alpha1.ControlPlane{
Gateway: &kamajiv1alpha1.GatewaySpec{
Hostname: gatewayv1alpha2.Hostname("test.example.com"),
AdditionalMetadata: kamajiv1alpha1.AdditionalMetadata{
Labels: map[string]string{
"test-label": "test-value",
},
},
GatewayParentRefs: []gatewayv1alpha2.ParentReference{
{
Name: "test-gateway",
},
},
},
},
},
Status: kamajiv1alpha1.TenantControlPlaneStatus{
Kubernetes: kamajiv1alpha1.KubernetesStatus{
Service: kamajiv1alpha1.KubernetesServiceStatus{
Name: "test-service",
Port: 6443,
},
},
},
}
})
Context("When GatewayRoutes is configured", func() {
It("should not cleanup", func() {
Expect(resource.ShouldCleanup(tcp)).To(BeFalse())
})
It("should define route resources", func() {
err := resource.Define(ctx, tcp)
Expect(err).NotTo(HaveOccurred())
})
It("should require status update when GatewayRoutes is configured but status is nil", func() {
tcp.Status.Kubernetes.Gateway = nil
shouldUpdate := resource.ShouldStatusBeUpdated(ctx, tcp)
Expect(shouldUpdate).To(BeTrue())
})
})
Context("When GatewayRoutes is not configured", func() {
BeforeEach(func() {
tcp.Spec.ControlPlane.Gateway = nil
tcp.Status.Kubernetes.Gateway = &kamajiv1alpha1.KubernetesGatewayStatus{
AccessPoints: nil,
}
})
It("should cleanup", func() {
Expect(resource.ShouldCleanup(tcp)).To(BeTrue())
})
It("should not require status update when both spec and status are nil", func() {
tcp.Status.Kubernetes.Gateway = nil
shouldUpdate := resource.ShouldStatusBeUpdated(ctx, tcp)
Expect(shouldUpdate).To(BeFalse())
})
})
Context("When hostname is missing", func() {
BeforeEach(func() {
tcp.Spec.ControlPlane.Gateway.Hostname = ""
})
It("should fail to create or update", func() {
err := resource.Define(ctx, tcp)
Expect(err).NotTo(HaveOccurred())
_, err = resource.CreateOrUpdate(ctx, tcp)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("missing hostname"))
})
})
Context("When service is not ready", func() {
BeforeEach(func() {
tcp.Status.Kubernetes.Service.Name = ""
tcp.Status.Kubernetes.Service.Port = 0
})
It("should fail to create or update", func() {
err := resource.Define(ctx, tcp)
Expect(err).NotTo(HaveOccurred())
_, err = resource.CreateOrUpdate(ctx, tcp)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("service not ready"))
})
})
It("should return correct resource name", func() {
Expect(resource.GetName()).To(Equal("gateway_routes"))
})
Describe("findMatchingListener", func() {
var (
listeners []gatewayv1.Listener
ref gatewayv1.ParentReference
)
BeforeEach(func() {
listeners = []gatewayv1.Listener{
{
Name: "first",
Port: gatewayv1.PortNumber(443),
},
{
Name: "middle",
Port: gatewayv1.PortNumber(6443),
},
{
Name: "last",
Port: gatewayv1.PortNumber(80),
},
}
ref = gatewayv1.ParentReference{
Name: "test-gateway",
}
})
It("should return an error when sectionName is nil", func() {
listener, err := resources.FindMatchingListener(listeners, ref)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("missing sectionName"))
Expect(listener).To(Equal(gatewayv1.Listener{}))
})
It("should return an error when sectionName is an empty string", func() {
sectionName := gatewayv1.SectionName("")
ref.SectionName = &sectionName
listener, err := resources.FindMatchingListener(listeners, ref)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("could not find listener ''"))
Expect(listener).To(Equal(gatewayv1.Listener{}))
})
It("should return the matching listener when sectionName points to an existing listener", func() {
sectionName := gatewayv1.SectionName("middle")
ref.SectionName = &sectionName
listener, err := resources.FindMatchingListener(listeners, ref)
Expect(err).NotTo(HaveOccurred())
Expect(listener.Port).To(Equal(gatewayv1.PortNumber(6443)))
})
It("should return an error when sectionName points to a non-existent listener", func() {
sectionName := gatewayv1.SectionName("non-existent")
ref.SectionName = &sectionName
listener, err := resources.FindMatchingListener(listeners, ref)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("could not find listener 'non-existent'"))
Expect(listener).To(Equal(gatewayv1.Listener{}))
})
It("should return the first listener", func() {
sectionName := gatewayv1.SectionName("first")
ref.SectionName = &sectionName
listener, err := resources.FindMatchingListener(listeners, ref)
Expect(err).NotTo(HaveOccurred())
Expect(listener.Port).To(Equal(gatewayv1.PortNumber(443)))
})
It("should return the last listener when matching by name", func() {
sectionName := gatewayv1.SectionName("last")
ref.SectionName = &sectionName
listener, err := resources.FindMatchingListener(listeners, ref)
Expect(err).NotTo(HaveOccurred())
Expect(listener.Port).To(Equal(gatewayv1.PortNumber(80)))
})
})
})

View File

@@ -151,7 +151,7 @@ func (r *KubernetesIngressResource) Define(_ context.Context, tenantControlPlane
func (r *KubernetesIngressResource) mutate(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn {
return func() error {
labels := utilities.MergeMaps(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()), tenantControlPlane.Spec.ControlPlane.Ingress.AdditionalMetadata.Labels)
labels := utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()), tenantControlPlane.Spec.ControlPlane.Ingress.AdditionalMetadata.Labels)
r.resource.SetLabels(labels)
annotations := utilities.MergeMaps(r.resource.GetAnnotations(), tenantControlPlane.Spec.ControlPlane.Ingress.AdditionalMetadata.Annotations)

View File

@@ -83,7 +83,12 @@ func (r *KubernetesServiceResource) mutate(ctx context.Context, tenantControlPla
address, _ := tenantControlPlane.DeclaredControlPlaneAddress(ctx, r.Client)
return func() error {
labels := utilities.MergeMaps(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()), tenantControlPlane.Spec.ControlPlane.Service.AdditionalMetadata.Labels)
labels := utilities.MergeMaps(
r.resource.GetLabels(),
utilities.KamajiLabels(
tenantControlPlane.GetName(), r.GetName()),
tenantControlPlane.Spec.ControlPlane.Service.AdditionalMetadata.Labels,
)
r.resource.SetLabels(labels)
annotations := utilities.MergeMaps(r.resource.GetAnnotations(), tenantControlPlane.Spec.ControlPlane.Service.AdditionalMetadata.Annotations)
@@ -93,14 +98,37 @@ func (r *KubernetesServiceResource) mutate(ctx context.Context, tenantControlPla
"kamaji.clastix.io/name": tenantControlPlane.GetName(),
}
if len(r.resource.Spec.Ports) == 0 {
if r.resource.Spec.Ports == nil {
r.resource.Spec.Ports = make([]corev1.ServicePort, 1)
}
r.resource.Spec.Ports[0].Name = "kube-apiserver"
r.resource.Spec.Ports[0].Protocol = corev1.ProtocolTCP
r.resource.Spec.Ports[0].Port = tenantControlPlane.Spec.NetworkProfile.Port
r.resource.Spec.Ports[0].TargetPort = intstr.FromInt32(tenantControlPlane.Spec.NetworkProfile.Port)
var ports []corev1.ServicePort
for i, port := range r.resource.Spec.Ports {
switch {
case i == 0:
port.Name = "kube-apiserver"
port.Protocol = corev1.ProtocolTCP
port.Port = tenantControlPlane.Spec.NetworkProfile.Port
port.TargetPort = intstr.FromInt32(tenantControlPlane.Spec.NetworkProfile.Port)
ports = append(ports, port)
case i == 1 && port.Name == "konnectivity-server":
ports = append(ports, port)
}
}
for _, port := range tenantControlPlane.Spec.ControlPlane.Service.AdditionalPorts {
ports = append(ports, corev1.ServicePort{
Name: port.Name,
Protocol: port.Protocol,
AppProtocol: port.AppProtocol,
Port: port.Port,
TargetPort: port.TargetPort,
NodePort: 0,
})
}
r.resource.Spec.Ports = ports
switch tenantControlPlane.Spec.ControlPlane.Service.ServiceType {
case kamajiv1alpha1.ServiceTypeLoadBalancer:

View File

@@ -5,6 +5,7 @@ package konnectivity
import (
"context"
"crypto/x509"
"fmt"
"time"
@@ -102,6 +103,16 @@ func (r *CertificateResource) mutate(ctx context.Context, tenantControlPlane *ka
return func() error {
logger := log.FromContext(ctx, "resource", r.GetName())
// Retrieving the TenantControlPlane CA:
// this is required to trigger a new generation in case of Certificate Authority rotation.
namespacedName := k8stypes.NamespacedName{Namespace: tenantControlPlane.GetNamespace(), Name: tenantControlPlane.Status.Certificates.CA.SecretName}
secretCA := &corev1.Secret{}
if err := r.Client.Get(ctx, namespacedName, secretCA); err != nil {
logger.Error(err, "cannot retrieve the CA secret")
return err
}
r.resource.SetLabels(utilities.MergeMaps(
r.resource.GetLabels(),
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
@@ -119,23 +130,20 @@ func (r *CertificateResource) mutate(ctx context.Context, tenantControlPlane *ka
isRotationRequested := utilities.IsRotationRequested(r.resource)
if checksum := tenantControlPlane.Status.Addons.Konnectivity.Certificate.Checksum; !isRotationRequested && (len(checksum) > 0 && checksum == utilities.CalculateMapChecksum(r.resource.Data)) {
isCAValid, err := crypto.VerifyCertificate(r.resource.Data[corev1.TLSCertKey], secretCA.Data[kubeadmconstants.CACertName], x509.ExtKeyUsageServerAuth)
if err != nil {
logger.Info(fmt.Sprintf("certificate-authority verify failed: %s", err.Error()))
}
isValid, err := crypto.IsValidCertificateKeyPairBytes(r.resource.Data[corev1.TLSCertKey], r.resource.Data[corev1.TLSPrivateKeyKey], r.CertExpirationThreshold)
if err != nil {
logger.Info(fmt.Sprintf("%s certificate-private_key pair is not valid: %s", konnectivityCertAndKeyBaseName, err.Error()))
}
if isValid {
if isCAValid && isValid {
return nil
}
}
namespacedName := k8stypes.NamespacedName{Namespace: tenantControlPlane.GetNamespace(), Name: tenantControlPlane.Status.Certificates.CA.SecretName}
secretCA := &corev1.Secret{}
if err := r.Client.Get(ctx, namespacedName, secretCA); err != nil {
logger.Error(err, "cannot retrieve the CA secret")
return err
}
ca := kubeadm.CertificatePrivateKeyPair{
Name: kubeadmconstants.CACertAndKeyBaseName,
Certificate: secretCA.Data[kubeadmconstants.CACertName],

View File

@@ -77,14 +77,6 @@ func (r *KubeadmConfigResource) UpdateTenantControlPlaneStatus(_ context.Context
return nil
}
func (r *KubeadmConfigResource) getControlPlaneEndpoint(ingress *kamajiv1alpha1.IngressSpec, address string, port int32) string {
if ingress != nil && len(ingress.Hostname) > 0 {
address, port = utilities.GetControlPlaneAddressAndPortFromHostname(ingress.Hostname, port)
}
return net.JoinHostPort(address, strconv.FormatInt(int64(port), 10))
}
func (r *KubeadmConfigResource) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn {
return func() error {
logger := log.FromContext(ctx, "resource", r.GetName())
@@ -96,14 +88,29 @@ func (r *KubeadmConfigResource) mutate(ctx context.Context, tenantControlPlane *
return err
}
r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
endpoint := net.JoinHostPort(address, strconv.FormatInt(int64(port), 10))
spec := tenantControlPlane.Spec.ControlPlane
if spec.Gateway != nil {
if len(spec.Gateway.Hostname) > 0 {
gaddr, gport := utilities.GetControlPlaneAddressAndPortFromHostname(string(spec.Gateway.Hostname), port)
endpoint = net.JoinHostPort(gaddr, strconv.FormatInt(int64(gport), 10))
}
}
if spec.Ingress != nil {
if len(spec.Ingress.Hostname) > 0 {
iaddr, iport := utilities.GetControlPlaneAddressAndPortFromHostname(spec.Ingress.Hostname, port)
endpoint = net.JoinHostPort(iaddr, strconv.FormatInt(int64(iport), 10))
}
}
params := kubeadm.Parameters{
TenantControlPlaneAddress: address,
TenantControlPlanePort: port,
TenantControlPlaneName: tenantControlPlane.GetName(),
TenantControlPlaneNamespace: tenantControlPlane.GetNamespace(),
TenantControlPlaneEndpoint: r.getControlPlaneEndpoint(tenantControlPlane.Spec.ControlPlane.Ingress, address, port),
TenantControlPlaneEndpoint: endpoint,
TenantControlPlaneCertSANs: tenantControlPlane.Spec.NetworkProfile.CertSANs,
TenantControlPlaneClusterDomain: tenantControlPlane.Spec.NetworkProfile.ClusterDomain,
TenantControlPlanePodCIDR: tenantControlPlane.Spec.NetworkProfile.PodCIDR,

View File

@@ -172,6 +172,7 @@ func (r *KubeconfigResource) mutate(ctx context.Context, tenantControlPlane *kam
}
r.resource.SetLabels(utilities.MergeMaps(
r.resource.GetLabels(),
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
map[string]string{
constants.ControllerLabelResource: utilities.CertificateKubeconfigLabel,

View File

@@ -16,6 +16,7 @@ var (
frontproxycaCollector prometheus.Histogram
deploymentCollector prometheus.Histogram
ingressCollector prometheus.Histogram
gatewayCollector prometheus.Histogram
serviceCollector prometheus.Histogram
kubeadmconfigCollector prometheus.Histogram
kubeadmupgradeCollector prometheus.Histogram

View File

@@ -122,7 +122,7 @@ func (r *SACertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1
kubeadmconstants.ServiceAccountPrivateKeyName: sa.PrivateKey,
}
r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
if isRotationRequested {
utilities.SetLastRotationTimestamp(r.resource)

View File

@@ -0,0 +1,109 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package utilities
import (
"context"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
"sigs.k8s.io/controller-runtime/pkg/client"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
)
// AreGatewayResourcesAvailable checks if Gateway API is available in the cluster through a discovery Client
// with fallback to client-based check.
func AreGatewayResourcesAvailable(ctx context.Context, c client.Client, discoveryClient discovery.DiscoveryInterface) bool {
if discoveryClient == nil {
return IsGatewayAPIAvailableViaClient(ctx, c)
}
available, err := GatewayAPIResourcesAvailable(ctx, discoveryClient)
if err != nil {
return false
}
return available
}
// NOTE: These functions are extremely similar, maybe they can be merged and accept a GVK.
// Explicit for now.
// GatewayAPIResourcesAvailable checks if Gateway API is available in the cluster.
func GatewayAPIResourcesAvailable(ctx context.Context, discoveryClient discovery.DiscoveryInterface) (bool, error) {
gatewayAPIGroup := gatewayv1.GroupName
serverGroups, err := discoveryClient.ServerGroups()
if err != nil {
return false, err
}
for _, group := range serverGroups.Groups {
if group.Name == gatewayAPIGroup {
return true, nil
}
}
return false, nil
}
// TLSRouteAPIAvailable checks specifically for TLSRoute resource availability.
func TLSRouteAPIAvailable(ctx context.Context, discoveryClient discovery.DiscoveryInterface) (bool, error) {
gv := gatewayv1alpha2.SchemeGroupVersion
resourceList, err := discoveryClient.ServerResourcesForGroupVersion(gv.String())
if err != nil {
return false, err
}
for _, resource := range resourceList.APIResources {
if resource.Kind == "TLSRoute" {
return true, nil
}
}
return false, nil
}
// IsTLSRouteAvailable checks if TLSRoute is available with fallback to client-based check.
func IsTLSRouteAvailable(ctx context.Context, c client.Client, discoveryClient discovery.DiscoveryInterface) bool {
if discoveryClient == nil {
return IsTLSRouteAvailableViaClient(ctx, c)
}
available, err := TLSRouteAPIAvailable(ctx, discoveryClient)
if err != nil {
return false
}
return available
}
// IsTLSRouteAvailableViaClient uses client to check TLSRoute availability.
func IsTLSRouteAvailableViaClient(ctx context.Context, c client.Client) bool {
// Try to check if TLSRoute GVK can be resolved
gvk := schema.GroupVersionKind{
Group: gatewayv1alpha2.GroupName,
Version: "v1alpha2",
Kind: "TLSRoute",
}
restMapper := c.RESTMapper()
_, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
if meta.IsNoMatchError(err) {
return false
}
// Other errors might be transient, assume available
return true
}
return true
}
// IsGatewayAPIAvailableViaClient uses client to check Gateway API availability.
func IsGatewayAPIAvailableViaClient(ctx context.Context, c client.Client) bool {
return IsTLSRouteAvailableViaClient(ctx, c)
}

View File

@@ -25,18 +25,21 @@ type handlersChainer struct {
//nolint:gocognit
func (h handlersChainer) Handler(object runtime.Object, routeHandlers ...handlers.Handler) admission.HandlerFunc {
return func(ctx context.Context, req admission.Request) admission.Response {
decodedObj, oldDecodedObj := object.DeepCopyObject(), object.DeepCopyObject()
var decodedObj, oldDecodedObj runtime.Object
if object != nil {
decodedObj, oldDecodedObj = object.DeepCopyObject(), object.DeepCopyObject()
switch req.Operation {
case admissionv1.Delete:
// When deleting the OldObject struct field contains the object being deleted:
// https://github.com/kubernetes/kubernetes/pull/76346
if err := h.decoder.DecodeRaw(req.OldObject, decodedObj); err != nil {
return admission.Errored(http.StatusInternalServerError, errors.Wrap(err, fmt.Sprintf("unable to decode deleted object into %T", object)))
}
default:
if err := h.decoder.Decode(req, decodedObj); err != nil {
return admission.Errored(http.StatusInternalServerError, errors.Wrap(err, fmt.Sprintf("unable to decode into %T", object)))
switch req.Operation {
case admissionv1.Delete:
// When deleting the OldObject struct field contains the object being deleted:
// https://github.com/kubernetes/kubernetes/pull/76346
if err := h.decoder.DecodeRaw(req.OldObject, decodedObj); err != nil {
return admission.Errored(http.StatusInternalServerError, errors.Wrap(err, fmt.Sprintf("unable to decode deleted object into %T", object)))
}
default:
if err := h.decoder.Decode(req, decodedObj); err != nil {
return admission.Errored(http.StatusInternalServerError, errors.Wrap(err, fmt.Sprintf("unable to decode into %T", object)))
}
}
}

View File

@@ -0,0 +1,77 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package handlers
import (
"context"
"fmt"
"gomodules.xyz/jsonpatch/v2"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/discovery"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/utilities"
"github.com/clastix/kamaji/internal/webhook/utils"
)
type TenantControlPlaneGatewayValidation struct {
Client client.Client
DiscoveryClient discovery.DiscoveryInterface
}
func (t TenantControlPlaneGatewayValidation) OnCreate(object runtime.Object) AdmissionResponse {
return func(ctx context.Context, _ admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
tcp, ok := object.(*kamajiv1alpha1.TenantControlPlane)
if !ok {
return nil, fmt.Errorf("cannot cast object to TenantControlPlane")
}
if tcp.Spec.ControlPlane.Gateway != nil {
// NOTE: Do we actually want to deny here if Gateway API is not available or a warning?
// Seems sensible to deny to avoid anything.
if err := t.validateGatewayAPIAvailability(ctx); err != nil {
return nil, err
}
}
return nil, nil
}
}
func (t TenantControlPlaneGatewayValidation) OnUpdate(object runtime.Object, _ runtime.Object) AdmissionResponse {
return func(ctx context.Context, _ admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
tcp, ok := object.(*kamajiv1alpha1.TenantControlPlane)
if !ok {
return nil, fmt.Errorf("cannot cast object to TenantControlPlane")
}
if tcp.Spec.ControlPlane.Gateway != nil {
if err := t.validateGatewayAPIAvailability(ctx); err != nil {
return nil, err
}
}
return nil, nil
}
}
func (t TenantControlPlaneGatewayValidation) OnDelete(object runtime.Object) AdmissionResponse {
return utils.NilOp()
}
func (t TenantControlPlaneGatewayValidation) validateGatewayAPIAvailability(ctx context.Context) error {
if !utilities.AreGatewayResourcesAvailable(ctx, t.Client, t.DiscoveryClient) {
return fmt.Errorf("the Gateway API is not available in this cluster, cannot use gatewayRoute configuration")
}
// Additional check for TLSRoute specifically
if !utilities.IsTLSRouteAvailable(ctx, t.Client, t.DiscoveryClient) {
return fmt.Errorf("TLSRoute resource is not available in this cluster")
}
return nil
}

View File

@@ -0,0 +1,253 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package handlers_test
import (
"context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/discovery"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/webhook/handlers"
)
// Mock discovery client for testing.
type mockDiscoveryClient struct {
discovery.DiscoveryInterface
serverGroups *metav1.APIGroupList
serverGroupsError error
serverResources map[string]*metav1.APIResourceList
serverResourcesError map[string]error
}
func (m *mockDiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) {
return m.serverGroups, m.serverGroupsError
}
func (m *mockDiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
if err, exists := m.serverResourcesError[groupVersion]; exists {
return nil, err
}
if resources, exists := m.serverResources[groupVersion]; exists {
return resources, nil
}
return &metav1.APIResourceList{}, nil
}
var _ = Describe("TCP Gateway Validation Webhook", func() {
var (
ctx context.Context
handler handlers.TenantControlPlaneGatewayValidation
tcp *kamajiv1alpha1.TenantControlPlane
mockClient client.Client
mockDiscovery *mockDiscoveryClient
)
BeforeEach(func() {
ctx = context.Background()
mockClient = nil
mockDiscovery = &mockDiscoveryClient{
serverResources: make(map[string]*metav1.APIResourceList),
serverResourcesError: make(map[string]error),
}
handler = handlers.TenantControlPlaneGatewayValidation{
Client: mockClient,
DiscoveryClient: mockDiscovery,
}
tcp = &kamajiv1alpha1.TenantControlPlane{
ObjectMeta: metav1.ObjectMeta{
Name: "test-tcp",
Namespace: "default",
},
Spec: kamajiv1alpha1.TenantControlPlaneSpec{},
}
})
Context("when TenantControlPlane has no Gateway configuration", func() {
It("should allow creation without Gateway APIs", func() {
mockDiscovery.serverGroups = &metav1.APIGroupList{
Groups: []metav1.APIGroup{},
}
_, err := handler.OnCreate(tcp)(ctx, admission.Request{})
Expect(err).ToNot(HaveOccurred())
})
It("should allow creation with Gateway APIs available", func() {
mockDiscovery.serverGroups = &metav1.APIGroupList{
Groups: []metav1.APIGroup{
{Name: "gateway.networking.k8s.io"},
},
}
_, err := handler.OnCreate(tcp)(ctx, admission.Request{})
Expect(err).ToNot(HaveOccurred())
})
})
Context("when TenantControlPlane has Gateway configuration", func() {
BeforeEach(func() {
tcp.Spec.ControlPlane.Gateway = &kamajiv1alpha1.GatewaySpec{
Hostname: gatewayv1.Hostname("api.example.com"),
}
})
Context("and Gateway APIs are available", func() {
BeforeEach(func() {
mockDiscovery.serverGroups = &metav1.APIGroupList{
Groups: []metav1.APIGroup{
{Name: "gateway.networking.k8s.io"},
},
}
mockDiscovery.serverResources["gateway.networking.k8s.io/v1alpha2"] = &metav1.APIResourceList{
APIResources: []metav1.APIResource{
{Kind: "TLSRoute"},
},
}
})
It("should allow creation", func() {
_, err := handler.OnCreate(tcp)(ctx, admission.Request{})
Expect(err).ToNot(HaveOccurred())
})
It("should allow updates", func() {
oldTCP := tcp.DeepCopy()
_, err := handler.OnUpdate(tcp, oldTCP)(ctx, admission.Request{})
Expect(err).ToNot(HaveOccurred())
})
})
Context("and Gateway APIs are not available", func() {
BeforeEach(func() {
mockDiscovery.serverGroups = &metav1.APIGroupList{
Groups: []metav1.APIGroup{},
}
})
It("should deny creation with clear error message", func() {
_, err := handler.OnCreate(tcp)(ctx, admission.Request{})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Gateway API is not available in this cluster"))
})
It("should deny updates with clear error message", func() {
oldTCP := tcp.DeepCopy()
_, err := handler.OnUpdate(tcp, oldTCP)(ctx, admission.Request{})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Gateway API is not available in this cluster"))
})
})
Context("and Gateway API group exists but TLSRoute is not available", func() {
BeforeEach(func() {
mockDiscovery.serverGroups = &metav1.APIGroupList{
Groups: []metav1.APIGroup{
{Name: "gateway.networking.k8s.io"},
},
}
mockDiscovery.serverResources["gateway.networking.k8s.io/v1alpha2"] = &metav1.APIResourceList{
APIResources: []metav1.APIResource{
{Kind: "Gateway"},
{Kind: "HTTPRoute"},
},
}
})
It("should deny creation when TLSRoute is missing", func() {
_, err := handler.OnCreate(tcp)(ctx, admission.Request{})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("TLSRoute resource is not available"))
})
})
})
Context("when Gateway configuration is added in update", func() {
It("should validate Gateway APIs when adding Gateway configuration", func() {
oldTCP := tcp.DeepCopy()
tcp.Spec.ControlPlane.Gateway = &kamajiv1alpha1.GatewaySpec{
Hostname: gatewayv1.Hostname("api.example.com"),
}
mockDiscovery.serverGroups = &metav1.APIGroupList{
Groups: []metav1.APIGroup{},
}
_, err := handler.OnUpdate(tcp, oldTCP)(ctx, admission.Request{})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Gateway API is not available"))
})
It("should allow removing Gateway configuration", func() {
// Start with Gateway configuration
oldTCP := tcp.DeepCopy()
oldTCP.Spec.ControlPlane.Gateway = &kamajiv1alpha1.GatewaySpec{
Hostname: gatewayv1.Hostname("api.example.com"),
}
mockDiscovery.serverGroups = &metav1.APIGroupList{
Groups: []metav1.APIGroup{},
}
_, err := handler.OnUpdate(tcp, oldTCP)(ctx, admission.Request{})
Expect(err).ToNot(HaveOccurred())
})
})
Context("OnDelete operations", func() {
It("should always allow delete operations", func() {
tcp.Spec.ControlPlane.Gateway = &kamajiv1alpha1.GatewaySpec{
Hostname: gatewayv1.Hostname("api.example.com"),
}
mockDiscovery.serverGroups = &metav1.APIGroupList{
Groups: []metav1.APIGroup{},
}
admissionResponse := handler.OnDelete(tcp)
_, err := admissionResponse(ctx, admission.Request{})
Expect(err).ToNot(HaveOccurred())
})
})
Context("with different Gateway API versions", func() {
BeforeEach(func() {
tcp.Spec.ControlPlane.Gateway = &kamajiv1alpha1.GatewaySpec{
Hostname: gatewayv1.Hostname("api.example.com"),
}
mockDiscovery.serverGroups = &metav1.APIGroupList{
Groups: []metav1.APIGroup{
{Name: "gateway.networking.k8s.io"},
},
}
})
It("should work with v1alpha2 TLSRoute", func() {
mockDiscovery.serverResources["gateway.networking.k8s.io/v1alpha2"] = &metav1.APIResourceList{
APIResources: []metav1.APIResource{
{Kind: "TLSRoute"},
},
}
_, err := handler.OnCreate(tcp)(ctx, admission.Request{})
Expect(err).ToNot(HaveOccurred())
})
It("should handle missing version gracefully", func() {
_, err := handler.OnCreate(tcp)(ctx, admission.Request{})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("TLSRoute resource is not available"))
})
})
})

View File

@@ -29,6 +29,8 @@ func (t TenantControlPlaneVersion) OnCreate(object runtime.Object) AdmissionResp
if err != nil {
return nil, errors.Wrap(err, "unable to parse the desired Kubernetes version")
}
// No need to check if the patch version
ver.Patch = 0
supportedVer, supportedErr := semver.Make(t.normalizeKubernetesVersion(upgrade.KubeadmVersion))
if supportedErr != nil {
@@ -36,7 +38,7 @@ func (t TenantControlPlaneVersion) OnCreate(object runtime.Object) AdmissionResp
}
if ver.GT(supportedVer) {
return nil, fmt.Errorf("unable to create a TenantControlPlane with a Kubernetes version greater than the supported one, actually %s", supportedVer.String())
return nil, fmt.Errorf("unable to create a TenantControlPlane with a Kubernetes version greater than the supported one, actually v%d.%d", supportedVer.Major, supportedVer.Minor)
}
return nil, nil
@@ -59,15 +61,23 @@ func (t TenantControlPlaneVersion) OnUpdate(object runtime.Object, oldObject run
return func(context.Context, admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
newTCP, oldTCP := object.(*kamajiv1alpha1.TenantControlPlane), oldObject.(*kamajiv1alpha1.TenantControlPlane) //nolint:forcetypeassert
if newTCP.DeletionTimestamp != nil {
return nil, nil
}
oldVer, oldErr := semver.Make(t.normalizeKubernetesVersion(oldTCP.Spec.Kubernetes.Version))
if oldErr != nil {
return nil, errors.Wrap(oldErr, "unable to parse the previous Kubernetes version")
}
// No need to check if the patch version
oldVer.Patch = 0
newVer, newErr := semver.New(t.normalizeKubernetesVersion(newTCP.Spec.Kubernetes.Version))
if newErr != nil {
return nil, errors.Wrap(newErr, "unable to parse the desired Kubernetes version")
}
// No need to check if the patch version
newVer.Patch = 0
supportedVer, supportedErr := semver.Make(t.normalizeKubernetesVersion(upgrade.KubeadmVersion))
if supportedErr != nil {
@@ -76,7 +86,7 @@ func (t TenantControlPlaneVersion) OnUpdate(object runtime.Object, oldObject run
switch {
case newVer.GT(supportedVer):
return nil, fmt.Errorf("unable to upgrade to a version greater than the supported one, actually %s", supportedVer.String())
return nil, fmt.Errorf("unable to upgrade to a version greater than the supported one (v%d.%d)", supportedVer.Major, supportedVer.Minor)
case newVer.LT(oldVer):
return nil, fmt.Errorf("unable to downgrade a TenantControlPlane from %s to %s", oldVer.String(), newVer.String())
case newVer.Minor-oldVer.Minor > 1:

View File

@@ -0,0 +1,32 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package handlers
import (
"context"
"fmt"
"gomodules.xyz/jsonpatch/v2"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)
type WritePermission struct{}
func (f WritePermission) OnCreate(runtime.Object) AdmissionResponse {
return f.response
}
func (f WritePermission) OnDelete(runtime.Object) AdmissionResponse {
return f.response
}
func (f WritePermission) OnUpdate(runtime.Object, runtime.Object) AdmissionResponse {
return f.response
}
func (f WritePermission) response(context.Context, admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
return nil, fmt.Errorf("the current Control Plane has limited write permissions, current changes are blocked: " +
"removing the webhook may lead to an inconsistent state upon its completion")
}

View File

@@ -4,7 +4,6 @@
package routes
import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
)
@@ -15,5 +14,5 @@ func (t TenantControlPlaneMigrate) GetPath() string {
}
func (t TenantControlPlaneMigrate) GetObject() runtime.Object {
return &corev1.Namespace{}
return nil
}

View File

@@ -0,0 +1,18 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package routes
import (
"k8s.io/apimachinery/pkg/runtime"
)
type TenantControlPlaneWritePermission struct{}
func (t TenantControlPlaneWritePermission) GetPath() string {
return "/write-permission"
}
func (t TenantControlPlaneWritePermission) GetObject() runtime.Object {
return nil
}

View File

@@ -9,6 +9,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"github.com/clastix/kamaji/cmd"
kubeconfig_generator "github.com/clastix/kamaji/cmd/kubeconfig-generator"
"github.com/clastix/kamaji/cmd/manager"
"github.com/clastix/kamaji/cmd/migrate"
)
@@ -16,9 +17,10 @@ import (
func main() {
scheme := runtime.NewScheme()
root, mgr, migrator := cmd.NewCmd(scheme), manager.NewCmd(scheme), migrate.NewCmd(scheme)
root, mgr, migrator, kubeconfigGenerator := cmd.NewCmd(scheme), manager.NewCmd(scheme), migrate.NewCmd(scheme), kubeconfig_generator.NewCmd(scheme)
root.AddCommand(mgr)
root.AddCommand(migrator)
root.AddCommand(kubeconfigGenerator)
if err := root.Execute(); err != nil {
os.Exit(1)