Compare commits

...

214 Commits

Author SHA1 Message Date
Enrico Candino
237a3cb280 Bump Charts to 0.3.4-rc3 (#445) 2025-08-25 19:02:30 +02:00
Enrico Candino
d23cf86fce Fix missing custom-certs flag in cli (#444)
* fix missing custom-certs path in cli

* fix docs
2025-08-25 18:37:29 +02:00
Enrico Candino
65cb8ad123 bump chart (#440) 2025-08-19 10:57:07 +02:00
Hussein Galal
6db88b5a00 Revert "Fix pod fieldpath annotation translation (#434)" (#435)
This reverts commit 883d401ae3.
2025-08-18 14:28:19 +03:00
Hussein Galal
8d89c7d133 Fix service port for generated kubeconfig secret (#433)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-08-18 09:17:30 +03:00
Hussein Galal
883d401ae3 Fix pod fieldpath annotation translation (#434)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-08-18 09:16:58 +03:00
Enrico Candino
f85702dc23 Bump version and appVersion to 0.3.4-rc1 in Chart.yaml (#429) 2025-07-24 17:13:12 +02:00
Enrico Candino
084701fcd9 Migrate from urfave/cli to cobra (#426)
* wip

* env var fix

* cluster create

* cluster create and delete

* cluster list

* cluster cmd

* kubeconfig

* policy create

* policy delete and list, and added root commands

* removed urfavecli from k3kcli

* fix policy command

* k3k-kubelet to cobra

* updated docs

* updated go.mod

* updated test

* added deletion

* added cleanup and flake attempts

* wip bind env

* simplified config
2025-07-24 16:49:40 +02:00
Enrico Candino
5eb1d2a5bb Adding some tests for k3kcli (#417)
* adding some cli tests

* added coverage and tests

* fix lint and cli tests

* fix defer

* some more cli tests
2025-07-23 11:03:41 +02:00
Enrico Candino
98d17cdb50 Added new golangci-lint formatters (#425)
* add gci formatter

* gofmt and gofumpt

* rewrite rule

* added make fmt
2025-07-22 10:42:41 +02:00
Enrico Candino
2047a600ed Migrate golangci-lint to v2 (#424)
* golangci-lint upgrade

* fix lint
2025-07-22 10:10:26 +02:00
Hussein Galal
a98c49b59a Adding custom certificate to the virtual clusters (#409)
* Adding custom certificate to the virtual clusters

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fix tests

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix tests

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* docs update

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* integrate cert-manager

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add individual cert tests

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* docs

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* docs

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-07-21 19:23:11 +03:00
Enrico Candino
1048e3f82d fix for portallocator initialization (#423) 2025-07-21 17:03:39 +02:00
Alex Bissessur
c480bc339e update ver for k3kcli install (#421)
Signed-off-by: xelab04 <alexbissessur@gmail.com>
2025-07-21 11:18:34 +02:00
Enrico Candino
a0af20f20f codecov (#418) 2025-07-18 11:50:57 +02:00
Enrico Candino
748a439d7a fix for restoring policy (#413) 2025-07-17 10:25:09 +02:00
Enrico Candino
0a55bec305 improve chart-release workflow (#412) 2025-07-14 15:56:30 +02:00
Enrico Candino
2ab71df139 Add Conditions and current status to Cluster (#408)
* Added Cluster Conditions

* added e2e tests

* fix lint

* cli polling

* update tests
2025-07-14 15:53:37 +02:00
Enrico Candino
753b31b52a Adding configurable maxConcurrentReconcilers and small CRD cleanup (#410)
* removed Persistence from Status, fixed default for StorageSize and StorageDefault

* added configurable maxConcurrentReconciles

* fix concurrent issues

* add validate as prereq for tests
2025-07-10 14:46:33 +02:00
Hussein Galal
fcc875ab85 Mirror host nodes (#389)
* mirror host nodes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* add mirror host nodes feature

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add controllername to secrets/configmap syncer

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* golint

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* build docs

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* setting controller namespace env

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fix typo

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add a controller_namespace env to the test

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fix tests

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fix tests

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add mirrorHostNodes spec to conformance tests

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* change the ptr int to int

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fix map key name

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-07-08 14:48:24 +03:00
Enrico Candino
57263bd10e fail fast matrix (#398) 2025-07-01 11:04:56 +02:00
Enrico Candino
bf82318ad9 Add PriorityClass reconciler (virtual cluster -> host) (#377)
* added priorityclass controller

* added priorityClass controller tests

* fix for update priorityClass

* fix system skip priorityclass

* fix name
2025-07-01 11:03:14 +02:00
jpgouin
1ca86d09d1 add troubleshoot how to guide (#390)
* add troubleshoot how to guide

Co-authored-by: Enrico Candino <enrico.candino@gmail.com>
2025-06-30 16:54:13 +02:00
Enrico Candino
584bae8974 bump charts (#403) 2025-06-30 10:43:53 +02:00
Hussein Galal
5a24c4edf7 bump charts to 0.3.3-r6 (#401)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-06-27 17:02:23 +03:00
Hussein Galal
44aa1a22ab Add pods/attach permission to k3k-kubelet (#400)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-06-27 16:42:05 +03:00
Enrico Candino
2b115a0b80 Add scheduled Conformance tests for shared mode (#396)
* add conformance tests with matrix

* fix serial

* splitted conformance and sigs

* push

* sig check focus fix

* cleanup cluster

* matrix for conformance tests

* removed push
2025-06-26 15:55:08 +02:00
Enrico Candino
8eb5c49ce4 bump chart (#395) 2025-06-25 10:48:52 +02:00
Enrico Candino
54ae8d2126 add named controller (#394) 2025-06-24 23:56:14 +02:00
Hussein Galal
3a101dccfd bump charts to 0.3.3-r4 (#393)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-06-24 00:11:17 +03:00
Hussein Galal
b81073619a Generate kubeconfig secret (#392)
* Generate kubeconfig secret

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fix typo

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fix typo

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-06-23 14:31:36 +03:00
Enrico Candino
f5d2e981ab Bump Charts to 0.3.3-r3 (#391)
* bump charts to 0.3.3-r2

* bump charts to 0.3.3-r3
2025-06-20 18:19:10 +02:00
jpgouin
541f506d9d [CLI] add storage-request-size flag (#372)
[CLI] add storage-request-size flag
2025-06-20 17:13:47 +02:00
Enrico Candino
f389a4e2be Fix Network Policy reconciliation (#388)
* logs

* fix delete cleanup

* update spec

* added policyName to status, skip netpol for policy managed clusters
2025-06-20 16:10:47 +02:00
jpgouin
818328c9d4 fix-howto usage of serverEnvs and agentEnvs (#385) 2025-06-20 12:32:43 +02:00
Enrico Candino
0c4752039d Fix for k3kcli policy delete (#386)
* fix for delete policy

* fix docs
2025-06-20 12:08:51 +02:00
Enrico Candino
eca219cb48 kubelet controllers cleanup (#376) 2025-06-20 12:08:28 +02:00
Hussein Galal
d1f88c32b3 Ephemeral containers fix (#371)
* Update virtual kubelet and k8s to 1.31.4

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix ephemeral containers in provider

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix linters

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix comments

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-06-20 12:52:45 +03:00
Enrico Candino
b8f0e77a71 fix for empty namespace (#375) 2025-06-19 14:28:27 +02:00
jpgouin
08ba3944e0 [DOC] add how-to create virtual clusters (#373)
* [DOC] add how-to create virtual clusters
2025-06-06 12:39:51 +02:00
Enrico Candino
09e8a180de charts-0.3.3-r1 (#370) 2025-06-04 09:50:48 +02:00
jpgouin
87032c8195 [DOC] add how to choose mode (#369)
*[DOC] add how to choose between `shared` and `virtual` mode doc
2025-06-03 11:12:12 +02:00
jpgouin
78e0c307b8 add workload exposition howto doc (#366)
* [DOC] add how to expose workloads outside the virtual cluster
2025-06-03 09:14:20 +02:00
Enrico Candino
5758b880a5 Added k3kcli cluster list and k3kcli policy list commands (#368)
* list commands

* go mod tidy

* moved logic to separate file, small refactor
2025-05-30 15:35:31 +02:00
Enrico Candino
2655d792cc Update allowedModeTypes field to allowedMode (#367)
* change allowedModeTypse to allowedMode

* added shortname "vcp" and additional mode column
2025-05-29 14:53:58 +02:00
Enrico Candino
93e1c85468 VirtualClusterPolicy documentation (#364)
* initial docs

* update for change/cleanup

* update crd docs config

* updated links

* crd docs updates
2025-05-29 11:18:26 +02:00
Enrico Candino
8fbe4b93e8 Change VirtualClusterPolicy scope to Cluster (#358)
* rename clusterset to policy

* fixes

* rename clusterset to policy

* wip

* go mod

* cluster scoped

* gomod

* gomod

* fix lint

* wip

* moved logic to vcp controller

* update for clusters

* small fixes

* update cli

* fix docs, updated spec

* fix cleanup

* added missing owns for limitranges
2025-05-29 10:45:48 +02:00
jpgouin
2515d19187 move howtos in docs folder (#362) 2025-05-27 14:11:13 +02:00
jpgouin
2b1448ffb8 add air-gap support (#359)
* add airgap support
* add airgap howto guide
2025-05-27 10:13:07 +02:00
Enrico Candino
fdb5bb9c19 update version in readme (#357) 2025-05-19 20:27:23 +02:00
Hussein Galal
45fdbf9363 Fix DNS options and allow custom dnsConfig (#354)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-05-15 15:58:36 +03:00
Enrico Candino
3590b48d91 remove --devel reference (#353) 2025-05-15 12:07:08 +02:00
Enrico Candino
cca3d0c309 Rename ClusterSet to VirtualClusterPolicy (#349)
* rename clusterset to policy

* fixes
2025-05-15 12:04:47 +02:00
Hussein Galal
f228c4536c Fix annotation update (#335)
* Fix annotation update

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix annotation update

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix annotation update

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-05-12 13:02:05 +03:00
Hussein Galal
37fe4493e7 Fix HA init server scaling (#333)
* Fix HA init server scaling

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* increase timeout in e2e test

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-05-12 11:51:35 +03:00
Enrico Candino
6a22f6f704 fix build-crds, bump Go to 1.24.2, bump golangci-lint (#344) 2025-05-06 17:24:35 +02:00
Enrico Candino
96a4341dfb Services updates (LoadBalancerConfig and NodePortConfig) (#329)
* updates to services

- added loadBalancerConfig
- removed service-port
- added logic to not expose services

* Refactor cluster tests to improve readability and maintainability

- Simplified service port expectations by directly accessing elements instead of using `ContainElement`.
- Enhanced clarity of test assertions for `k3s-server-port` and `k3s-etcd-port` attributes.
- Removed redundant code for checking service ports.

* fix ports for ingress expose, update kubeconfig generate
2025-04-22 11:52:18 +02:00
Hussein Galal
510ab4bb8a Add extra env for servers/agents (#324)
* Add extra env for servers/agents

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* cli docs

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix container env

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-04-21 12:25:51 +02:00
Hussein Galal
9d96ee1e9c Display error if agents flag is provided in shared mode (#330)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-04-18 17:48:43 +02:00
Enrico Candino
7c424821ca Update Chart.yaml (#332) 2025-04-18 12:43:14 +02:00
jpgouin
a2f5fd7592 Merge pull request #328 from takushi-35/ehemeral-docs
[DOC] fix: Incorrect creation method when using Ephemeral Storage in advanced-usage.md
2025-04-11 14:27:40 +02:00
takushi-35
c8df86b83b fix: Correcting incorrect procedures in advanced-usage.md 2025-04-10 13:27:00 +09:00
Hussein Galal
d41d2b8c31 Fix update bug in ensureObject (#325)
* Fix update bug in ensureObjects

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix log msg

Co-authored-by: Enrico Candino <enrico.candino@gmail.com>

* Fix import

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
Co-authored-by: Enrico Candino <enrico.candino@gmail.com>
2025-04-09 17:25:48 +02:00
Enrico Candino
7cb2399b89 Update and fix to k3kcli for new ClusterSet integration (#321)
* added clusterset flag to cluster creation and displayname to clusterset creation

* updated cli docs
2025-04-04 13:22:55 +02:00
Enrico Candino
90568f24b1 Added ClusterSet as singleton (#316)
* added ClusterSet as singleton

* fix tests
2025-04-03 16:26:25 +02:00
Hussein Galal
0843a9e313 Initial support for ResourceQuotas in clustersets (#308)
* Add ResourceQuota to clusterset

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Generate docs

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fix tests

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add a defualt limitRange for ClusterSets

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fix linting

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fix docs

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add test for clusterset limitRange

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add server and worker limits

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* make charts

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* add default limits and fixes to resourcesquota

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl lint

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl lint

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* make docs

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* make build-crds

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* make build-crds

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* make spec as pointer

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fix tests

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fix tests

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* delete default limit

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl lint

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Update tests

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Update tests

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* return on delete in limitrange

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-04-03 12:30:48 +02:00
Enrico Candino
b58578788c Add clusterset commands (#319)
* added clusterset create command, small refactor with appcontext

* added clusterset delete

* updated docs
2025-04-03 11:07:32 +02:00
Enrico Candino
c4cc1e69cd requeue if server not ready (#318) 2025-04-03 10:45:18 +02:00
Enrico Candino
bd947c0fcb Create dedicated namespace for new clusters (#314)
* create dedicated namespace for new clusters

* porcelain test

* use --exit-code instead of test and shell for escaping issue

* update go.mod
2025-03-26 14:53:41 +01:00
Hussein Galal
b0b61f8d8e Fix delete cli (#281)
* Fix delete cli

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* make lint

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* update docs

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix delete cli

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl lint

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* wsl lint

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* check if object has a controller reference before removing

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* move the update to the if condition

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* move the update to the if condition

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-03-24 13:44:00 +02:00
Enrico Candino
3281d54c6c fix typo (#300) 2025-03-24 10:48:42 +01:00
Hussein Galal
853b0a7e05 Chart update for 0.3.1 (#309)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-03-21 01:59:53 +02:00
Rossella Sblendido
28b15d2e92 Merge pull request #299 from rancher/doc-k3d
Add documentation to install k3k on k3d
2025-03-10 11:07:02 +01:00
rossella
cad59c0494 Moved how to to development.md 2025-03-07 17:38:22 +01:00
Enrico Candino
d0810af17c Fix kubeconfig load from multiple configuration files (#301)
* fix kubeconfig load with standards kubectl approach

* update cli docs
2025-03-07 15:11:00 +01:00
Enrico Candino
2b7202e676 Added NetworkPolicy for Cluster isolation (#290)
* added cluster NetworkPolicy

* wip tests

* remove focus

* added networking test

* test refactoring

* unfocus

* revert labels

* added async creation of clusters, and namespace deletion

* add unfocus validation
2025-03-07 14:36:49 +01:00
rossella
4975b0b799 Add documentation to install k3k on k3d
k3d is a valid local option for testing k3k. Adding documentation
on how to install k3k on top of k3d.
2025-03-04 16:48:21 +01:00
jpgouin
90d17cd6dd Merge pull request #288 from jp-gouin/fix-cli 2025-03-04 13:48:34 +01:00
Justin J. Janes
3e5e9c7965 adding a new issue template for bug reports (#258)
* adding a new issue template for bug reports

* adding extra helper commands for logs from k3k
2025-03-04 12:33:34 +02:00
jpgouin
1d027909ee cli - fix bug with Kubeconfig value resolution 2025-03-04 10:23:54 +00:00
Enrico Candino
6105402bf2 charts-0.3.1-r1 (#283) 2025-03-03 17:14:13 +01:00
jpgouin
6031eeb09b Merge pull request #273 from jp-gouin/readme-1
Update quickstart pre-requisites and generate automatically cli docs
2025-03-03 14:44:11 +01:00
jpgouin
2f582a473a update cli docs 2025-03-03 13:31:40 +00:00
jpgouin
26d3d29ba1 remove reference to persistence type 2025-03-03 13:21:07 +00:00
jpgouin
e97a3f5966 remove reference to persistence type 2025-03-03 13:21:07 +00:00
jpgouin
07b9cdcc86 update readme and advanced-usage doc 2025-03-03 13:21:07 +00:00
jpgouin
a3cbe42782 update doc and genclidoc 2025-03-03 13:21:07 +00:00
jpgouin
3cf8c0a744 default the kubeconfig flag to $HOME/.kube/config 2025-03-03 13:21:07 +00:00
jpgouin
ddc367516b fix test 2025-03-03 13:21:07 +00:00
jpgouin
7b83b9fd36 fix test 2025-03-03 13:21:07 +00:00
jpgouin
bf8fdd9071 fix lint 2025-03-03 13:21:07 +00:00
jpgouin
4ca5203df1 Flatten the cli structure, add docs generation in the Makefile and remove crds/Makefile 2025-03-03 13:21:07 +00:00
Enrico Candino
5e8bc0d3cd Update CRDs documentation (#279)
* complete CRD documentation

* fix missing rebuild of CRDs
2025-03-03 11:47:53 +01:00
Enrico Candino
430e18bf30 Added wsl linter, and fixed related issues (#275)
* added wsl linter

* fixed issues
2025-02-27 10:59:02 +01:00
Hussein Galal
ec0e5a4a87 Support for multi node in shared mode (#269)
* Support for multi node in shared mode

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fixing typo

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-02-26 22:55:31 +02:00
Hussein Galal
29438121ba Fix server/agent extra args in the cli (#272)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-02-26 11:32:25 +02:00
Hussein Galal
c2cde0c9ba Fix the default CIDRs for both modes (#271)
* Fix the default CIDRs for both modes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix service/cluster cidr

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-02-26 11:32:17 +02:00
jpgouin
1be43e0564 Merge pull request #270 from jp-gouin/readme-1
Update README.md
2025-02-25 11:27:57 +01:00
jpgouin
8913772240 Update README.md
Co-authored-by: Enrico Candino <enrico.candino@gmail.com>
2025-02-25 11:07:47 +01:00
jpgouin
dbbe03ca96 Update README.md
Co-authored-by: Enrico Candino <enrico.candino@gmail.com>
2025-02-25 10:56:45 +01:00
jpgouin
e52a682cca Update README.md
add explanation to create a cluster from a host cluster accessed through Rancher
2025-02-24 14:22:56 +01:00
Enrico Candino
26a0bb6583 Added ServiceCIDR lookup, and changed default (#263)
* added serviceCIDR lookup

* fix log

* fix comment

* swap serviceCIDR lookup
2025-02-24 12:08:58 +01:00
Enrico Candino
dee20455ee added multiarch support (#262) 2025-02-21 14:36:17 +01:00
Enrico Candino
f5c9a4b3a1 fix duplicated envvars, added tests (#260) 2025-02-19 16:27:46 +01:00
Enrico Candino
65fe7a678f fix k3s version metadata (#256) 2025-02-18 16:04:11 +01:00
Hussein Galal
8811ba74de Fix cluster spec update (#257)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-02-18 15:40:49 +02:00
Enrico Candino
127b5fc848 Remove dapper (#254)
* wip drop dapper

* added tests, validate

* fix kubebuilder assets

* debug

* fix maybe

* export global

* export global 2

* fix goreleaser

* dev doc section improved

* crd and docs

* drop dapper

* drop unused tmpl

* added help

* typos, and added `build-crds` target to default
2025-02-18 11:59:20 +01:00
Hussein Galal
d95e3fd33a Chart update for v0.3.0 (#255)
* Chart update for v0.3.0

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Update chart to 0.3.0

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Update chart to 0.3.0-r1

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-02-17 15:06:22 +02:00
Hussein Galal
1f4b3c4835 Assign pod's hostname if not assigned (#253)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-02-17 12:47:49 +02:00
Enrico Candino
0056e4a3f7 add check for number of arguments (#252) 2025-02-17 10:37:42 +01:00
Hussein Galal
8bc5519db0 Update chart (#251)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-02-14 15:28:20 +02:00
Hussein Galal
fa553d25d4 Default to dynamic persistence and fix HA restarts (#250)
* Default to dynamic persistence and fix HA restarts

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-02-14 14:26:10 +02:00
Enrico Candino
51a8fd8a8d Fix and enhancements to IngressExposeConfig (annotations) (#248)
* ingress fixes

* added annotations to IngressConfig

* sync annotations with CR

* removed hosts

* small doc for ingress
2025-02-14 12:38:42 +01:00
Enrico Candino
fdb133ad4a Added Ports to NodePortConfig and expose fixes (#247)
* fix NodePort service update

* updated crd docs
2025-02-11 14:47:01 +01:00
Enrico Candino
0aa60b7f3a Update of some docs (#231)
* updated README, docs folder

* updated architecture doc

* shared and virtual architecture images

* advanced usage

* added crd-ref-docs tool for CRDs documentation

* small fixes

* requested changes

* full example in advanced usage

* removed security part

* Apply suggestions from code review

Co-authored-by: jpgouin <jp-gouin@hotmail.fr>

---------

Co-authored-by: jpgouin <jp-gouin@hotmail.fr>
2025-02-10 16:21:33 +01:00
Enrico Candino
8d1bda4733 fix panic for nil Expose (#240) 2025-02-10 12:44:53 +01:00
Hussein Galal
f23b538f11 Fix metadata information for the virtual pods (#228)
* Fix metadata information for the virtual pods

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-02-10 10:24:56 +02:00
Hussein Galal
ac132a5840 Fixing etcd pod controller (#233)
* Fixing etcd pod controller

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix logic in etcd pod controller

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-02-06 22:36:05 +02:00
Enrico Candino
2f44b4068a Small cli refactor (cluster name as arg, default kubeconfig path) (#230)
* cli refactor

* restored name
2025-02-05 23:54:40 +01:00
Enrico Candino
48efbe575e Fix Webhook certificate recreate (#226)
* wip cert webhook

* fix lint

* cleanup and refactor

* fix go.mod

* removed logs

* renamed

* small simplification

* improved logging

* improved logging

* some tests for config data

* fix logs

* moved interface
2025-02-05 21:55:34 +01:00
jpgouin
3df5a5b780 Merge pull request #213 from jp-gouin/fix-ingress
fix ingress creation, use the ingress host in Kubeconfig when enabled
2025-02-04 09:31:49 +01:00
Enrico Candino
2a7541cdca Fix missing updates of server certificates (#219)
* merge

* wip test

* added test for restart

* tests reorg

* simplified tests
2025-02-04 09:17:56 +01:00
Enrico Candino
997216f4bb chart-releaser action (#222) 2025-02-04 09:17:27 +01:00
Enrico Candino
bc3f906280 Fix status update, updated k3s default version, updated CRDs (#218)
* fix status update

* fix schema and default image

* removed retry in controller

* removed fmt
2025-01-30 12:56:42 +01:00
Hussein Galal
19efdc81c3 Add initial support for daemonsets (#217)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-01-30 00:59:25 +02:00
Enrico Candino
54be0ba9d8 Logs and organization cleanup (#208)
* logs and organization cleanup

* getting log from context

* reused log var
2025-01-29 12:03:33 +01:00
Enrico Candino
72b5a98dff Fix typos and adding spellcheck linter (#215)
* adding spellcheck linter

* fix typos
2025-01-28 17:47:45 +01:00
jpgouin
2019decc78 check value of cluster.Spec.Expose.Ingress 2025-01-28 14:34:50 +00:00
jpgouin
ebdeb3aa58 fix merge 2025-01-28 14:27:50 +00:00
jpgouin
c88890e502 Merge branch 'main' into fix-ingress 2025-01-28 15:11:09 +01:00
Hussein Galal
86d543b4be Fix webhook restarts in k3k-kubelet (#214)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-01-28 01:52:55 +02:00
Enrico Candino
44045c5592 Added test (virtual cluster creation, with pod) and small kubeconfig refactor (#211)
* added virtual cluster and pod test

* moved ClusterCreate

* match patch k8s host version
2025-01-24 22:26:01 +01:00
jpgouin
e6db5a34c8 fix ingress creation, use the ingress host in Kubeconfig when enabled 2025-01-24 18:48:31 +00:00
jpgouin
8f24151b3f Merge pull request #209 from jp-gouin/cli
add flag to override kubernetes server value in the generated kubeconfig
2025-01-24 10:46:26 +01:00
Hussein Galal
8b0383f35e Fix chart release action (#210)
* Fix chart release action

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix chart release action

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-01-23 21:02:34 +02:00
jpgouin
ec93371b71 check err 2025-01-23 16:29:43 +00:00
jpgouin
5721c108b6 add flag to override kubernetes server value in the generated kubeconfig 2025-01-23 16:16:45 +00:00
Enrico Candino
9e52c375a0 bump urfave/cli to v2 (#205) 2025-01-23 10:14:01 +01:00
Hussein Galal
ca8f30fd9e upgrade chart (#207)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-01-23 02:30:12 +02:00
Hussein Galal
931c7c5fcb Fix secret tokens and DNS translation (#200)
* Include init containers in token translation

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix kubernetes.defaul service DNS translation

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add skip test var to dapper

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add kubelet version and image pull policy to the shared agent

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-01-23 01:55:05 +02:00
Enrico Candino
fd6ed8184f removed antiaffinity (#199) 2025-01-22 18:34:30 +01:00
Enrico Candino
c285004944 fix release tag (#201) 2025-01-22 15:18:10 +01:00
Enrico Candino
b0aa22b2f4 Simplify Cluster spec (#193)
* removed some required parameters, adding defaults

* add hostVersion in Status field

* fix tests
2025-01-21 21:19:44 +01:00
Enrico Candino
3f49593f96 Add Cluster creation test (#192)
* added k3kcli to path

* test create cluster

* updated ptr

* added cluster creation test
2025-01-21 17:53:42 +01:00
Enrico Candino
0b3a5f250e Added golangci-lint action (#197)
* added golangci-lint action

* linters

* cleanup linters

* fix error, increase timeout

* removed unnecessary call to Stringer
2025-01-21 11:30:57 +01:00
Enrico Candino
e7671134d2 fixed missing version (#196) 2025-01-21 10:52:27 +01:00
Enrico Candino
f9b3d62413 bump go1.23 (#198) 2025-01-21 10:50:23 +01:00
Enrico Candino
d4368da9a0 E2E tests scaffolding (#189)
* testcontainers

add build script

dropped namespace from chart

upload logs

removed old tests

* show go.mod diffs
2025-01-16 20:40:53 +01:00
Hussein Galal
c93cdd0333 Add retry for k3k-kubelet provider functions (#188)
* Add retry for k3k kubelet provider functions

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add retry for k3k kubelet provider function

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* go mod tidy

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-01-16 21:34:28 +02:00
Enrico Candino
958d515a59 removed Namespace creation from charts, edited default (#190) 2025-01-16 18:34:17 +01:00
Hussein Galal
9d0c907df2 Fix downward api for status fields in k3k-kubelet (#185)
* Fix downward api for status fields in k3k-kubelet

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-01-16 02:40:17 +02:00
Enrico Candino
1691d48875 Fix for UpdatePod (#187)
* fix for UpdatePod

* removed print
2025-01-15 18:50:21 +01:00
Enrico Candino
960afe9504 fix error for existing webhook (#186) 2025-01-15 18:43:12 +01:00
Enrico Candino
349f54d627 fix for default priorityClasses (#182) 2025-01-14 20:30:16 +01:00
Hussein Galal
ccaa09fa4a Add PVC syncing support (#179)
* Add pvc syncing support

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2025-01-14 20:57:04 +02:00
Enrico Candino
f9ddec53b7 Added priorityClass to Clusters and ClusterSets (#180)
* added priorityClass to Clusters and ClusterSets

* fixed comment
2025-01-14 11:05:48 +01:00
Enrico Candino
5892121dbe Fix action event check on wrong field event_name (#177)
The event name should be checked against the `event_name` field.
2025-01-09 11:28:43 +01:00
Enrico Candino
524dc69b98 Fix for missing permission (#176) 2025-01-09 10:25:38 +01:00
Enrico Candino
4fdce5b1aa Test release workflows (#173)
* goreleaser action

* removed old release

* fix gomega version in tests

* updated build workflow

* fix for empty var
2025-01-09 10:10:53 +01:00
Enrico Candino
9fc4a57fc2 Fix go.mod (#171)
* check go mod

* fix go.mod
2025-01-08 10:02:23 +01:00
Enrico Candino
ee00b08927 Add real node resources to virtual node (#169)
* add real nodes capacity to virtual node

* distinguish capacity from allocatable node resources
2025-01-02 22:22:18 +01:00
Enrico Candino
7fdd48d577 Implementation of GetStatsSummary and GetMetricsResource for Virtual Kubelet (#163)
* implemented  GetStatsSummary and GetMetricsResource for Virtual Kubelet

* fixed ClusterRole for node proxy

* limit the clusterrole with get and list

* remove unused Metrics client interface
2024-12-27 11:41:40 +01:00
jpgouin
70a098df4c allow exec into pod and fetching log in shared mode (#160) 2024-12-17 11:41:17 +01:00
Hussein Galal
6739aa0382 Initial networking support for shared mode (#154)
* Initial networking support for shared mode

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix deletion logic and controller reference

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* golintci

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-12-10 23:22:55 +02:00
Enrico Candino
acd9d96732 fix timeout (#157) 2024-12-05 19:34:12 +01:00
Enrico Candino
72b2a5f1d1 Added podSecurityAdmissionLevel to ClusterSet (#145)
* added Namespace reconciliation for PodSecurity labels

* added Namespace Watch

* added tests, and example

* bump deps
2024-12-04 21:38:02 +01:00
Enrico Candino
8e7d0f43a9 changed cluster creation backoff (#156) 2024-12-04 20:44:43 +01:00
Enrico Candino
a235b85362 Bump testing dependencies (#155)
* fixed testing deps, added doc

* added manual dispatch
2024-12-04 20:31:33 +01:00
Enrico Candino
6d716e43b2 Bump deps and enable tests on PRs (#152)
* enable tests on PRs

* bump deps
2024-11-28 20:13:57 +01:00
Enrico Candino
6db5247ff7 fix netpol reconciliation (#150) 2024-11-28 01:44:37 +01:00
Enrico Candino
c561b033df Added allowedNodeTypes to ClusterSet, and fixed NetworkPolicy reconciliation (#144)
* updated CRDs

* added Mode to ClusterSet, and enum to CRD

* fix typos

* fix mode type in cli

* deletion of second clusterset in same namespace

* removed focused test, added clusterset example

* renamed modes

* added allowedNodeTypes, fixed samples

* fixed network policy reconciliation
2024-11-27 23:00:39 +02:00
Enrico Candino
37573d36a4 Added envtest integration tests for ClusterSet (#143)
* init tests

* added clusterset tests

* added github action

* updated Dapper with envtest bins
2024-11-11 18:13:20 +02:00
Hussein Galal
bc25c1c70a Serviceaccount token synchronization (#139)
* Serviceaccount token sync

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fixing typo

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-11-08 00:11:56 +02:00
Enrico Candino
c9599963d1 added node selector to workloads (#138) 2024-11-06 21:50:51 +02:00
Hussein Galal
84f921641b Token random generation (#136)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-11-01 21:27:03 +02:00
Michael Bolot
26a7fa023f Adding basic volume syncing (#137)
* Adding basic volume syncing

Adds syncing for basic volume types (secret/configmap/projected secret
and configmap). Also changes the virtual kubelet to use a cache from
controller-runtime rather than a client for some operations.
2024-10-31 11:57:59 -05:00
Hussein Galal
7599d6946f Fix virtual node types (#135)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-10-24 23:56:17 +03:00
Hussein Galal
f04902f0a2 Add structured logging via zap (#133)
* Add structured logging properly

use a centralized logger wrapper to work with controller and virt-kubelet

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix some log messages

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-10-22 01:04:21 +03:00
Hussein Galal
d19f0f9ca6 virtual-kubelet controller integration (#130)
* Virtual kubelet controller integration

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add k3k-kubelet image to the release workflow

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add k3k-kubelet image to the release workflow

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix build/release workflow

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Remove pkg directory in k3k-kubelet

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* rename Type to Config

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Move the kubelet and config outside of pkg

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fix comments

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix naming throughout the package

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix comments

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* more fixes to naming

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-10-21 22:54:08 +03:00
Hussein Galal
bf1fe2a71c Adding Networkpolicy to ClusterSets (#125)
* Adding cluster set types

Adds types for cluster sets, which allows constraining a few elements of
clusters including: overall resource usage, and which nodes it can use.

* Add networkpolicy to clustersets

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix comments

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix linting issues

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fixing node controller logic and nit fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* more fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fix main cli

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Comment the resource quota for clustersets

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
Co-authored-by: Michael Bolot <michael.bolot@suse.com>
2024-10-16 00:27:42 +03:00
Hussein Galal
dbe6767aff Adding experimental disclaimer (#129)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-10-11 00:33:27 +03:00
Michael Bolot
ab33b3cb3f Adding poc for virtual kubelet (#112)
Adds a POC for running pods in the host cluster powered by virtual kubelet.
2024-10-01 00:33:10 +03:00
Michael Bolot
56da25941f Fixing bugs with namespaced clusters (#111)
Fixes a few bugs with namespaced clusters, specifically:
- The agent config still used a hardcoded value for the config secret
  mount
- The kubeconfig generation still used the old "cluster namespace" as
  the destination
In addition, changes the headless service name to not have two "-".
2024-09-06 02:15:36 +03:00
Michael Bolot
9faab4f82d Changing the cluster to be namespaced (#110)
* Changing the cluster to be namespaced

Changes the cluster type to be namespaced (and changes the various
controllers to work with this new feature). Also adds crd generation and
docs to the core cluster type.

* CI fix
2024-09-05 22:50:11 +03:00
Hussein Galal
bf72d39280 Use gh tool (#106)
* use gh tool instead of third party gh action

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix checksum

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add GH_TOKEN env

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-06-21 23:56:43 +03:00
Hussein Galal
3879912b57 Move to Github Action (#105)
* Move to Github Action

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Move to Github Action

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fix code generation

* Add release and chart workflows

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add release and chart workflows

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add release and chart workflows

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add release and chart workflows

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* test release and charts

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* test release and charts

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* test release and charts

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* test release and charts

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* test release and charts

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix GHA migration

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix GHA migration

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-05-21 00:00:47 +03:00
Hussein Galal
0d6bf4922a Fix code generation (#104)
* Fix code generation

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* update go

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-05-14 01:05:13 +03:00
Cuong Nguyen Duc
57c24f6f3c Correct file name (#95) 2024-03-18 08:46:59 +02:00
Hussein Galal
fe23607b71 Update chart to 0.1.4-r1 (#98)
* Update chart to 0.1.4-r1

* Update image to v0.2.1
2024-03-15 02:10:33 +02:00
Hussein Galal
caa0537d5e Renaming binaries and fix typo (#97) 2024-03-15 01:39:18 +02:00
Hussein Galal
0cad65e4fe Fix for readiness probe (#96)
* Fix for readiness probe

* update code generator code
2024-03-15 01:04:52 +02:00
Hussein Galal
cc914cf870 Update chart (#91)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-02-15 23:59:59 +02:00
Hussein Galal
ba35d12124 Cluster spec update (#90)
* Remove unused functions

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* enable cluster server and agent update

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-01-25 06:37:59 +02:00
Hussein Galal
6fc22df6bc Cluster type validations (#89)
* Cluster type validations

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Cluster type validations

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-01-12 23:09:30 +02:00
Hussein Galal
c92f722122 Add delete subcommand (#88)
* Add delete subcommand

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add delete subcommand

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-01-11 02:36:12 +02:00
Hussein Galal
5e141fe98e Add kubeconfig subcommand (#87)
* Add kubeconfig subcommand

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add kubeconfig subcommand

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add kubeconfig subcommand

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add kubeconfig subcommand

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-01-11 00:57:46 +02:00
Hussein Galal
4b2308e709 Update chart to v0.1.2-r1 (#82)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-01-06 07:38:54 +02:00
Hussein Galal
3cdcb04e1a Add validation for system cluster name for both controller and cli (#81)
* Add validation for system cluster name for both controller and cli

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add validation for system cluster name for both controller and cli

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Add validation for system cluster name for both controller and cli

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-01-06 02:15:20 +02:00
Hussein Galal
fedfa109b5 Fix append to empty slice (#80)
* Fix append to empty slice

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix initialization of addresses slice

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-01-04 01:49:48 +02:00
Hussein Galal
99d043f2ee fix chart releases (#79)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-01-03 02:55:09 +02:00
Hussein Galal
57ed675a7f fix chart releases (#78)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-01-03 02:49:05 +02:00
Hussein Galal
7c9060c394 fix chart release (#77)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-01-03 02:37:08 +02:00
Hussein Galal
a104aacf5f Add github config mail and username for pushing k3k release (#76)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-01-03 02:24:46 +02:00
Hussein Galal
6346b06eb3 Add github config mail and username for pushing k3k release (#75)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-01-03 02:08:10 +02:00
Hussein Galal
6fd745f268 Fix chart release (#74)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-01-03 01:53:26 +02:00
Hussein Galal
1258fb6d58 Upgrade chart and fix manifest (#73)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2024-01-03 00:03:08 +02:00
Matt Trachier
130dbb0a33 Fix: upgrade go in go.mod (#64)
Signed-off-by: matttrach <matttrach@gmail.com>
2023-12-13 00:21:26 +02:00
Hussein Galal
67c8cac611 [controller] HA stabilization and fix rejoining ephermal nodes (#68)
* Remove etcd member if server pod gets removed

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* make sure to add finalizer to server pod only in HA mode

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* Fix recursion bug and add new fields to cluster status

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fixing comments

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fixing comments

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2023-12-13 00:21:02 +02:00
Hussein Galal
dd618e580a use statefulsets for servers (#67)
* use statefulsets for servers

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* remove unused code

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* fix comments

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2023-11-28 23:51:50 +02:00
Paulo Gomes
dc2f410c17 build: Align drone base images (#66)
Align the base images used in drone with the images used across the
ecosystem.
2023-11-28 19:01:37 +02:00
Hussein Galal
a620f6c66f Fix kubeconfig extract in cli (#65)
Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2023-11-28 19:00:10 +02:00
Hussein Galal
3c283ce178 Add readiness probe and fix readme (#63)
* Add readiness probe and fix readme

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

* typos and fixes

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>

---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
2023-09-05 22:24:27 +03:00
Brian Downs
0dd234b2d5 Add Addon Feature (#61)
* resolve conflicts and other changes

Signed-off-by: Brian Downs <brian.downs@gmail.com>

* updates

Signed-off-by: Brian Downs <brian.downs@gmail.com>

* fix remaining conflict

Signed-off-by: Brian Downs <brian.downs@gmail.com>

* add back cluster and service cidr

Signed-off-by: Brian Downs <brian.downs@gmail.com>

---------

Signed-off-by: Brian Downs <brian.downs@gmail.com>
2023-08-31 01:34:35 +03:00
Johnatas
986216f9cd Feat improve k3kcli os support (#62)
* Allowing multiple OSs

* add docs for windows

* Improve docs

* add manifests

* fix readme

* Update README.md

* Improve macos docs

* Change base build to go v1.20.7 and add freebsd
2023-08-31 01:27:48 +03:00
186 changed files with 48996 additions and 2973 deletions

3
.cr.yaml Normal file
View File

@@ -0,0 +1,3 @@
release-name-template: chart-{{ .Version }}
make-release-latest: false
skip-existing: true

View File

@@ -1,137 +0,0 @@
---
kind: pipeline
name: amd64
platform:
os: linux
arch: amd64
steps:
- name: build
image: rancher/dapper:v0.5.0
environment:
GITHUB_TOKEN:
from_secret: github_token
commands:
- dapper ci
- echo "${DRONE_TAG}-amd64" | sed -e 's/+/-/g' >.tags
volumes:
- name: docker
path: /var/run/docker.sock
when:
branch:
exclude:
- k3k-chart
- name: package-chart
image: rancher/dapper:v0.5.0
environment:
GITHUB_TOKEN:
from_secret: github_token
commands:
- dapper package-chart
volumes:
- name: docker
path: /var/run/docker.sock
when:
branch:
- k3k-chart
instance:
- drone-publish.rancher.io
- name: release-chart
image: rancher/dapper:v0.5.0
environment:
GITHUB_TOKEN:
from_secret: github_token
commands:
- dapper release-chart
volumes:
- name: docker
path: /var/run/docker.sock
when:
branch:
- k3k-chart
instance:
- drone-publish.rancher.io
- name: github_binary_release
image: ibuildthecloud/github-release:v0.0.1
settings:
api_key:
from_secret: github_token
prerelease: true
checksum:
- sha256
checksum_file: CHECKSUMsum-amd64.txt
checksum_flatten: true
files:
- "bin/*"
when:
instance:
- drone-publish.rancher.io
ref:
- refs/head/master
- refs/tags/*
event:
- tag
branch:
exclude:
- k3k-chart
- name: docker-publish
image: plugins/docker
settings:
dockerfile: package/Dockerfile
password:
from_secret: docker_password
repo: "rancher/k3k"
username:
from_secret: docker_username
when:
instance:
- drone-publish.rancher.io
ref:
- refs/head/master
- refs/tags/*
event:
- tag
branch:
exclude:
- k3k-chart
volumes:
- name: docker
host:
path: /var/run/docker.sock
---
kind: pipeline
type: docker
name: manifest
platform:
os: linux
arch: amd64
steps:
- name: push-runtime-manifest
image: plugins/manifest
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
spec: manifest-runtime.tmpl
when:
event:
- tag
instance:
- drone-publish.rancher.io
ref:
- refs/head/master
- refs/tags/*
branch:
exclude:
- k3k-chart
depends_on:
- amd64

41
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,41 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
<!-- Thanks for helping us to improve K3K! We welcome all bug reports. Please fill out each area of the template so we can better help you. Comments like this will be hidden when you post but you can delete them if you wish. -->
**Environmental Info:**
Host Cluster Version:
<!-- For example K3S v1.32.1+k3s1 or RKE2 v1.31.5+rke2r1 -->
Node(s) CPU architecture, OS, and Version:
<!-- Provide the output from "uname -a" on the node(s) -->
Host Cluster Configuration:
<!-- Provide some basic information on the cluster configuration. For example, "1 servers, 2 agents CNI: Flannel". -->
K3K Cluster Configuration:
<!-- Provide some basic information on the cluster configuration. For example, "3 servers, 2 agents". -->
**Describe the bug:**
<!-- A clear and concise description of what the bug is. -->
**Steps To Reproduce:**
- Created a cluster with `k3k create`:
**Expected behavior:**
<!-- A clear and concise description of what you expected to happen. -->
**Actual behavior:**
<!-- A clear and concise description of what actually happened. -->
**Additional context / logs:**
<!-- Add any other context and/or logs about the problem here. -->
<!-- kubectl logs -n k3k-system -l app.kubernetes.io/instance=k3k -->
<!-- $ kubectl logs -n <cluster-namespace> k3k-<cluster-name>-server-0 -->
<!-- $ kubectl logs -n <cluster-namespace> -l cluster=<cluster-name>,mode=shared # in shared mode -->

37
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Build
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: v2
args: --clean --snapshot
env:
REPO: ${{ github.repository }}
REGISTRY: ""

33
.github/workflows/chart.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Chart
on:
workflow_dispatch:
permissions:
contents: write
jobs:
chart-release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure Git
run: |
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Install Helm
uses: azure/setup-helm@v4
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: Run chart-releaser
uses: helm/chart-releaser-action@v1.6.0
with:
config: .cr.yaml
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

61
.github/workflows/release-delete.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: Release - Delete Draft
on:
workflow_dispatch:
inputs:
tag:
type: string
description: The tag of the release
permissions:
contents: write
packages: write
env:
GH_TOKEN: ${{ github.token }}
jobs:
release-delete:
runs-on: ubuntu-latest
steps:
- name: Check tag
if: inputs.tag == ''
run: echo "::error::Missing tag from input" && exit 1
- name: Checkout code
uses: actions/checkout@v4
- name: Check if release is draft
run: |
CURRENT_TAG=${{ inputs.tag }}
isDraft=$(gh release view ${CURRENT_TAG} --json isDraft --jq ".isDraft")
if [ "$isDraft" = true ]; then
echo "Release ${CURRENT_TAG} is draft"
else
echo "::error::Cannot delete non-draft release" && exit 1
fi
- name: Delete packages from Github Container Registry
run: |
CURRENT_TAG=${{ inputs.tag }}
echo "Deleting packages with tag ${CURRENT_TAG}"
JQ_QUERY=".[] | select(.metadata.container.tags[] == \"${CURRENT_TAG}\")"
for package in k3k k3k-kubelet
do
echo "Deleting ${package} image"
PACKAGE_TO_DELETE=$(gh api /user/packages/container/${package}/versions --jq "${JQ_QUERY}")
echo $PACKAGE_TO_DELETE | jq
PACKAGE_ID=$(echo $PACKAGE_TO_DELETE | jq .id)
echo "Deleting ${PACKAGE_ID}"
gh api --method DELETE /user/packages/container/${package}/versions/${PACKAGE_ID}
done
- name: Delete Github release
run: |
CURRENT_TAG=${{ inputs.tag }}
echo "Deleting release ${CURRENT_TAG}"
gh release delete ${CURRENT_TAG}

90
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,90 @@
name: Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
commit:
type: string
description: Checkout a specific commit
permissions:
contents: write
packages: write
id-token: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Checkout code at the specific commit
if: inputs.commit != ''
run: git checkout ${{ inputs.commit }}
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: "Read secrets"
uses: rancher-eio/read-vault-secrets@main
if: github.repository_owner == 'rancher'
with:
secrets: |
secret/data/github/repo/${{ github.repository }}/dockerhub/${{ github.repository_owner }}/credentials username | DOCKER_USERNAME ;
secret/data/github/repo/${{ github.repository }}/dockerhub/${{ github.repository_owner }}/credentials password | DOCKER_PASSWORD ;
# Manually dispatched workflows (or forks) will use ghcr.io
- name: Setup ghcr.io
if: github.event_name == 'workflow_dispatch' || github.repository_owner != 'rancher'
run: |
echo "REGISTRY=ghcr.io" >> $GITHUB_ENV
echo "DOCKER_USERNAME=${{ github.actor }}" >> $GITHUB_ENV
echo "DOCKER_PASSWORD=${{ github.token }}" >> $GITHUB_ENV
- name: Login to container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.DOCKER_USERNAME }}
password: ${{ env.DOCKER_PASSWORD }}
# If the tag does not exists the workflow was manually triggered.
# That means we are creating temporary nightly builds, with a "fake" local tag
- name: Check release tag
id: release-tag
run: |
CURRENT_TAG=$(git describe --tag --always --match="v[0-9]*")
if git show-ref --tags ${CURRENT_TAG} --quiet; then
echo "tag ${CURRENT_TAG} already exists";
else
echo "tag ${CURRENT_TAG} does not exist"
git tag ${CURRENT_TAG}
fi
echo "CURRENT_TAG=${CURRENT_TAG}" >> "$GITHUB_OUTPUT"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: v2
args: --clean
env:
GITHUB_TOKEN: ${{ github.token }}
GORELEASER_CURRENT_TAG: ${{ steps.release-tag.outputs.CURRENT_TAG }}
REGISTRY: ${{ env.REGISTRY }}
REPO: ${{ github.repository }}

302
.github/workflows/test-conformance.yaml vendored Normal file
View File

@@ -0,0 +1,302 @@
name: Conformance Tests
on:
schedule:
- cron: "0 1 * * *"
workflow_dispatch:
inputs:
test:
description: "Run specific test"
type: choice
options:
- conformance
- sig-api-machinery
- sig-apps
- sig-architecture
- sig-auth
- sig-cli
- sig-instrumentation
- sig-network
- sig-node
- sig-scheduling
- sig-storage
permissions:
contents: read
jobs:
conformance:
runs-on: ubuntu-latest
if: inputs.test == '' || inputs.test == 'conformance'
strategy:
fail-fast: false
matrix:
type:
- parallel
- serial
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install helm
uses: azure/setup-helm@v4.3.0
- name: Install hydrophone
run: go install sigs.k8s.io/hydrophone@latest
- name: Install k3d and kubectl
run: |
wget -q -O - https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash
k3d version
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
- name: Setup Kubernetes (k3d)
env:
REPO_NAME: k3k-registry
REPO_PORT: 12345
run: |
echo "127.0.0.1 ${REPO_NAME}" | sudo tee -a /etc/hosts
k3d registry create ${REPO_NAME} --port ${REPO_PORT}
k3d cluster create k3k --servers 3 \
-p "30000-30010:30000-30010@server:0" \
--registry-use k3d-${REPO_NAME}:${REPO_PORT}
kubectl cluster-info
kubectl get nodes
- name: Setup K3k
env:
REPO: k3k-registry:12345
run: |
echo "127.0.0.1 k3k-registry" | sudo tee -a /etc/hosts
make build
make package
make push
# add k3kcli to $PATH
echo "${{ github.workspace }}/bin" >> $GITHUB_PATH
VERSION=$(make version)
k3d image import ${REPO}/k3k:${VERSION} -c k3k --verbose
k3d image import ${REPO}/k3k-kubelet:${VERSION} -c k3k --verbose
make install
echo "Wait for K3k controller to be available"
kubectl wait -n k3k-system pod --for condition=Ready -l "app.kubernetes.io/name=k3k" --timeout=5m
- name: Check k3kcli
run: k3kcli -v
- name: Create virtual cluster
run: |
kubectl create namespace k3k-mycluster
cat <<EOF | kubectl apply -f -
apiVersion: k3k.io/v1alpha1
kind: Cluster
metadata:
name: mycluster
namespace: k3k-mycluster
spec:
servers: 2
mirrorHostNodes: true
tlsSANs:
- "127.0.0.1"
expose:
nodePort:
serverPort: 30001
EOF
echo "Wait for bootstrap secret to be available"
kubectl wait -n k3k-mycluster --for=create secret k3k-mycluster-bootstrap --timeout=5m
k3kcli kubeconfig generate --name mycluster
export KUBECONFIG=${{ github.workspace }}/k3k-mycluster-mycluster-kubeconfig.yaml
kubectl cluster-info
kubectl get nodes
kubectl get pods -A
- name: Run conformance tests (parallel)
if: matrix.type == 'parallel'
run: |
# Run conformance tests in parallel mode (skipping serial)
hydrophone --conformance --parallel 4 --skip='\[Serial\]' \
--kubeconfig ${{ github.workspace }}/k3k-mycluster-mycluster-kubeconfig.yaml \
--output-dir /tmp
- name: Run conformance tests (serial)
if: matrix.type == 'serial'
run: |
# Run serial conformance tests
hydrophone --focus='\[Serial\].*\[Conformance\]' \
--kubeconfig ${{ github.workspace }}/k3k-mycluster-mycluster-kubeconfig.yaml \
--output-dir /tmp
- name: Archive conformance logs
uses: actions/upload-artifact@v4
if: always()
with:
name: conformance-${{ matrix.type }}-logs
path: /tmp/e2e.log
sigs:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
tests:
- name: sig-api-machinery
focus: '\[sig-api-machinery\].*\[Conformance\]'
- name: sig-apps
focus: '\[sig-apps\].*\[Conformance\]'
- name: sig-architecture
focus: '\[sig-architecture\].*\[Conformance\]'
- name: sig-auth
focus: '\[sig-auth\].*\[Conformance\]'
- name: sig-cli
focus: '\[sig-cli\].*\[Conformance\]'
- name: sig-instrumentation
focus: '\[sig-instrumentation\].*\[Conformance\]'
- name: sig-network
focus: '\[sig-network\].*\[Conformance\]'
- name: sig-node
focus: '\[sig-node\].*\[Conformance\]'
- name: sig-scheduling
focus: '\[sig-scheduling\].*\[Conformance\]'
- name: sig-storage
focus: '\[sig-storage\].*\[Conformance\]'
steps:
- name: Validate input and fail fast
if: inputs.test != '' && inputs.test != matrix.tests.name
run: |
echo "Failing this job as it's not the intended target."
exit 1
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install helm
uses: azure/setup-helm@v4.3.0
- name: Install hydrophone
run: go install sigs.k8s.io/hydrophone@latest
- name: Install k3d and kubectl
run: |
wget -q -O - https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash
k3d version
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
- name: Setup Kubernetes (k3d)
env:
REPO_NAME: k3k-registry
REPO_PORT: 12345
run: |
echo "127.0.0.1 ${REPO_NAME}" | sudo tee -a /etc/hosts
k3d registry create ${REPO_NAME} --port ${REPO_PORT}
k3d cluster create k3k --servers 3 \
-p "30000-30010:30000-30010@server:0" \
--registry-use k3d-${REPO_NAME}:${REPO_PORT}
kubectl cluster-info
kubectl get nodes
- name: Setup K3k
env:
REPO: k3k-registry:12345
run: |
echo "127.0.0.1 k3k-registry" | sudo tee -a /etc/hosts
make build
make package
make push
# add k3kcli to $PATH
echo "${{ github.workspace }}/bin" >> $GITHUB_PATH
VERSION=$(make version)
k3d image import ${REPO}/k3k:${VERSION} -c k3k --verbose
k3d image import ${REPO}/k3k-kubelet:${VERSION} -c k3k --verbose
make install
echo "Wait for K3k controller to be available"
kubectl wait -n k3k-system pod --for condition=Ready -l "app.kubernetes.io/name=k3k" --timeout=5m
- name: Check k3kcli
run: k3kcli -v
- name: Create virtual cluster
run: |
kubectl create namespace k3k-mycluster
cat <<EOF | kubectl apply -f -
apiVersion: k3k.io/v1alpha1
kind: Cluster
metadata:
name: mycluster
namespace: k3k-mycluster
spec:
servers: 2
mirrorHostNodes: true
tlsSANs:
- "127.0.0.1"
expose:
nodePort:
serverPort: 30001
EOF
echo "Wait for bootstrap secret to be available"
kubectl wait -n k3k-mycluster --for=create secret k3k-mycluster-bootstrap --timeout=5m
k3kcli kubeconfig generate --name mycluster
export KUBECONFIG=${{ github.workspace }}/k3k-mycluster-mycluster-kubeconfig.yaml
kubectl cluster-info
kubectl get nodes
kubectl get pods -A
- name: Run sigs tests
run: |
FOCUS="${{ matrix.tests.focus }}"
echo "Running with --focus=${FOCUS}"
hydrophone --focus "${FOCUS}" \
--kubeconfig ${{ github.workspace }}/k3k-mycluster-mycluster-kubeconfig.yaml \
--output-dir /tmp
- name: Archive conformance logs
uses: actions/upload-artifact@v4
if: always()
with:
name: ${{ matrix.tests.name }}-logs
path: /tmp/e2e.log

179
.github/workflows/test.yaml vendored Normal file
View File

@@ -0,0 +1,179 @@
name: Tests
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
args: --timeout=5m
version: v2.3.0
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Validate
run: make validate
tests:
runs-on: ubuntu-latest
needs: validate
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Run unit tests
run: make test-unit
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./cover.out
flags: unit
tests-e2e:
runs-on: ubuntu-latest
needs: validate
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install Ginkgo
run: go install github.com/onsi/ginkgo/v2/ginkgo
- name: Build and package
run: |
make build
make package
# add k3kcli to $PATH
echo "${{ github.workspace }}/bin" >> $GITHUB_PATH
- name: Check k3kcli
run: k3kcli -v
- name: Run e2e tests
run: make test-e2e
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./cover.out
flags: e2e
- name: Archive k3s logs
uses: actions/upload-artifact@v4
if: always()
with:
name: e2e-k3s-logs
path: /tmp/k3s.log
- name: Archive k3k logs
uses: actions/upload-artifact@v4
if: always()
with:
name: e2e-k3k-logs
path: /tmp/k3k.log
tests-cli:
runs-on: ubuntu-latest
needs: validate
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install Ginkgo
run: go install github.com/onsi/ginkgo/v2/ginkgo
- name: Set coverage environment
run: |
mkdir ${{ github.workspace }}/covdata
echo "COVERAGE=true" >> $GITHUB_ENV
echo "GOCOVERDIR=${{ github.workspace }}/covdata" >> $GITHUB_ENV
- name: Build and package
run: |
make build
make package
# add k3kcli to $PATH
echo "${{ github.workspace }}/bin" >> $GITHUB_PATH
- name: Check k3kcli
run: k3kcli -v
- name: Run cli tests
run: make test-cli
- name: Convert coverage data
run: go tool covdata textfmt -i=${{ github.workspace }}/covdata -o ${{ github.workspace }}/covdata/cover.out
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ${{ github.workspace }}/covdata/cover.out
flags: cli
- name: Archive k3s logs
uses: actions/upload-artifact@v4
if: always()
with:
name: cli-k3s-logs
path: /tmp/k3s.log
- name: Archive k3k logs
uses: actions/upload-artifact@v4
if: always()
with:
name: cli-k3k-logs
path: /tmp/k3k.log

8
.gitignore vendored
View File

@@ -4,4 +4,10 @@
/dist
*.swp
.idea
.vscode/
__debug*
*-kubeconfig.yaml
.envtest
cover.out
covcounters.**
covmeta.**

27
.golangci.yml Normal file
View File

@@ -0,0 +1,27 @@
version: "2"
linters:
enable:
- misspell
- wsl_v5
formatters:
enable:
- gci
- gofmt
- gofumpt
settings:
gci:
# The default order is `standard > default > custom > blank > dot > alias > localmodule`.
custom-order: true
sections:
- standard
- default
- alias
- localmodule
- dot
- blank
gofmt:
rewrite-rules:
- pattern: 'interface{}'
replacement: 'any'

148
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,148 @@
version: 2
release:
draft: true
replace_existing_draft: true
prerelease: auto
before:
hooks:
- go mod tidy
- go generate ./...
builds:
- id: k3k
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- "amd64"
- "arm64"
- "s390x"
ldflags:
- -w -s # strip debug info and symbol table
- -X "github.com/rancher/k3k/pkg/buildinfo.Version={{ .Tag }}"
- id: k3k-kubelet
main: ./k3k-kubelet
binary: k3k-kubelet
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- "amd64"
- "arm64"
- "s390x"
ldflags:
- -w -s # strip debug info and symbol table
- -X "github.com/rancher/k3k/pkg/buildinfo.Version={{ .Tag }}"
- id: k3kcli
main: ./cli
binary: k3kcli
env:
- CGO_ENABLED=0
goarch:
- "amd64"
- "arm64"
ldflags:
- -w -s # strip debug info and symbol table
- -X "github.com/rancher/k3k/pkg/buildinfo.Version={{ .Tag }}"
archives:
- format: binary
name_template: >-
{{ .Binary }}-{{- .Os }}-{{ .Arch }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
format_overrides:
- goos: windows
format: zip
# For the image_templates we are using the following expression to build images for the correct registry
# {{- if .Env.REGISTRY }}{{ .Env.REGISTRY }}/{{ end }}
#
# REGISTRY= -> rancher/k3k:vX.Y.Z
# REGISTRY=ghcr.io -> ghcr.io/rancher/k3k:latest:vX.Y.Z
#
dockers:
# k3k amd64
- use: buildx
goarch: amd64
ids:
- k3k
- k3kcli
dockerfile: "package/Dockerfile.k3k"
skip_push: false
image_templates:
- "{{- if .Env.REGISTRY }}{{ .Env.REGISTRY }}/{{ end }}{{ .Env.REPO }}:{{ .Tag }}-amd64"
build_flag_templates:
- "--build-arg=BIN_K3K=k3k"
- "--build-arg=BIN_K3KCLI=k3kcli"
- "--pull"
- "--platform=linux/amd64"
# k3k arm64
- use: buildx
goarch: arm64
ids:
- k3k
- k3kcli
dockerfile: "package/Dockerfile.k3k"
skip_push: false
image_templates:
- "{{- if .Env.REGISTRY }}{{ .Env.REGISTRY }}/{{ end }}{{ .Env.REPO }}:{{ .Tag }}-arm64"
build_flag_templates:
- "--build-arg=BIN_K3K=k3k"
- "--build-arg=BIN_K3KCLI=k3kcli"
- "--pull"
- "--platform=linux/arm64"
# k3k-kubelet amd64
- use: buildx
goarch: amd64
ids:
- k3k-kubelet
dockerfile: "package/Dockerfile.k3k-kubelet"
skip_push: false
image_templates:
- "{{- if .Env.REGISTRY }}{{ .Env.REGISTRY }}/{{ end }}{{ .Env.REPO }}-kubelet:{{ .Tag }}-amd64"
build_flag_templates:
- "--build-arg=BIN_K3K_KUBELET=k3k-kubelet"
- "--pull"
- "--platform=linux/amd64"
# k3k-kubelet arm64
- use: buildx
goarch: arm64
ids:
- k3k-kubelet
dockerfile: "package/Dockerfile.k3k-kubelet"
skip_push: false
image_templates:
- "{{- if .Env.REGISTRY }}{{ .Env.REGISTRY }}/{{ end }}{{ .Env.REPO }}-kubelet:{{ .Tag }}-arm64"
build_flag_templates:
- "--build-arg=BIN_K3K_KUBELET=k3k-kubelet"
- "--pull"
- "--platform=linux/arm64"
docker_manifests:
# k3k
- name_template: "{{- if .Env.REGISTRY }}{{ .Env.REGISTRY }}/{{ end }}{{ .Env.REPO }}:{{ .Tag }}"
image_templates:
- "{{- if .Env.REGISTRY }}{{ .Env.REGISTRY }}/{{ end }}{{ .Env.REPO }}:{{ .Tag }}-amd64"
- "{{- if .Env.REGISTRY }}{{ .Env.REGISTRY }}/{{ end }}{{ .Env.REPO }}:{{ .Tag }}-arm64"
# k3k-kubelet arm64
- name_template: "{{- if .Env.REGISTRY }}{{ .Env.REGISTRY }}/{{ end }}{{ .Env.REPO }}-kubelet:{{ .Tag }}"
image_templates:
- "{{- if .Env.REGISTRY }}{{ .Env.REGISTRY }}/{{ end }}{{ .Env.REPO }}-kubelet:{{ .Tag }}-amd64"
- "{{- if .Env.REGISTRY }}{{ .Env.REGISTRY }}/{{ end }}{{ .Env.REPO }}-kubelet:{{ .Tag }}-arm64"
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"

View File

@@ -1,24 +0,0 @@
ARG GOLANG=rancher/hardened-build-base:v1.20.6b2
FROM ${GOLANG}
ARG DAPPER_HOST_ARCH
ENV ARCH $DAPPER_HOST_ARCH
RUN apk -U add \bash git gcc musl-dev docker vim less file curl wget ca-certificates
RUN if [ "${ARCH}" == "amd64" ]; then \
curl -sL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.15.0; \
fi
RUN curl -sL https://github.com/helm/chart-releaser/releases/download/v1.5.0/chart-releaser_1.5.0_linux_${ARCH}.tar.gz | tar -xz cr \
&& mv cr /bin/
ENV GO111MODULE on
ENV DAPPER_ENV REPO TAG DRONE_TAG CROSS GITHUB_TOKEN
ENV DAPPER_SOURCE /go/src/github.com/rancher/k3k/
ENV DAPPER_OUTPUT ./bin ./dist ./deploy
ENV DAPPER_DOCKER_SOCKET true
ENV HOME ${DAPPER_SOURCE}
WORKDIR ${DAPPER_SOURCE}
ENTRYPOINT ["./ops/entry"]
CMD ["ci"]

123
Makefile
View File

@@ -1,15 +1,116 @@
TARGETS := $(shell ls ops)
.dapper:
@echo Downloading dapper
@curl -sL https://releases.rancher.com/dapper/latest/dapper-$$(uname -s)-$$(uname -m) > .dapper.tmp
@@chmod +x .dapper.tmp
@./.dapper.tmp -v
@mv .dapper.tmp .dapper
REPO ?= rancher
COVERAGE ?= false
VERSION ?= $(shell git describe --tags --always --dirty --match="v[0-9]*")
$(TARGETS): .dapper
./.dapper $@
## Dependencies
.DEFAULT_GOAL := default
GOLANGCI_LINT_VERSION := v2.3.0
GINKGO_VERSION ?= v2.21.0
GINKGO_FLAGS ?= -v -r --coverprofile=cover.out --coverpkg=./...
ENVTEST_VERSION ?= v0.0.0-20250505003155-b6c5897febe5
ENVTEST_K8S_VERSION := 1.31.0
CRD_REF_DOCS_VER ?= v0.1.0
.PHONY: $(TARGETS)
GOLANGCI_LINT ?= go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
GINKGO ?= go run github.com/onsi/ginkgo/v2/ginkgo@$(GINKGO_VERSION)
CRD_REF_DOCS := go run github.com/elastic/crd-ref-docs@$(CRD_REF_DOCS_VER)
ENVTEST ?= go run sigs.k8s.io/controller-runtime/tools/setup-envtest@$(ENVTEST_VERSION)
ENVTEST_DIR ?= $(shell pwd)/.envtest
export KUBEBUILDER_ASSETS ?= $(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(ENVTEST_DIR) -p path)
.PHONY: all
all: version generate build package ## Run 'make' or 'make all' to run 'version', 'generate', 'build' and 'package'
.PHONY: version
version: ## Print the current version
@echo $(VERSION)
.PHONY: build
build: ## Build the the K3k binaries (k3k, k3k-kubelet and k3kcli)
@VERSION=$(VERSION) COVERAGE=$(COVERAGE) ./scripts/build
.PHONY: package
package: package-k3k package-k3k-kubelet ## Package the k3k and k3k-kubelet Docker images
.PHONY: package-%
package-%:
docker build -f package/Dockerfile.$* \
-t $(REPO)/$*:$(VERSION) \
-t $(REPO)/$*:latest \
-t $(REPO)/$*:dev .
.PHONY: push
push: push-k3k push-k3k-kubelet ## Push the K3k images to the registry
.PHONY: push-%
push-%:
docker push $(REPO)/$*:$(VERSION)
docker push $(REPO)/$*:latest
docker push $(REPO)/$*:dev
.PHONY: test
test: ## Run all the tests
$(GINKGO) $(GINKGO_FLAGS) --label-filter=$(label-filter)
.PHONY: test-unit
test-unit: ## Run the unit tests (skips the e2e)
$(GINKGO) $(GINKGO_FLAGS) --skip-file=tests/*
.PHONY: test-controller
test-controller: ## Run the controller tests (pkg/controller)
$(GINKGO) $(GINKGO_FLAGS) pkg/controller
.PHONY: test-kubelet-controller
test-kubelet-controller: ## Run the controller tests (pkg/controller)
$(GINKGO) $(GINKGO_FLAGS) k3k-kubelet/controller
.PHONY: test-e2e
test-e2e: ## Run the e2e tests
$(GINKGO) $(GINKGO_FLAGS) --label-filter=e2e tests
.PHONY: test-cli
test-cli: ## Run the cli tests
$(GINKGO) $(GINKGO_FLAGS) --label-filter=cli --flake-attempts=3 tests
.PHONY: generate
generate: ## Generate the CRDs specs
go generate ./...
.PHONY: docs
docs: ## Build the CRDs and CLI docs
$(CRD_REF_DOCS) --config=./docs/crds/config.yaml \
--renderer=markdown \
--source-path=./pkg/apis/k3k.io/v1alpha1 \
--output-path=./docs/crds/crd-docs.md
@go run ./docs/cli/genclidoc.go
.PHONY: lint
lint: ## Find any linting issues in the project
$(GOLANGCI_LINT) run --timeout=5m
.PHONY: fmt
fmt: ## Find any linting issues in the project
$(GOLANGCI_LINT) fmt ./...
.PHONY: validate
validate: generate docs fmt ## Validate the project checking for any dependency or doc mismatch
$(GINKGO) unfocus
go mod tidy
git status --porcelain
git --no-pager diff --exit-code
.PHONY: install
install: ## Install K3k with Helm on the targeted Kubernetes cluster
helm upgrade --install --namespace k3k-system --create-namespace \
--set image.repository=$(REPO)/k3k \
--set image.tag=$(VERSION) \
--set sharedAgent.image.repository=$(REPO)/k3k-kubelet \
--set sharedAgent.image.tag=$(VERSION) \
k3k ./charts/k3k/
.PHONY: help
help: ## Show this help.
@egrep -h '\s##\s' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m %-30s\033[0m %s\n", $$1, $$2}'

193
README.md
View File

@@ -1,58 +1,183 @@
# K3K
# K3k: Kubernetes in Kubernetes
A Kubernetes in Kubernetes tool, k3k provides a way to run multiple embedded isolated k3s clusters on your kubernetes cluster.
[![Experimental](https://img.shields.io/badge/status-experimental-orange.svg)](https://shields.io/)
[![Go Report Card](https://goreportcard.com/badge/github.com/rancher/k3k)](https://goreportcard.com/report/github.com/rancher/k3k)
![Tests](https://github.com/rancher/k3k/actions/workflows/test.yaml/badge.svg)
![Build](https://github.com/rancher/k3k/actions/workflows/build.yml/badge.svg)
## Example
An example on creating a k3k cluster on an RKE2 host using k3kcli
K3k, Kubernetes in Kubernetes, is a tool that empowers you to create and manage isolated K3s clusters within your existing Kubernetes environment. It enables efficient multi-tenancy, streamlined experimentation, and robust resource isolation, minimizing infrastructure costs by allowing you to run multiple lightweight Kubernetes clusters on the same physical host. K3k offers both "shared" mode, optimizing resource utilization, and "virtual" mode, providing complete isolation with dedicated K3s server pods. This allows you to access a full Kubernetes experience without the overhead of managing separate physical resources.
K3k integrates seamlessly with Rancher for simplified management of your embedded clusters.
**Experimental Tool**
This project is still under development and is considered experimental. It may have limitations, bugs, or changes. Please use with caution and report any issues you encounter. We appreciate your feedback as we continue to refine and improve this tool.
## Features and Benefits
- **Resource Isolation:** Ensure workload isolation and prevent resource contention between teams or applications. K3k allows you to define resource limits and quotas for each embedded cluster, guaranteeing that one team's workloads won't impact another's performance.
- **Simplified Multi-Tenancy:** Easily create dedicated Kubernetes environments for different users or projects, simplifying access control and management. Provide each team with their own isolated cluster, complete with its own namespaces, RBAC, and resource quotas, without the complexity of managing multiple physical clusters.
- **Lightweight and Fast:** Leverage the lightweight nature of K3s to spin up and tear down clusters quickly, accelerating development and testing cycles. Spin up a new K3k cluster in seconds, test your application in a clean environment, and tear it down just as quickly, streamlining your CI/CD pipeline.
- **Optimized Resource Utilization (Shared Mode):** Maximize your infrastructure investment by running multiple K3s clusters on the same physical host. K3k's shared mode allows you to efficiently share underlying resources, reducing overhead and minimizing costs.
- **Complete Isolation (Virtual Mode):** For enhanced security and isolation, K3k's virtual mode provides dedicated K3s server pods for each embedded cluster. This ensures complete separation of workloads and eliminates any potential resource contention or security risks.
- **Rancher Integration:** Simplify the management of your K3k clusters with Rancher. Leverage Rancher's intuitive UI and powerful features to monitor, manage, and scale your embedded clusters with ease.
## Installation
This section provides instructions on how to install K3k and the `k3kcli`.
### Prerequisites
* [Helm](https://helm.sh) must be installed to use the charts. Please refer to Helm's [documentation](https://helm.sh/docs) to get started.
* An existing [RKE2](https://docs.rke2.io/install/quickstart) Kubernetes cluster (recommended).
* A configured storage provider with a default storage class.
**Note:** If you do not have a storage provider, you can configure the cluster to use ephemeral or static storage. Please consult the [k3kcli advance usage](./docs/advanced-usage.md#using-the-cli) for instructions on using these options.
### Install the K3k controller
1. Add the K3k Helm repository:
```bash
helm repo add k3k https://rancher.github.io/k3k
helm repo update
```
2. Install the K3k controller:
```bash
helm install --namespace k3k-system --create-namespace k3k k3k/k3k
```
**NOTE:** K3k is currently under development. We recommend using the latest released version when possible.
### Install the `k3kcli`
The `k3kcli` provides a quick and easy way to create K3k clusters and automatically exposes them via a kubeconfig.
To install it, simply download the latest available version for your architecture from the GitHub Releases page.
For example, you can download the Linux amd64 version with:
```
wget -qO k3kcli https://github.com/rancher/k3k/releases/download/v0.3.3/k3kcli-linux-amd64 && \
chmod +x k3kcli && \
sudo mv k3kcli /usr/local/bin
```
You should now be able to run:
```bash
-> % k3kcli --version
k3kcli Version: v0.3.3
```
[![asciicast](https://asciinema.org/a/eYlc3dsL2pfP2B50i3Ea8MJJp.svg)](https://asciinema.org/a/eYlc3dsL2pfP2B50i3Ea8MJJp)
## Usage
K3K consists of a controller and a cli tool, the controller can be deployed via a helm chart and the cli can be downloaded from the releases page.
This section provides examples of how to use the `k3kcli` to manage your K3k clusters.
### Deploy Controller
**K3k operates within the context of your currently configured `kubectl` context.** This means that K3k respects the standard Kubernetes mechanisms for context configuration, including the `--kubeconfig` flag, the `$KUBECONFIG` environment variable, and the default `$HOME/.kube/config` file. Any K3k clusters you create will reside within the Kubernetes cluster that your `kubectl` is currently pointing to.
[Helm](https://helm.sh) must be installed to use the charts. Please refer to
Helm's [documentation](https://helm.sh/docs) to get started.
Once Helm has been set up correctly, add the repo as follows:
### Creating a K3k Cluster
```sh
helm repo add k3k https://rancher.github.io/k3k
To create a new K3k cluster, use the following command:
```bash
k3kcli cluster create mycluster
```
> [!NOTE]
> **Creating a K3k Cluster on a Rancher-Managed Host Cluster**
>
> If your *host* Kubernetes cluster is managed by Rancher (e.g., your kubeconfig's `server` address includes a Rancher URL), use the `--kubeconfig-server` flag when creating your K3k cluster:
>
>```bash
>k3kcli cluster create --kubeconfig-server <host_node_IP_or_load_balancer_IP> mycluster
>```
>
> This ensures the generated kubeconfig connects to the correct endpoint.
When the K3s server is ready, `k3kcli` will generate the necessary kubeconfig file and print instructions on how to use it.
Here's an example of the output:
```bash
INFO[0000] Creating a new cluster [mycluster]
INFO[0000] Extracting Kubeconfig for [mycluster] cluster
INFO[0000] waiting for cluster to be available..
INFO[0073] certificate CN=system:admin,O=system:masters signed by CN=k3s-client-ca@1738746570: notBefore=2025-02-05 09:09:30 +0000 UTC notAfter=2026-02-05 09:10:42 +0000 UTC
INFO[0073] You can start using the cluster with:
export KUBECONFIG=/my/current/directory/mycluster-kubeconfig.yaml
kubectl cluster-info
```
If you had already added this repo earlier, run `helm repo update` to retrieve
the latest versions of the packages. You can then run `helm search repo
k3k --devel` to see the charts.
After exporting the generated kubeconfig, you should be able to reach your Kubernetes cluster:
To install the k3k chart:
```sh
helm install my-k3k k3k/k3k --devel
```bash
export KUBECONFIG=/my/current/directory/mycluster-kubeconfig.yaml
kubectl get nodes
kubectl get pods -A
```
To uninstall the chart:
You can also directly create a Cluster resource in some namespace, to create a K3k cluster:
```sh
helm delete my-k3k
```bash
kubectl apply -f - <<EOF
apiVersion: k3k.io/v1alpha1
kind: Cluster
metadata:
name: mycluster
namespace: k3k-mycluster
EOF
```
**NOTE: Since k3k is still under development, the chart is marked as a development chart, this means that you need to add the `--devel` flag to install it.**
and use the `k3kcli` to retrieve the kubeconfig:
### Create a new cluster
To create a new cluster you need to install and run the cli or create a cluster object, to install the cli:
```sh
wget https://github.com/rancher/k3k/releases/download/v0.0.0-alpha6/k3kcli
chmod +x k3kcli
sudo cp k3kcli /usr/local/bin
```bash
k3kcli kubeconfig generate --namespace k3k-mycluster --name mycluster
```
To create a new cluster you can use:
```sh
k3k cluster create --name example-cluster --token test
### Deleting a K3k Cluster
To delete a K3k cluster, use the following command:
```bash
k3kcli cluster delete mycluster
```
## Architecture
For a detailed explanation of the K3k architecture, please refer to the [Architecture documentation](./docs/architecture.md).
## Advanced Usage
For more in-depth examples and information on advanced K3k usage, including details on shared vs. virtual modes, resource management, and other configuration options, please see the [Advanced Usage documentation](./docs/advanced-usage.md).
## Development
If you're interested in building K3k from source or contributing to the project, please refer to the [Development documentation](./docs/development.md).
## License
Copyright (c) 2014-2025 [SUSE](http://rancher.com/)
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

View File

@@ -2,5 +2,5 @@ apiVersion: v2
name: k3k
description: A Helm chart for K3K
type: application
version: 0.1.0-r1
appVersion: 0.0.0-alpha6
version: 0.3.4-rc3
appVersion: v0.3.4-rc3

View File

@@ -1,93 +0,0 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: clusters.k3k.io
spec:
group: k3k.io
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
name:
type: string
version:
type: string
servers:
type: integer
agents:
type: integer
token:
type: string
clusterCIDR:
type: string
serviceCIDR:
type: string
clusterDNS:
type: string
serverArgs:
type: array
items:
type: string
agentArgs:
type: array
items:
type: string
tlsSANs:
type: array
items:
type: string
persistence:
type: object
properties:
type:
type: string
default: "ephermal"
storageClassName:
type: string
storageRequestSize:
type: string
expose:
type: object
properties:
ingress:
type: object
properties:
enabled:
type: boolean
ingressClassName:
type: string
loadbalancer:
type: object
properties:
enabled:
type: boolean
nodePort:
type: object
properties:
enabled:
type: boolean
status:
type: object
properties:
overrideClusterCIDR:
type: boolean
clusterCIDR:
type: string
overrideServiceCIDR:
type: boolean
serviceCIDR:
type: string
clusterDNS:
type: string
scope: Cluster
names:
plural: clusters
singular: cluster
kind: Cluster

View File

@@ -0,0 +1,723 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.0
name: clusters.k3k.io
spec:
group: k3k.io
names:
kind: Cluster
listKind: ClusterList
plural: clusters
singular: cluster
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .spec.mode
name: Mode
type: string
- jsonPath: .status.phase
name: Status
type: string
- jsonPath: .status.policyName
name: Policy
type: string
name: v1alpha1
schema:
openAPIV3Schema:
description: |-
Cluster defines a virtual Kubernetes cluster managed by k3k.
It specifies the desired state of a virtual cluster, including version, node configuration, and networking.
k3k uses this to provision and manage these virtual clusters.
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:
default: {}
description: Spec defines the desired state of the Cluster.
properties:
addons:
description: Addons specifies secrets containing raw YAML to deploy
on cluster startup.
items:
description: Addon specifies a Secret containing YAML to be deployed
on cluster startup.
properties:
secretNamespace:
description: SecretNamespace is the namespace of the Secret.
type: string
secretRef:
description: SecretRef is the name of the Secret.
type: string
type: object
type: array
agentArgs:
description: |-
AgentArgs specifies ordered key-value pairs for K3s agent pods.
Example: ["--node-name=my-agent-node"]
items:
type: string
type: array
agentEnvs:
description: AgentEnvs specifies list of environment variables to
set in the agent pod.
items:
description: EnvVar represents an environment variable present in
a Container.
properties:
name:
description: Name of the environment variable. Must be a C_IDENTIFIER.
type: string
value:
description: |-
Variable references $(VAR_NAME) are expanded
using the previously defined environment variables in the container and
any service environment variables. If a variable cannot be resolved,
the reference in the input string will be unchanged. Double $$ are reduced
to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e.
"$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)".
Escaped references will never be expanded, regardless of whether the variable
exists or not.
Defaults to "".
type: string
valueFrom:
description: Source for the environment variable's value. Cannot
be used if value is not empty.
properties:
configMapKeyRef:
description: Selects a key of a ConfigMap.
properties:
key:
description: The key to select.
type: string
name:
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
optional:
description: Specify whether the ConfigMap or its key
must be defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
fieldRef:
description: |-
Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`,
spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.
properties:
apiVersion:
description: Version of the schema the FieldPath is
written in terms of, defaults to "v1".
type: string
fieldPath:
description: Path of the field to select in the specified
API version.
type: string
required:
- fieldPath
type: object
x-kubernetes-map-type: atomic
resourceFieldRef:
description: |-
Selects a resource of the container: only resources limits and requests
(limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported.
properties:
containerName:
description: 'Container name: required for volumes,
optional for env vars'
type: string
divisor:
anyOf:
- type: integer
- type: string
description: Specifies the output format of the exposed
resources, defaults to "1"
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
resource:
description: 'Required: resource to select'
type: string
required:
- resource
type: object
x-kubernetes-map-type: atomic
secretKeyRef:
description: Selects a key of a secret in the pod's namespace
properties:
key:
description: The key of the secret to select from. Must
be a valid secret key.
type: string
name:
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
optional:
description: Specify whether the Secret or its key must
be defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
type: object
required:
- name
type: object
type: array
agents:
default: 0
description: |-
Agents specifies the number of K3s pods to run in agent (worker) mode.
Must be 0 or greater. Defaults to 0.
This field is ignored in "shared" mode.
format: int32
type: integer
x-kubernetes-validations:
- message: invalid value for agents
rule: self >= 0
clusterCIDR:
description: |-
ClusterCIDR is the CIDR range for pod IPs.
Defaults to 10.42.0.0/16 in shared mode and 10.52.0.0/16 in virtual mode.
This field is immutable.
type: string
x-kubernetes-validations:
- message: clusterCIDR is immutable
rule: self == oldSelf
clusterDNS:
description: |-
ClusterDNS is the IP address for the CoreDNS service.
Must be within the ServiceCIDR range. Defaults to 10.43.0.10.
This field is immutable.
type: string
x-kubernetes-validations:
- message: clusterDNS is immutable
rule: self == oldSelf
customCAs:
description: CustomCAs specifies the cert/key pairs for custom CA
certificates.
properties:
enabled:
description: Enabled toggles this feature on or off.
type: boolean
sources:
description: Sources defines the sources for all required custom
CA certificates.
properties:
clientCA:
description: ClientCA specifies the client-ca cert/key pair.
properties:
secretName:
description: |-
SecretName specifies the name of an existing secret to use.
The controller expects specific keys inside based on the credential type:
- For TLS pairs (e.g., ServerCA): 'tls.crt' and 'tls.key'.
- For ServiceAccountTokenKey: 'tls.key'.
type: string
type: object
etcdPeerCA:
description: ETCDPeerCA specifies the etcd-peer-ca cert/key
pair.
properties:
secretName:
description: |-
SecretName specifies the name of an existing secret to use.
The controller expects specific keys inside based on the credential type:
- For TLS pairs (e.g., ServerCA): 'tls.crt' and 'tls.key'.
- For ServiceAccountTokenKey: 'tls.key'.
type: string
type: object
etcdServerCA:
description: ETCDServerCA specifies the etcd-server-ca cert/key
pair.
properties:
secretName:
description: |-
SecretName specifies the name of an existing secret to use.
The controller expects specific keys inside based on the credential type:
- For TLS pairs (e.g., ServerCA): 'tls.crt' and 'tls.key'.
- For ServiceAccountTokenKey: 'tls.key'.
type: string
type: object
requestHeaderCA:
description: RequestHeaderCA specifies the request-header-ca
cert/key pair.
properties:
secretName:
description: |-
SecretName specifies the name of an existing secret to use.
The controller expects specific keys inside based on the credential type:
- For TLS pairs (e.g., ServerCA): 'tls.crt' and 'tls.key'.
- For ServiceAccountTokenKey: 'tls.key'.
type: string
type: object
serverCA:
description: ServerCA specifies the server-ca cert/key pair.
properties:
secretName:
description: |-
SecretName specifies the name of an existing secret to use.
The controller expects specific keys inside based on the credential type:
- For TLS pairs (e.g., ServerCA): 'tls.crt' and 'tls.key'.
- For ServiceAccountTokenKey: 'tls.key'.
type: string
type: object
serviceAccountToken:
description: ServiceAccountToken specifies the service-account-token
key.
properties:
secretName:
description: |-
SecretName specifies the name of an existing secret to use.
The controller expects specific keys inside based on the credential type:
- For TLS pairs (e.g., ServerCA): 'tls.crt' and 'tls.key'.
- For ServiceAccountTokenKey: 'tls.key'.
type: string
type: object
type: object
type: object
expose:
description: |-
Expose specifies options for exposing the API server.
By default, it's only exposed as a ClusterIP.
properties:
ingress:
description: Ingress specifies options for exposing the API server
through an Ingress.
properties:
annotations:
additionalProperties:
type: string
description: Annotations specifies annotations to add to the
Ingress.
type: object
ingressClassName:
description: IngressClassName specifies the IngressClass to
use for the Ingress.
type: string
type: object
loadbalancer:
description: LoadBalancer specifies options for exposing the API
server through a LoadBalancer service.
properties:
etcdPort:
description: |-
ETCDPort is the port on which the ETCD service is exposed when type is LoadBalancer.
If not specified, the default etcd 2379 port will be allocated.
If 0 or negative, the port will not be exposed.
format: int32
type: integer
serverPort:
description: |-
ServerPort is the port on which the K3s server is exposed when type is LoadBalancer.
If not specified, the default https 443 port will be allocated.
If 0 or negative, the port will not be exposed.
format: int32
type: integer
type: object
nodePort:
description: NodePort specifies options for exposing the API server
through NodePort.
properties:
etcdPort:
description: |-
ETCDPort is the port on each node on which the ETCD service is exposed when type is NodePort.
If not specified, a random port between 30000-32767 will be allocated.
If out of range, the port will not be exposed.
format: int32
type: integer
serverPort:
description: |-
ServerPort is the port on each node on which the K3s server is exposed when type is NodePort.
If not specified, a random port between 30000-32767 will be allocated.
If out of range, the port will not be exposed.
format: int32
type: integer
type: object
type: object
mirrorHostNodes:
description: |-
MirrorHostNodes controls whether node objects from the host cluster
are mirrored into the virtual cluster.
type: boolean
mode:
allOf:
- enum:
- shared
- virtual
- enum:
- shared
- virtual
default: shared
description: |-
Mode specifies the cluster provisioning mode: "shared" or "virtual".
Defaults to "shared". This field is immutable.
type: string
x-kubernetes-validations:
- message: mode is immutable
rule: self == oldSelf
nodeSelector:
additionalProperties:
type: string
description: |-
NodeSelector specifies node labels to constrain where server/agent pods are scheduled.
In "shared" mode, this also applies to workloads.
type: object
persistence:
description: |-
Persistence specifies options for persisting etcd data.
Defaults to dynamic persistence, which uses a PersistentVolumeClaim to provide data persistence.
A default StorageClass is required for dynamic persistence.
properties:
storageClassName:
description: |-
StorageClassName is the name of the StorageClass to use for the PVC.
This field is only relevant in "dynamic" mode.
type: string
storageRequestSize:
default: 1G
description: |-
StorageRequestSize is the requested size for the PVC.
This field is only relevant in "dynamic" mode.
type: string
type:
default: dynamic
description: Type specifies the persistence mode.
type: string
type: object
priorityClass:
description: |-
PriorityClass specifies the priorityClassName for server/agent pods.
In "shared" mode, this also applies to workloads.
type: string
serverArgs:
description: |-
ServerArgs specifies ordered key-value pairs for K3s server pods.
Example: ["--tls-san=example.com"]
items:
type: string
type: array
serverEnvs:
description: ServerEnvs specifies list of environment variables to
set in the server pod.
items:
description: EnvVar represents an environment variable present in
a Container.
properties:
name:
description: Name of the environment variable. Must be a C_IDENTIFIER.
type: string
value:
description: |-
Variable references $(VAR_NAME) are expanded
using the previously defined environment variables in the container and
any service environment variables. If a variable cannot be resolved,
the reference in the input string will be unchanged. Double $$ are reduced
to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e.
"$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)".
Escaped references will never be expanded, regardless of whether the variable
exists or not.
Defaults to "".
type: string
valueFrom:
description: Source for the environment variable's value. Cannot
be used if value is not empty.
properties:
configMapKeyRef:
description: Selects a key of a ConfigMap.
properties:
key:
description: The key to select.
type: string
name:
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
optional:
description: Specify whether the ConfigMap or its key
must be defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
fieldRef:
description: |-
Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`,
spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.
properties:
apiVersion:
description: Version of the schema the FieldPath is
written in terms of, defaults to "v1".
type: string
fieldPath:
description: Path of the field to select in the specified
API version.
type: string
required:
- fieldPath
type: object
x-kubernetes-map-type: atomic
resourceFieldRef:
description: |-
Selects a resource of the container: only resources limits and requests
(limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported.
properties:
containerName:
description: 'Container name: required for volumes,
optional for env vars'
type: string
divisor:
anyOf:
- type: integer
- type: string
description: Specifies the output format of the exposed
resources, defaults to "1"
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
resource:
description: 'Required: resource to select'
type: string
required:
- resource
type: object
x-kubernetes-map-type: atomic
secretKeyRef:
description: Selects a key of a secret in the pod's namespace
properties:
key:
description: The key of the secret to select from. Must
be a valid secret key.
type: string
name:
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
optional:
description: Specify whether the Secret or its key must
be defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
type: object
required:
- name
type: object
type: array
serverLimit:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: ServerLimit specifies resource limits for server nodes.
type: object
servers:
default: 1
description: |-
Servers specifies the number of K3s pods to run in server (control plane) mode.
Must be at least 1. Defaults to 1.
format: int32
type: integer
x-kubernetes-validations:
- message: cluster must have at least one server
rule: self >= 1
serviceCIDR:
description: |-
ServiceCIDR is the CIDR range for service IPs.
Defaults to 10.43.0.0/16 in shared mode and 10.53.0.0/16 in virtual mode.
This field is immutable.
type: string
x-kubernetes-validations:
- message: serviceCIDR is immutable
rule: self == oldSelf
tlsSANs:
description: TLSSANs specifies subject alternative names for the K3s
server certificate.
items:
type: string
type: array
tokenSecretRef:
description: |-
TokenSecretRef is a Secret reference containing the token used by worker nodes to join the cluster.
The Secret must have a "token" field in its data.
properties:
name:
description: name is unique within a namespace to reference a
secret resource.
type: string
namespace:
description: namespace defines the space within which the secret
name must be unique.
type: string
type: object
x-kubernetes-map-type: atomic
version:
description: |-
Version is the K3s version to use for the virtual nodes.
It should follow the K3s versioning convention (e.g., v1.28.2-k3s1).
If not specified, the Kubernetes version of the host node will be used.
type: string
workerLimit:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: WorkerLimit specifies resource limits for agent nodes.
type: object
type: object
status:
default: {}
description: Status reflects the observed state of the Cluster.
properties:
clusterCIDR:
description: ClusterCIDR is the CIDR range for pod IPs.
type: string
clusterDNS:
description: ClusterDNS is the IP address for the CoreDNS service.
type: string
conditions:
description: Conditions are the individual conditions for the cluster
set.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
hostVersion:
description: HostVersion is the Kubernetes version of the host node.
type: string
kubeletPort:
description: KubeletPort specefies the port used by k3k-kubelet in
shared mode.
type: integer
phase:
default: Unknown
description: Phase is a high-level summary of the cluster's current
lifecycle state.
enum:
- Pending
- Provisioning
- Ready
- Failed
- Terminating
- Unknown
type: string
policyName:
description: PolicyName specifies the virtual cluster policy name
bound to the virtual cluster.
type: string
serviceCIDR:
description: ServiceCIDR is the CIDR range for service IPs.
type: string
tlsSANs:
description: TLSSANs specifies subject alternative names for the K3s
server certificate.
items:
type: string
type: array
webhookPort:
description: WebhookPort specefies the port used by webhook in k3k-kubelet
in shared mode.
type: integer
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -0,0 +1,310 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.0
name: virtualclusterpolicies.k3k.io
spec:
group: k3k.io
names:
kind: VirtualClusterPolicy
listKind: VirtualClusterPolicyList
plural: virtualclusterpolicies
shortNames:
- vcp
singular: virtualclusterpolicy
scope: Cluster
versions:
- additionalPrinterColumns:
- jsonPath: .spec.allowedMode
name: Mode
type: string
name: v1alpha1
schema:
openAPIV3Schema:
description: |-
VirtualClusterPolicy allows defining common configurations and constraints
for clusters within a clusterpolicy.
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:
default: {}
description: Spec defines the desired state of the VirtualClusterPolicy.
properties:
allowedMode:
default: shared
description: AllowedMode specifies the allowed cluster provisioning
mode. Defaults to "shared".
enum:
- shared
- virtual
type: string
x-kubernetes-validations:
- message: mode is immutable
rule: self == oldSelf
defaultNodeSelector:
additionalProperties:
type: string
description: DefaultNodeSelector specifies the node selector that
applies to all clusters (server + agent) in the target Namespace.
type: object
defaultPriorityClass:
description: DefaultPriorityClass specifies the priorityClassName
applied to all pods of all clusters in the target Namespace.
type: string
disableNetworkPolicy:
description: DisableNetworkPolicy indicates whether to disable the
creation of a default network policy for cluster isolation.
type: boolean
limit:
description: |-
Limit specifies the LimitRange that will be applied to all pods within the VirtualClusterPolicy
to set defaults and constraints (min/max)
properties:
limits:
description: Limits is the list of LimitRangeItem objects that
are enforced.
items:
description: LimitRangeItem defines a min/max usage limit for
any resource that matches on kind.
properties:
default:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: Default resource requirement limit value by
resource name if resource limit is omitted.
type: object
defaultRequest:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: DefaultRequest is the default resource requirement
request value by resource name if resource request is
omitted.
type: object
max:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: Max usage constraints on this kind by resource
name.
type: object
maxLimitRequestRatio:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: MaxLimitRequestRatio if specified, the named
resource must have a request and limit that are both non-zero
where limit divided by request is less than or equal to
the enumerated value; this represents the max burst for
the named resource.
type: object
min:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: Min usage constraints on this kind by resource
name.
type: object
type:
description: Type of resource that this limit applies to.
type: string
required:
- type
type: object
type: array
x-kubernetes-list-type: atomic
required:
- limits
type: object
podSecurityAdmissionLevel:
description: PodSecurityAdmissionLevel specifies the pod security
admission level applied to the pods in the namespace.
enum:
- privileged
- baseline
- restricted
type: string
quota:
description: Quota specifies the resource limits for clusters within
a clusterpolicy.
properties:
hard:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
hard is the set of desired hard limits for each named resource.
More info: https://kubernetes.io/docs/concepts/policy/resource-quotas/
type: object
scopeSelector:
description: |-
scopeSelector is also a collection of filters like scopes that must match each object tracked by a quota
but expressed using ScopeSelectorOperator in combination with possible values.
For a resource to match, both scopes AND scopeSelector (if specified in spec), must be matched.
properties:
matchExpressions:
description: A list of scope selector requirements by scope
of the resources.
items:
description: |-
A scoped-resource selector requirement is a selector that contains values, a scope name, and an operator
that relates the scope name and values.
properties:
operator:
description: |-
Represents a scope's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist.
type: string
scopeName:
description: The name of the scope that the selector
applies to.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- operator
- scopeName
type: object
type: array
x-kubernetes-list-type: atomic
type: object
x-kubernetes-map-type: atomic
scopes:
description: |-
A collection of filters that must match each object tracked by a quota.
If not specified, the quota matches all objects.
items:
description: A ResourceQuotaScope defines a filter that must
match each object tracked by a quota
type: string
type: array
x-kubernetes-list-type: atomic
type: object
type: object
status:
description: Status reflects the observed state of the VirtualClusterPolicy.
properties:
conditions:
description: Conditions are the individual conditions for the cluster
set.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
lastUpdateTime:
description: LastUpdate is the timestamp when the status was last
updated.
type: string
observedGeneration:
description: ObservedGeneration was the generation at the time the
status was updated.
format: int64
type: integer
summary:
description: Summary is a summary of the status.
type: string
type: object
required:
- metadata
- spec
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -4,7 +4,7 @@ metadata:
name: {{ include "k3k.fullname" . }}
labels:
{{- include "k3k.labels" . | nindent 4 }}
namespace: {{ .Values.namespace }}
namespace: {{ .Release.Namespace }}
spec:
replicas: {{ .Values.image.replicaCount }}
selector:
@@ -16,11 +16,36 @@ spec:
{{- include "k3k.selectorLabels" . | nindent 8 }}
spec:
containers:
- image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
- image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
name: {{ .Chart.Name }}
env:
- name: CLUSTER_CIDR
value: {{ .Values.host.clusterCIDR }}
- name: SHARED_AGENT_IMAGE
value: "{{ .Values.sharedAgent.image.repository }}:{{ default .Chart.AppVersion .Values.sharedAgent.image.tag }}"
- name: SHARED_AGENT_PULL_POLICY
value: {{ .Values.sharedAgent.image.pullPolicy }}
- name: K3S_IMAGE
value: {{ .Values.k3sServer.image.repository }}
- name: K3S_IMAGE_PULL_POLICY
value: {{ .Values.k3sServer.image.pullPolicy }}
- name: KUBELET_PORT_RANGE
value: {{ .Values.sharedAgent.kubeletPortRange }}
- name: WEBHOOK_PORT_RANGE
value: {{ .Values.sharedAgent.webhookPortRange }}
- name: CONTROLLER_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
{{- with .Values.extraEnv }}
{{- toYaml . | nindent 10 }}
{{- end }}
ports:
- containerPort: 8080
name: https
protocol: TCP
serviceAccountName: {{ include "k3k.serviceAccountName" . }}
- containerPort: 9443
name: https-webhook
protocol: TCP
serviceAccountName: {{ include "k3k.serviceAccountName" . }}

View File

@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: {{ .Values.namespace }}

View File

@@ -11,4 +11,48 @@ roleRef:
subjects:
- kind: ServiceAccount
name: {{ include "k3k.serviceAccountName" . }}
namespace: {{ .Values.namespace }}
namespace: {{ .Release.Namespace }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: k3k-kubelet-node
rules:
- apiGroups:
- ""
resources:
- "nodes"
- "nodes/proxy"
verbs:
- "get"
- "list"
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: k3k-kubelet-node
roleRef:
kind: ClusterRole
name: k3k-kubelet-node
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: k3k-priorityclass
rules:
- apiGroups:
- "scheduling.k8s.io"
resources:
- "priorityclasses"
verbs:
- "*"
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: k3k-priorityclass
roleRef:
kind: ClusterRole
name: k3k-priorityclass
apiGroup: rbac.authorization.k8s.io

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: k3k-webhook
labels:
{{- include "k3k.labels" . | nindent 4 }}
namespace: {{ .Release.Namespace }}
spec:
ports:
- port: 443
protocol: TCP
name: https-webhook
targetPort: 9443
selector:
{{- include "k3k.selectorLabels" . | nindent 6 }}

View File

@@ -5,5 +5,5 @@ metadata:
name: {{ include "k3k.serviceAccountName" . }}
labels:
{{- include "k3k.labels" . | nindent 4 }}
namespace: {{ .Values.namespace }}
{{- end }}
namespace: {{ .Release.Namespace }}
{{- end }}

View File

@@ -1,19 +1,51 @@
replicaCount: 1
namespace: k3k-system
image:
repository: rancher/k3k
pullPolicy: Always
# Overrides the image tag whose default is the chart appVersion.
tag: "v0.0.0-alpha6"
tag: ""
pullPolicy: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
# extraEnv allows you to specify additional environment variables for the k3k controller deployment.
# This is useful for passing custom configuration or secrets to the controller.
# For example:
# extraEnv:
# - name: MY_CUSTOM_VAR
# value: "my_custom_value"
# - name: ANOTHER_VAR
# valueFrom:
# secretKeyRef:
# name: my-secret
# key: my-key
extraEnv: []
host:
# clusterCIDR specifies the clusterCIDR that will be added to the default networkpolicy, if not set
# the controller will collect the PodCIDRs of all the nodes on the system.
clusterCIDR: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
# configuration related to the shared agent mode in k3k
sharedAgent:
# Specifies the port range that will be used for k3k-kubelet api if mirrorHostNodes is enabled
kubeletPortRange: "50000-51000"
# Specifies the port range that will be used for webhook if mirrorHostNodes is enabled
webhookPortRange: "51001-52000"
image:
repository: "rancher/k3k-kubelet"
tag: ""
pullPolicy: ""
# image registry configuration related to the k3s server
k3sServer:
image:
repository: "rancher/k3s"
pullPolicy: ""

20
cli/cmds/cluster.go Normal file
View File

@@ -0,0 +1,20 @@
package cmds
import (
"github.com/spf13/cobra"
)
func NewClusterCmd(appCtx *AppContext) *cobra.Command {
cmd := &cobra.Command{
Use: "cluster",
Short: "cluster command",
}
cmd.AddCommand(
NewClusterCreateCmd(appCtx),
NewClusterDeleteCmd(appCtx),
NewClusterListCmd(appCtx),
)
return cmd
}

View File

@@ -1,25 +0,0 @@
package cluster
import (
"github.com/rancher/k3k/cli/cmds"
"github.com/urfave/cli"
)
var clusterSubcommands = []cli.Command{
{
Name: "create",
Usage: "Create new cluster",
SkipFlagParsing: false,
SkipArgReorder: true,
Action: createCluster,
Flags: append(cmds.CommonFlags, clusterCreateFlags...),
},
}
func NewClusterCommand() cli.Command {
return cli.Command{
Name: "cluster",
Usage: "cluster command",
Subcommands: clusterSubcommands,
}
}

View File

@@ -1,327 +0,0 @@
package cluster
import (
"context"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/rancher/k3k/cli/cmds"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
"github.com/rancher/k3k/pkg/controller/cluster"
"github.com/rancher/k3k/pkg/controller/util"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
)
var (
Scheme = runtime.NewScheme()
backoff = wait.Backoff{
Steps: 5,
Duration: 3 * time.Second,
Factor: 2,
Jitter: 0.1,
}
)
func init() {
_ = clientgoscheme.AddToScheme(Scheme)
_ = v1alpha1.AddToScheme(Scheme)
}
var (
name string
token string
clusterCIDR string
serviceCIDR string
servers int64
agents int64
serverArgs cli.StringSlice
agentArgs cli.StringSlice
persistenceType string
storageClassName string
version string
clusterCreateFlags = []cli.Flag{
cli.StringFlag{
Name: "name",
Usage: "name of the cluster",
Destination: &name,
},
cli.Int64Flag{
Name: "servers",
Usage: "number of servers",
Destination: &servers,
Value: 1,
},
cli.Int64Flag{
Name: "agents",
Usage: "number of agents",
Destination: &agents,
},
cli.StringFlag{
Name: "token",
Usage: "token of the cluster",
Destination: &token,
},
cli.StringFlag{
Name: "cluster-cidr",
Usage: "cluster CIDR",
Destination: &clusterCIDR,
},
cli.StringFlag{
Name: "service-cidr",
Usage: "service CIDR",
Destination: &serviceCIDR,
},
cli.StringFlag{
Name: "persistence-type",
Usage: "Persistence mode for the nodes (ephermal, static, dynamic)",
Value: cluster.EphermalNodesType,
Destination: &persistenceType,
},
cli.StringFlag{
Name: "storage-class-name",
Usage: "Storage class name for dynamic persistence type",
Destination: &storageClassName,
},
cli.StringSliceFlag{
Name: "server-args",
Usage: "servers extra arguments",
Value: &serverArgs,
},
cli.StringSliceFlag{
Name: "agent-args",
Usage: "agents extra arguments",
Value: &agentArgs,
},
cli.StringFlag{
Name: "version",
Usage: "k3s version",
Destination: &version,
Value: "v1.26.1-k3s1",
},
}
)
func createCluster(clx *cli.Context) error {
ctx := context.Background()
if err := validateCreateFlags(clx); err != nil {
return err
}
restConfig, err := clientcmd.BuildConfigFromFlags("", cmds.Kubeconfig)
if err != nil {
return err
}
ctrlClient, err := client.New(restConfig, client.Options{
Scheme: Scheme,
})
if err != nil {
return err
}
logrus.Infof("Creating a new cluster [%s]", name)
cluster := newCluster(
name,
token,
int32(servers),
int32(agents),
clusterCIDR,
serviceCIDR,
serverArgs,
agentArgs,
)
cluster.Spec.Expose = &v1alpha1.ExposeConfig{
NodePort: &v1alpha1.NodePortConfig{
Enabled: true,
},
}
// add Host IP address as an extra TLS-SAN to expose the k3k cluster
url, err := url.Parse(restConfig.Host)
if err != nil {
return err
}
host := strings.Split(url.Host, ":")
cluster.Spec.TLSSANs = []string{host[0]}
if err := ctrlClient.Create(ctx, cluster); err != nil {
if apierrors.IsAlreadyExists(err) {
logrus.Infof("Cluster [%s] already exists", name)
} else {
return err
}
}
logrus.Infof("Extracting Kubeconfig for [%s] cluster", name)
var kubeconfig []byte
if err := retry.OnError(backoff, apierrors.IsNotFound, func() error {
kubeconfig, err = extractKubeconfig(ctx, ctrlClient, cluster, host[0])
if err != nil {
return err
}
return nil
}); err != nil {
return err
}
pwd, err := os.Getwd()
if err != nil {
return err
}
logrus.Infof(`You can start using the cluster with:
export KUBECONFIG=%s
kubectl cluster-info
`, filepath.Join(pwd, cluster.Name+"-kubeconfig.yaml"))
return os.WriteFile(cluster.Name+"-kubeconfig.yaml", kubeconfig, 0644)
}
func validateCreateFlags(clx *cli.Context) error {
if persistenceType != cluster.EphermalNodesType &&
persistenceType != cluster.DynamicNodesType {
return errors.New("invalid persistence type")
}
if token == "" {
return errors.New("empty cluster token")
}
if name == "" {
return errors.New("empty cluster name")
}
if servers <= 0 {
return errors.New("invalid number of servers")
}
if cmds.Kubeconfig == "" && os.Getenv("KUBECONFIG") == "" {
return errors.New("empty kubeconfig")
}
return nil
}
func newCluster(name, token string, servers, agents int32, clusterCIDR, serviceCIDR string, serverArgs, agentArgs []string) *v1alpha1.Cluster {
return &v1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
TypeMeta: metav1.TypeMeta{
Kind: "Cluster",
APIVersion: "k3k.io/v1alpha1",
},
Spec: v1alpha1.ClusterSpec{
Name: name,
Token: token,
Servers: &servers,
Agents: &agents,
ClusterCIDR: clusterCIDR,
ServiceCIDR: serviceCIDR,
ServerArgs: serverArgs,
AgentArgs: agentArgs,
Version: version,
Persistence: &v1alpha1.PersistenceConfig{
Type: persistenceType,
StorageClassName: storageClassName,
},
},
}
}
func extractKubeconfig(ctx context.Context, client client.Client, cluster *v1alpha1.Cluster, serverIP string) ([]byte, error) {
nn := types.NamespacedName{
Name: cluster.Name + "-kubeconfig",
Namespace: util.ClusterNamespace(cluster),
}
var kubeSecret v1.Secret
if err := client.Get(ctx, nn, &kubeSecret); err != nil {
return nil, err
}
kubeconfig := kubeSecret.Data["kubeconfig.yaml"]
if kubeconfig == nil {
return nil, errors.New("empty kubeconfig")
}
nn = types.NamespacedName{
Name: "k3k-server-service",
Namespace: util.ClusterNamespace(cluster),
}
var k3kService v1.Service
if err := client.Get(ctx, nn, &k3kService); err != nil {
return nil, err
}
if k3kService.Spec.Type == v1.ServiceTypeNodePort {
nodePort := k3kService.Spec.Ports[0].NodePort
restConfig, err := clientcmd.RESTConfigFromKubeConfig(kubeconfig)
if err != nil {
return nil, err
}
hostURL := fmt.Sprintf("https://%s:%d", serverIP, nodePort)
restConfig.Host = hostURL
clientConfig := generateKubeconfigFromRest(restConfig)
b, err := clientcmd.Write(clientConfig)
if err != nil {
return nil, err
}
kubeconfig = b
}
return kubeconfig, nil
}
func generateKubeconfigFromRest(config *rest.Config) clientcmdapi.Config {
clusters := make(map[string]*clientcmdapi.Cluster)
clusters["default-cluster"] = &clientcmdapi.Cluster{
Server: config.Host,
CertificateAuthorityData: config.CAData,
}
contexts := make(map[string]*clientcmdapi.Context)
contexts["default-context"] = &clientcmdapi.Context{
Cluster: "default-cluster",
Namespace: "default",
AuthInfo: "default",
}
authinfos := make(map[string]*clientcmdapi.AuthInfo)
authinfos["default"] = &clientcmdapi.AuthInfo{
ClientCertificateData: config.CertData,
ClientKeyData: config.KeyData,
}
clientConfig := clientcmdapi.Config{
Kind: "Config",
APIVersion: "v1",
Clusters: clusters,
Contexts: contexts,
CurrentContext: "default-context",
AuthInfos: authinfos,
}
return clientConfig
}

View File

@@ -1 +0,0 @@
package cluster

343
cli/cmds/cluster_create.go Normal file
View File

@@ -0,0 +1,343 @@
package cmds
import (
"context"
"errors"
"fmt"
"net/url"
"os"
"strings"
"time"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/util/retry"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
"github.com/rancher/k3k/pkg/controller"
k3kcluster "github.com/rancher/k3k/pkg/controller/cluster"
"github.com/rancher/k3k/pkg/controller/kubeconfig"
)
type CreateConfig struct {
token string
clusterCIDR string
serviceCIDR string
servers int
agents int
serverArgs []string
agentArgs []string
serverEnvs []string
agentEnvs []string
persistenceType string
storageClassName string
storageRequestSize string
version string
mode string
kubeconfigServerHost string
policy string
mirrorHostNodes bool
customCertsPath string
}
func NewClusterCreateCmd(appCtx *AppContext) *cobra.Command {
createConfig := &CreateConfig{}
cmd := &cobra.Command{
Use: "create",
Short: "Create new cluster",
Example: "k3kcli cluster create [command options] NAME",
PreRunE: func(cmd *cobra.Command, args []string) error {
return validateCreateConfig(createConfig)
},
RunE: createAction(appCtx, createConfig),
Args: cobra.ExactArgs(1),
}
CobraFlagNamespace(appCtx, cmd.Flags())
createFlags(cmd, createConfig)
return cmd
}
func createAction(appCtx *AppContext, config *CreateConfig) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client := appCtx.Client
name := args[0]
if name == k3kcluster.ClusterInvalidName {
return errors.New("invalid cluster name")
}
if config.mode == string(v1alpha1.SharedClusterMode) && config.agents != 0 {
return errors.New("invalid flag, --agents flag is only allowed in virtual mode")
}
namespace := appCtx.Namespace(name)
if err := createNamespace(ctx, client, namespace, config.policy); err != nil {
return err
}
if strings.Contains(config.version, "+") {
orig := config.version
config.version = strings.ReplaceAll(config.version, "+", "-")
logrus.Warnf("Invalid K3s docker reference version: '%s'. Using '%s' instead", orig, config.version)
}
if config.token != "" {
logrus.Info("Creating cluster token secret")
obj := k3kcluster.TokenSecretObj(config.token, name, namespace)
if err := client.Create(ctx, &obj); err != nil {
return err
}
}
if config.customCertsPath != "" {
if err := CreateCustomCertsSecrets(ctx, name, namespace, config.customCertsPath, client); err != nil {
return err
}
}
logrus.Infof("Creating cluster [%s] in namespace [%s]", name, namespace)
cluster := newCluster(name, namespace, config)
cluster.Spec.Expose = &v1alpha1.ExposeConfig{
NodePort: &v1alpha1.NodePortConfig{},
}
// add Host IP address as an extra TLS-SAN to expose the k3k cluster
url, err := url.Parse(appCtx.RestConfig.Host)
if err != nil {
return err
}
host := strings.Split(url.Host, ":")
if config.kubeconfigServerHost != "" {
host = []string{config.kubeconfigServerHost}
}
cluster.Spec.TLSSANs = []string{host[0]}
if err := client.Create(ctx, cluster); err != nil {
if apierrors.IsAlreadyExists(err) {
logrus.Infof("Cluster [%s] already exists", name)
} else {
return err
}
}
logrus.Infof("Waiting for cluster to be available..")
if err := waitForCluster(ctx, client, cluster); err != nil {
return fmt.Errorf("failed to wait for cluster to become ready (status: %s): %w", cluster.Status.Phase, err)
}
logrus.Infof("Extracting Kubeconfig for [%s] cluster", name)
// retry every 5s for at most 2m, or 25 times
availableBackoff := wait.Backoff{
Duration: 5 * time.Second,
Cap: 2 * time.Minute,
Steps: 25,
}
cfg := kubeconfig.New()
var kubeconfig *clientcmdapi.Config
if err := retry.OnError(availableBackoff, apierrors.IsNotFound, func() error {
kubeconfig, err = cfg.Generate(ctx, client, cluster, host[0], 0)
return err
}); err != nil {
return err
}
return writeKubeconfigFile(cluster, kubeconfig, "")
}
}
func newCluster(name, namespace string, config *CreateConfig) *v1alpha1.Cluster {
cluster := &v1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
TypeMeta: metav1.TypeMeta{
Kind: "Cluster",
APIVersion: "k3k.io/v1alpha1",
},
Spec: v1alpha1.ClusterSpec{
Servers: ptr.To(int32(config.servers)),
Agents: ptr.To(int32(config.agents)),
ClusterCIDR: config.clusterCIDR,
ServiceCIDR: config.serviceCIDR,
ServerArgs: config.serverArgs,
AgentArgs: config.agentArgs,
ServerEnvs: env(config.serverEnvs),
AgentEnvs: env(config.agentEnvs),
Version: config.version,
Mode: v1alpha1.ClusterMode(config.mode),
Persistence: v1alpha1.PersistenceConfig{
Type: v1alpha1.PersistenceMode(config.persistenceType),
StorageClassName: ptr.To(config.storageClassName),
StorageRequestSize: config.storageRequestSize,
},
MirrorHostNodes: config.mirrorHostNodes,
},
}
if config.storageClassName == "" {
cluster.Spec.Persistence.StorageClassName = nil
}
if config.token != "" {
cluster.Spec.TokenSecretRef = &v1.SecretReference{
Name: k3kcluster.TokenSecretName(name),
Namespace: namespace,
}
}
if config.customCertsPath != "" {
cluster.Spec.CustomCAs = v1alpha1.CustomCAs{
Enabled: true,
Sources: v1alpha1.CredentialSources{
ClientCA: v1alpha1.CredentialSource{
SecretName: controller.SafeConcatNameWithPrefix(cluster.Name, "client-ca"),
},
ServerCA: v1alpha1.CredentialSource{
SecretName: controller.SafeConcatNameWithPrefix(cluster.Name, "server-ca"),
},
ETCDServerCA: v1alpha1.CredentialSource{
SecretName: controller.SafeConcatNameWithPrefix(cluster.Name, "etcd-server-ca"),
},
ETCDPeerCA: v1alpha1.CredentialSource{
SecretName: controller.SafeConcatNameWithPrefix(cluster.Name, "etcd-peer-ca"),
},
RequestHeaderCA: v1alpha1.CredentialSource{
SecretName: controller.SafeConcatNameWithPrefix(cluster.Name, "request-header-ca"),
},
ServiceAccountToken: v1alpha1.CredentialSource{
SecretName: controller.SafeConcatNameWithPrefix(cluster.Name, "service-account-token"),
},
},
}
}
return cluster
}
func env(envSlice []string) []v1.EnvVar {
var envVars []v1.EnvVar
for _, env := range envSlice {
keyValue := strings.Split(env, "=")
if len(keyValue) != 2 {
logrus.Fatalf("incorrect value for environment variable %s", env)
}
envVars = append(envVars, v1.EnvVar{
Name: keyValue[0],
Value: keyValue[1],
})
}
return envVars
}
func waitForCluster(ctx context.Context, k8sClient client.Client, cluster *v1alpha1.Cluster) error {
interval := 5 * time.Second
timeout := 2 * time.Minute
return wait.PollUntilContextTimeout(ctx, interval, timeout, true, func(ctx context.Context) (bool, error) {
key := client.ObjectKeyFromObject(cluster)
if err := k8sClient.Get(ctx, key, cluster); err != nil {
return false, fmt.Errorf("failed to get resource: %w", err)
}
// If resource ready -> stop polling
if cluster.Status.Phase == v1alpha1.ClusterReady {
return true, nil
}
// If resource failed -> stop polling with an error
if cluster.Status.Phase == v1alpha1.ClusterFailed {
return true, fmt.Errorf("cluster creation failed: %s", cluster.Status.Phase)
}
// Condition not met, continue polling.
return false, nil
})
}
func CreateCustomCertsSecrets(ctx context.Context, name, namespace, customCertsPath string, k8sclient client.Client) error {
customCAsMap := map[string]string{
"etcd-peer-ca": "/etcd/peer-ca",
"etcd-server-ca": "/etcd/server-ca",
"server-ca": "/server-ca",
"client-ca": "/client-ca",
"request-header-ca": "/request-header-ca",
"service-account-token": "/service",
}
for certName, fileName := range customCAsMap {
var (
certFilePath, keyFilePath string
cert, key []byte
err error
)
if certName != "service-account-token" {
certFilePath = customCertsPath + fileName + ".crt"
cert, err = os.ReadFile(certFilePath)
if err != nil {
return err
}
}
keyFilePath = customCertsPath + fileName + ".key"
key, err = os.ReadFile(keyFilePath)
if err != nil {
return err
}
certSecret := caCertSecret(certName, name, namespace, cert, key)
if err := k8sclient.Create(ctx, certSecret); err != nil {
return client.IgnoreAlreadyExists(err)
}
}
return nil
}
func caCertSecret(certName, clusterName, clusterNamespace string, cert, key []byte) *v1.Secret {
return &v1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: controller.SafeConcatNameWithPrefix(clusterName, certName),
Namespace: clusterNamespace,
},
Type: v1.SecretTypeTLS,
Data: map[string][]byte{
v1.TLSCertKey: cert,
v1.TLSPrivateKeyKey: key,
},
}
}

View File

@@ -0,0 +1,61 @@
package cmds
import (
"errors"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/resource"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
)
func createFlags(cmd *cobra.Command, cfg *CreateConfig) {
cmd.Flags().IntVar(&cfg.servers, "servers", 1, "number of servers")
cmd.Flags().IntVar(&cfg.agents, "agents", 0, "number of agents")
cmd.Flags().StringVar(&cfg.token, "token", "", "token of the cluster")
cmd.Flags().StringVar(&cfg.clusterCIDR, "cluster-cidr", "", "cluster CIDR")
cmd.Flags().StringVar(&cfg.serviceCIDR, "service-cidr", "", "service CIDR")
cmd.Flags().BoolVar(&cfg.mirrorHostNodes, "mirror-host-nodes", false, "Mirror Host Cluster Nodes")
cmd.Flags().StringVar(&cfg.persistenceType, "persistence-type", string(v1alpha1.DynamicPersistenceMode), "persistence mode for the nodes (dynamic, ephemeral, static)")
cmd.Flags().StringVar(&cfg.storageClassName, "storage-class-name", "", "storage class name for dynamic persistence type")
cmd.Flags().StringVar(&cfg.storageRequestSize, "storage-request-size", "", "storage size for dynamic persistence type")
cmd.Flags().StringSliceVar(&cfg.serverArgs, "server-args", []string{}, "servers extra arguments")
cmd.Flags().StringSliceVar(&cfg.agentArgs, "agent-args", []string{}, "agents extra arguments")
cmd.Flags().StringSliceVar(&cfg.serverEnvs, "server-envs", []string{}, "servers extra Envs")
cmd.Flags().StringSliceVar(&cfg.agentEnvs, "agent-envs", []string{}, "agents extra Envs")
cmd.Flags().StringVar(&cfg.version, "version", "", "k3s version")
cmd.Flags().StringVar(&cfg.mode, "mode", "shared", "k3k mode type (shared, virtual)")
cmd.Flags().StringVar(&cfg.kubeconfigServerHost, "kubeconfig-server", "", "override the kubeconfig server host")
cmd.Flags().StringVar(&cfg.policy, "policy", "", "The policy to create the cluster in")
cmd.Flags().StringVar(&cfg.customCertsPath, "custom-certs", "", "The path for custom certificate directory")
}
func validateCreateConfig(cfg *CreateConfig) error {
if cfg.servers <= 0 {
return errors.New("invalid number of servers")
}
if cfg.persistenceType != "" {
switch v1alpha1.PersistenceMode(cfg.persistenceType) {
case v1alpha1.EphemeralPersistenceMode, v1alpha1.DynamicPersistenceMode:
return nil
default:
return errors.New(`persistence-type should be one of "dynamic", "ephemeral" or "static"`)
}
}
if _, err := resource.ParseQuantity(cfg.storageRequestSize); err != nil {
return errors.New(`invalid storage size, should be a valid resource quantity e.g "10Gi"`)
}
if cfg.mode != "" {
switch cfg.mode {
case string(v1alpha1.VirtualClusterMode), string(v1alpha1.SharedClusterMode):
return nil
default:
return errors.New(`mode should be one of "shared" or "virtual"`)
}
}
return nil
}

115
cli/cmds/cluster_delete.go Normal file
View File

@@ -0,0 +1,115 @@
package cmds
import (
"context"
"errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
k3kcluster "github.com/rancher/k3k/pkg/controller/cluster"
"github.com/rancher/k3k/pkg/controller/cluster/agent"
)
var keepData bool
func NewClusterDeleteCmd(appCtx *AppContext) *cobra.Command {
cmd := &cobra.Command{
Use: "delete",
Short: "Delete an existing cluster",
Example: "k3kcli cluster delete [command options] NAME",
RunE: delete(appCtx),
Args: cobra.ExactArgs(1),
}
CobraFlagNamespace(appCtx, cmd.Flags())
cmd.Flags().BoolVar(&keepData, "keep-data", false, "keeps persistence volumes created for the cluster after deletion")
return cmd
}
func delete(appCtx *AppContext) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client := appCtx.Client
name := args[0]
if name == k3kcluster.ClusterInvalidName {
return errors.New("invalid cluster name")
}
namespace := appCtx.Namespace(name)
logrus.Infof("Deleting [%s] cluster in namespace [%s]", name, namespace)
cluster := v1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
}
// keep bootstrap secrets and tokens if --keep-data flag is passed
if keepData {
// skip removing tokenSecret
if err := RemoveOwnerReferenceFromSecret(ctx, k3kcluster.TokenSecretName(cluster.Name), client, cluster); err != nil {
return err
}
// skip removing webhook secret
if err := RemoveOwnerReferenceFromSecret(ctx, agent.WebhookSecretName(cluster.Name), client, cluster); err != nil {
return err
}
} else {
matchingLabels := ctrlclient.MatchingLabels(map[string]string{"cluster": cluster.Name, "role": "server"})
listOpts := ctrlclient.ListOptions{Namespace: cluster.Namespace}
matchingLabels.ApplyToList(&listOpts)
deleteOpts := &ctrlclient.DeleteAllOfOptions{ListOptions: listOpts}
if err := client.DeleteAllOf(ctx, &v1.PersistentVolumeClaim{}, deleteOpts); err != nil {
return ctrlclient.IgnoreNotFound(err)
}
}
if err := client.Delete(ctx, &cluster); err != nil {
return ctrlclient.IgnoreNotFound(err)
}
return nil
}
}
func RemoveOwnerReferenceFromSecret(ctx context.Context, name string, cl ctrlclient.Client, cluster v1alpha1.Cluster) error {
var secret v1.Secret
key := types.NamespacedName{
Name: name,
Namespace: cluster.Namespace,
}
if err := cl.Get(ctx, key, &secret); err != nil {
if apierrors.IsNotFound(err) {
logrus.Warnf("%s secret is not found", name)
return nil
}
return err
}
if controllerutil.HasControllerReference(&secret) {
if err := controllerutil.RemoveOwnerReference(&cluster, &secret, cl.Scheme()); err != nil {
return err
}
return cl.Update(ctx, &secret)
}
return nil
}

52
cli/cmds/cluster_list.go Normal file
View File

@@ -0,0 +1,52 @@
package cmds
import (
"context"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/types"
"k8s.io/cli-runtime/pkg/printers"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
)
func NewClusterListCmd(appCtx *AppContext) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List all the existing cluster",
Example: "k3kcli cluster list [command options]",
RunE: list(appCtx),
Args: cobra.NoArgs,
}
CobraFlagNamespace(appCtx, cmd.Flags())
return cmd
}
func list(appCtx *AppContext) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client := appCtx.Client
var clusters v1alpha1.ClusterList
if err := client.List(ctx, &clusters, ctrlclient.InNamespace(appCtx.namespace)); err != nil {
return err
}
crd := &apiextensionsv1.CustomResourceDefinition{}
if err := client.Get(ctx, types.NamespacedName{Name: "clusters.k3k.io"}, crd); err != nil {
return err
}
items := toPointerSlice(clusters.Items)
table := createTable(crd, items)
printer := printers.NewTablePrinter(printers.PrintOptions{WithNamespace: true})
return printer.PrintObj(table, cmd.OutOrStdout())
}
}

153
cli/cmds/kubeconfig.go Normal file
View File

@@ -0,0 +1,153 @@
package cmds
import (
"context"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/retry"
apierrors "k8s.io/apimachinery/pkg/api/errors"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
"github.com/rancher/k3k/pkg/controller"
"github.com/rancher/k3k/pkg/controller/certs"
"github.com/rancher/k3k/pkg/controller/kubeconfig"
)
type GenerateKubeconfigConfig struct {
name string
configName string
cn string
org []string
altNames []string
expirationDays int64
kubeconfigServerHost string
}
func NewKubeconfigCmd(appCtx *AppContext) *cobra.Command {
cmd := &cobra.Command{
Use: "kubeconfig",
Short: "Manage kubeconfig for clusters",
}
cmd.AddCommand(
NewKubeconfigGenerateCmd(appCtx),
)
return cmd
}
func NewKubeconfigGenerateCmd(appCtx *AppContext) *cobra.Command {
cfg := &GenerateKubeconfigConfig{}
cmd := &cobra.Command{
Use: "generate",
Short: "Generate kubeconfig for clusters",
RunE: generate(appCtx, cfg),
Args: cobra.NoArgs,
}
CobraFlagNamespace(appCtx, cmd.Flags())
generateKubeconfigFlags(cmd, cfg)
return cmd
}
func generateKubeconfigFlags(cmd *cobra.Command, cfg *GenerateKubeconfigConfig) {
cmd.Flags().StringVar(&cfg.name, "name", "", "cluster name")
cmd.Flags().StringVar(&cfg.configName, "config-name", "", "the name of the generated kubeconfig file")
cmd.Flags().StringVar(&cfg.cn, "cn", controller.AdminCommonName, "Common name (CN) of the generated certificates for the kubeconfig")
cmd.Flags().StringSliceVar(&cfg.org, "org", nil, "Organization name (ORG) of the generated certificates for the kubeconfig")
cmd.Flags().StringSliceVar(&cfg.altNames, "altNames", nil, "altNames of the generated certificates for the kubeconfig")
cmd.Flags().Int64Var(&cfg.expirationDays, "expiration-days", 365, "Expiration date of the certificates used for the kubeconfig")
cmd.Flags().StringVar(&cfg.kubeconfigServerHost, "kubeconfig-server", "", "override the kubeconfig server host")
}
func generate(appCtx *AppContext, cfg *GenerateKubeconfigConfig) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client := appCtx.Client
clusterKey := types.NamespacedName{
Name: cfg.name,
Namespace: appCtx.Namespace(cfg.name),
}
var cluster v1alpha1.Cluster
if err := client.Get(ctx, clusterKey, &cluster); err != nil {
return err
}
url, err := url.Parse(appCtx.RestConfig.Host)
if err != nil {
return err
}
host := strings.Split(url.Host, ":")
if cfg.kubeconfigServerHost != "" {
host = []string{cfg.kubeconfigServerHost}
cfg.altNames = append(cfg.altNames, cfg.kubeconfigServerHost)
}
certAltNames := certs.AddSANs(cfg.altNames)
if len(cfg.org) == 0 {
cfg.org = []string{user.SystemPrivilegedGroup}
}
kubeCfg := kubeconfig.KubeConfig{
CN: cfg.cn,
ORG: cfg.org,
ExpiryDate: time.Hour * 24 * time.Duration(cfg.expirationDays),
AltNames: certAltNames,
}
logrus.Infof("waiting for cluster to be available..")
var kubeconfig *clientcmdapi.Config
if err := retry.OnError(controller.Backoff, apierrors.IsNotFound, func() error {
kubeconfig, err = kubeCfg.Generate(ctx, client, &cluster, host[0], 0)
return err
}); err != nil {
return err
}
return writeKubeconfigFile(&cluster, kubeconfig, cfg.configName)
}
}
func writeKubeconfigFile(cluster *v1alpha1.Cluster, kubeconfig *clientcmdapi.Config, configName string) error {
if configName == "" {
configName = cluster.Namespace + "-" + cluster.Name + "-kubeconfig.yaml"
}
pwd, err := os.Getwd()
if err != nil {
return err
}
logrus.Infof(`You can start using the cluster with:
export KUBECONFIG=%s
kubectl cluster-info
`, filepath.Join(pwd, configName))
kubeconfigData, err := clientcmd.Write(*kubeconfig)
if err != nil {
return err
}
return os.WriteFile(configName, kubeconfigData, 0o644)
}

20
cli/cmds/policy.go Normal file
View File

@@ -0,0 +1,20 @@
package cmds
import (
"github.com/spf13/cobra"
)
func NewPolicyCmd(appCtx *AppContext) *cobra.Command {
cmd := &cobra.Command{
Use: "policy",
Short: "policy command",
}
cmd.AddCommand(
NewPolicyCreateCmd(appCtx),
NewPolicyDeleteCmd(appCtx),
NewPolicyListCmd(appCtx),
)
return cmd
}

109
cli/cmds/policy_create.go Normal file
View File

@@ -0,0 +1,109 @@
package cmds
import (
"context"
"errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
"github.com/rancher/k3k/pkg/controller/policy"
)
type VirtualClusterPolicyCreateConfig struct {
mode string
}
func NewPolicyCreateCmd(appCtx *AppContext) *cobra.Command {
config := &VirtualClusterPolicyCreateConfig{}
cmd := &cobra.Command{
Use: "create",
Short: "Create new policy",
Example: "k3kcli policy create [command options] NAME",
PreRunE: func(cmd *cobra.Command, args []string) error {
switch config.mode {
case string(v1alpha1.VirtualClusterMode), string(v1alpha1.SharedClusterMode):
return nil
default:
return errors.New(`mode should be one of "shared" or "virtual"`)
}
},
RunE: policyCreateAction(appCtx, config),
Args: cobra.ExactArgs(1),
}
cmd.Flags().StringVar(&config.mode, "mode", "shared", "The allowed mode type of the policy")
return cmd
}
func policyCreateAction(appCtx *AppContext, config *VirtualClusterPolicyCreateConfig) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client := appCtx.Client
policyName := args[0]
_, err := createPolicy(ctx, client, v1alpha1.ClusterMode(config.mode), policyName)
return err
}
}
func createNamespace(ctx context.Context, client client.Client, name, policyName string) error {
ns := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name}}
if policyName != "" {
ns.Labels = map[string]string{
policy.PolicyNameLabelKey: policyName,
}
}
if err := client.Get(ctx, types.NamespacedName{Name: name}, ns); err != nil {
if !apierrors.IsNotFound(err) {
return err
}
logrus.Infof(`Creating namespace [%s]`, name)
if err := client.Create(ctx, ns); err != nil {
return err
}
}
return nil
}
func createPolicy(ctx context.Context, client client.Client, mode v1alpha1.ClusterMode, policyName string) (*v1alpha1.VirtualClusterPolicy, error) {
logrus.Infof("Creating policy [%s]", policyName)
policy := &v1alpha1.VirtualClusterPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: policyName,
},
TypeMeta: metav1.TypeMeta{
Kind: "VirtualClusterPolicy",
APIVersion: "k3k.io/v1alpha1",
},
Spec: v1alpha1.VirtualClusterPolicySpec{
AllowedMode: mode,
},
}
if err := client.Create(ctx, policy); err != nil {
if !apierrors.IsAlreadyExists(err) {
return nil, err
}
logrus.Infof("Policy [%s] already exists", policyName)
}
return policy, nil
}

43
cli/cmds/policy_delete.go Normal file
View File

@@ -0,0 +1,43 @@
package cmds
import (
"context"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
)
func NewPolicyDeleteCmd(appCtx *AppContext) *cobra.Command {
return &cobra.Command{
Use: "delete",
Short: "Delete an existing policy",
Example: "k3kcli policy delete [command options] NAME",
RunE: policyDeleteAction(appCtx),
Args: cobra.ExactArgs(1),
}
}
func policyDeleteAction(appCtx *AppContext) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client := appCtx.Client
name := args[0]
policy := &v1alpha1.VirtualClusterPolicy{}
policy.Name = name
if err := client.Delete(ctx, policy); err != nil {
if apierrors.IsNotFound(err) {
logrus.Warnf("Policy not found")
} else {
return err
}
}
return nil
}
}

47
cli/cmds/policy_list.go Normal file
View File

@@ -0,0 +1,47 @@
package cmds
import (
"context"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/types"
"k8s.io/cli-runtime/pkg/printers"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
)
func NewPolicyListCmd(appCtx *AppContext) *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all the existing policies",
Example: "k3kcli policy list [command options]",
RunE: policyList(appCtx),
Args: cobra.NoArgs,
}
}
func policyList(appCtx *AppContext) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client := appCtx.Client
var policies v1alpha1.VirtualClusterPolicyList
if err := client.List(ctx, &policies); err != nil {
return err
}
crd := &apiextensionsv1.CustomResourceDefinition{}
if err := client.Get(ctx, types.NamespacedName{Name: "virtualclusterpolicies.k3k.io"}, crd); err != nil {
return err
}
items := toPointerSlice(policies.Items)
table := createTable(crd, items)
printer := printers.NewTablePrinter(printers.PrintOptions{})
return printer.PrintObj(table, cmd.OutOrStdout())
}
}

View File

@@ -1,42 +1,119 @@
package cmds
import (
"fmt"
"strings"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/controller-runtime/pkg/client"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
"github.com/rancher/k3k/pkg/buildinfo"
)
var (
debug bool
Kubeconfig string
CommonFlags = []cli.Flag{
cli.StringFlag{
Name: "kubeconfig",
EnvVar: "KUBECONFIG",
Usage: "Kubeconfig path",
Destination: &Kubeconfig,
},
}
)
type AppContext struct {
RestConfig *rest.Config
Client client.Client
func NewApp() *cli.App {
app := cli.NewApp()
app.Name = "k3kcli"
app.Usage = "CLI for K3K"
app.Flags = []cli.Flag{
cli.BoolFlag{
Name: "debug",
Usage: "Turn on debug logs",
Destination: &debug,
EnvVar: "K3K_DEBUG",
},
}
app.Before = func(clx *cli.Context) error {
if debug {
logrus.SetLevel(logrus.DebugLevel)
}
return nil
}
return app
// Global flags
Debug bool
Kubeconfig string
namespace string
}
func NewRootCmd() *cobra.Command {
appCtx := &AppContext{}
rootCmd := &cobra.Command{
Use: "k3kcli",
Short: "CLI for K3K",
Version: buildinfo.Version,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
InitializeConfig(cmd)
if appCtx.Debug {
logrus.SetLevel(logrus.DebugLevel)
}
restConfig, err := loadRESTConfig(appCtx.Kubeconfig)
if err != nil {
return err
}
scheme := runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
_ = v1alpha1.AddToScheme(scheme)
_ = apiextensionsv1.AddToScheme(scheme)
ctrlClient, err := client.New(restConfig, client.Options{Scheme: scheme})
if err != nil {
return err
}
appCtx.RestConfig = restConfig
appCtx.Client = ctrlClient
return nil
},
DisableAutoGenTag: true,
}
rootCmd.PersistentFlags().StringVar(&appCtx.Kubeconfig, "kubeconfig", "", "kubeconfig path ($HOME/.kube/config or $KUBECONFIG if set)")
rootCmd.PersistentFlags().BoolVar(&appCtx.Debug, "debug", false, "Turn on debug logs")
rootCmd.AddCommand(
NewClusterCmd(appCtx),
NewPolicyCmd(appCtx),
NewKubeconfigCmd(appCtx),
)
return rootCmd
}
func (ctx *AppContext) Namespace(name string) string {
if ctx.namespace != "" {
return ctx.namespace
}
return "k3k-" + name
}
func loadRESTConfig(kubeconfig string) (*rest.Config, error) {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
configOverrides := &clientcmd.ConfigOverrides{}
if kubeconfig != "" {
loadingRules.ExplicitPath = kubeconfig
}
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
return kubeConfig.ClientConfig()
}
func CobraFlagNamespace(appCtx *AppContext, flag *pflag.FlagSet) {
flag.StringVarP(&appCtx.namespace, "namespace", "n", "", "namespace of the k3k cluster")
}
func InitializeConfig(cmd *cobra.Command) {
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
viper.AutomaticEnv()
// Bind the current command's flags to viper
cmd.Flags().VisitAll(func(f *pflag.Flag) {
// Apply the viper config value to the flag when the flag is not set and viper has a value
if !f.Changed && viper.IsSet(f.Name) {
val := viper.Get(f.Name)
_ = cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val))
}
})
}

104
cli/cmds/table_printer.go Normal file
View File

@@ -0,0 +1,104 @@
package cmds
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/util/jsonpath"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// createTable creates a table to print from the printerColumn defined in the CRD spec, plus the name at the beginning
func createTable[T runtime.Object](crd *apiextensionsv1.CustomResourceDefinition, objs []T) *metav1.Table {
printerColumns := getPrinterColumnsFromCRD(crd)
return &metav1.Table{
TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1", Kind: "Table"},
ColumnDefinitions: convertToTableColumns(printerColumns),
Rows: createTableRows(objs, printerColumns),
}
}
func getPrinterColumnsFromCRD(crd *apiextensionsv1.CustomResourceDefinition) []apiextensionsv1.CustomResourceColumnDefinition {
printerColumns := []apiextensionsv1.CustomResourceColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: "Name of the Resource", JSONPath: ".metadata.name"},
}
for _, version := range crd.Spec.Versions {
if version.Name == "v1alpha1" {
printerColumns = append(printerColumns, version.AdditionalPrinterColumns...)
break
}
}
return printerColumns
}
func convertToTableColumns(printerColumns []apiextensionsv1.CustomResourceColumnDefinition) []metav1.TableColumnDefinition {
var columnDefinitions []metav1.TableColumnDefinition
for _, col := range printerColumns {
columnDefinitions = append(columnDefinitions, metav1.TableColumnDefinition{
Name: col.Name,
Type: col.Type,
Format: col.Format,
Description: col.Description,
Priority: col.Priority,
})
}
return columnDefinitions
}
func createTableRows[T runtime.Object](objs []T, printerColumns []apiextensionsv1.CustomResourceColumnDefinition) []metav1.TableRow {
var rows []metav1.TableRow
for _, obj := range objs {
objMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&obj)
if err != nil {
rows = append(rows, metav1.TableRow{Cells: []any{"<error: " + err.Error() + ">"}})
continue
}
rows = append(rows, metav1.TableRow{
Cells: buildRowCells(objMap, printerColumns),
Object: runtime.RawExtension{Object: obj},
})
}
return rows
}
func buildRowCells(objMap map[string]any, printerColumns []apiextensionsv1.CustomResourceColumnDefinition) []any {
var cells []any
for _, printCol := range printerColumns {
j := jsonpath.New(printCol.Name)
err := j.Parse("{" + printCol.JSONPath + "}")
if err != nil {
cells = append(cells, "<error>")
continue
}
results, err := j.FindResults(objMap)
if err != nil || len(results) == 0 || len(results[0]) == 0 {
cells = append(cells, "<none>")
continue
}
cells = append(cells, results[0][0].Interface())
}
return cells
}
func toPointerSlice[T any](v []T) []*T {
vPtr := make([]*T, len(v))
for i := range v {
vPtr[i] = &v[i]
}
return vPtr
}

View File

@@ -1,28 +1,14 @@
package main
import (
"os"
"github.com/sirupsen/logrus"
"github.com/rancher/k3k/cli/cmds"
"github.com/rancher/k3k/cli/cmds/cluster"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
const (
program = "k3k"
version = "dev"
gitCommit = "HEAD"
)
func main() {
app := cmds.NewApp()
app.Commands = []cli.Command{
cluster.NewClusterCommand(),
}
app.Version = version + " (" + gitCommit + ")"
if err := app.Run(os.Args); err != nil {
app := cmds.NewRootCmd()
if err := app.Execute(); err != nil {
logrus.Fatal(err)
}
}

132
docs/advanced-usage.md Normal file
View File

@@ -0,0 +1,132 @@
# Advanced Usage
This document provides advanced usage information for k3k, including detailed use cases and explanations of the `Cluster` resource fields for customization.
## Customizing the Cluster Resource
The `Cluster` resource provides a variety of fields for customizing the behavior of your virtual clusters. You can check the [CRD documentation](./crds/crd-docs.md) for the full specs.
**Note:** Most of these customization options can also be configured using the `k3kcli` tool. Refer to the [k3kcli](./cli/cli-docs.md) documentation for more details.
This example creates a "shared" mode K3k cluster with:
- 3 servers
- K3s version v1.31.3-k3s1
- Custom network configuration
- Deployment on specific nodes with the `nodeSelector`
- `kube-api` exposed using an ingress
- Custom K3s `serverArgs`
- ETCD data persisted using a `PVC`
```yaml
apiVersion: k3k.io/v1alpha1
kind: Cluster
metadata:
name: my-virtual-cluster
namespace: my-namespace
spec:
mode: shared
version: v1.31.3-k3s1
servers: 3
tlsSANs:
- my-cluster.example.com
nodeSelector:
disktype: ssd
expose:
ingress:
ingressClassName: nginx
annotations:
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
nginx.ingress.kubernetes.io/backend-protocol: "true"
nginx.ingress.kubernetes.io/ssl-redirect: "HTTPS"
clusterCIDR: 10.42.0.0/16
serviceCIDR: 10.43.0.0/16
clusterDNS: 10.43.0.10
serverArgs:
- --tls-san=my-cluster.example.com
persistence:
type: dynamic
storageClassName: local-path
```
### `mode`
The `mode` field specifies the cluster provisioning mode, which can be either `shared` or `virtual`. The default mode is `shared`.
* **`shared` mode:** In this mode, the virtual cluster shares the host cluster's resources and networking. This mode is suitable for lightweight workloads and development environments where isolation is not a primary concern.
* **`virtual` mode:** In this mode, the virtual cluster runs as a separate K3s cluster within the host cluster. This mode provides stronger isolation and is suitable for production workloads or when dedicated resources are required.
### `version`
The `version` field specifies the Kubernetes version to be used by the virtual nodes. If not specified, K3k will use the same K3s version as the host cluster. For example, if the host cluster is running Kubernetes v1.31.3, K3k will use the corresponding K3s version (e.g., `v1.31.3-k3s1`).
### `servers`
The `servers` field specifies the number of K3s server nodes to deploy for the virtual cluster. The default value is 1.
### `agents`
The `agents` field specifies the number of K3s agent nodes to deploy for the virtual cluster. The default value is 0.
**Note:** In `shared` mode, this field is ignored, as the Virtual Kubelet acts as the agent, and there are no K3s worker nodes.
### `nodeSelector`
The `nodeSelector` field allows you to specify a node selector that will be applied to all server/agent pods. In `shared` mode, the node selector will also be applied to the workloads.
### `expose`
The `expose` field contains options for exposing the API server of the virtual cluster. By default, the API server is only exposed as a `ClusterIP`, which is relatively secure but difficult to access from outside the cluster.
You can use the `expose` field to enable exposure via `NodePort`, `LoadBalancer`, or `Ingress`.
In this example we are exposing the Cluster with a Nginx ingress-controller, that has to be configured with the `--enable-ssl-passthrough` flag.
### `clusterCIDR`
The `clusterCIDR` field specifies the CIDR range for the pods of the cluster. The default value is `10.42.0.0/16` in shared mode, and `10.52.0.0/16` in virtual mode.
### `serviceCIDR`
The `serviceCIDR` field specifies the CIDR range for the services in the cluster. The default value is `10.43.0.0/16` in shared mode, and `10.53.0.0/16` in virtual mode.
**Note:** In `shared` mode, the `serviceCIDR` should match the host cluster's `serviceCIDR` to prevent conflicts and in `virtual` mode both `serviceCIDR` and `clusterCIDR` should be different than the host cluster.
### `clusterDNS`
The `clusterDNS` field specifies the IP address for the CoreDNS service. It needs to be in the range provided by `serviceCIDR`. The default value is `10.43.0.10`.
### `serverArgs`
The `serverArgs` field allows you to specify additional arguments to be passed to the K3s server pods.
## Using the cli
You can check the [k3kcli documentation](./cli/cli-docs.md) for the full specs.
### No storage provider:
* Ephemeral Storage:
```bash
k3kcli cluster create --persistence-type ephemeral my-cluster
```
*Important Notes:*
* Using `--persistence-type ephemeral` will result in data loss if the nodes are restarted.
* It is highly recommended to use `--persistence-type dynamic` with a configured storage class.

140
docs/architecture.md Normal file
View File

@@ -0,0 +1,140 @@
# Architecture
Virtual Clusters are isolated Kubernetes clusters provisioned on a physical cluster. K3k leverages [K3s](https://k3s.io/) as the control plane of the Kubernetes cluster because of its lightweight footprint.
K3k provides two modes of deploying virtual clusters: the "shared" mode (default), and "virtual".
## Shared Mode
The default `shared` mode uses a K3s server as control plane with an [agentless servers configuration](https://docs.k3s.io/advanced#running-agentless-servers-experimental). With this option enabled, the servers do not run the kubelet, container runtime, or CNI. The server uses a [Virtual Kubelet](https://virtual-kubelet.io/) provider implementation specific to K3k, which schedules the workloads and other eventually needed resources on the host cluster. This K3k Virtual Kubelet provider handles the reflection of resources and workload execution within the shared host cluster environment.
![Shared Mode](./images/architecture/shared-mode.png)
### Networking and Storage
Because of this shared infrastructure, the CNI will be the same one configured in the host cluster. To provide the needed isolation, K3k will leverage Network Policies.
The same goes for the available storage, so the Storage Classes and Volumes are those of the host cluster.
### Resource Sharing and Limits
In shared mode, K3k leverages Kubernetes ResourceQuotas and LimitRanges to manage resource sharing and enforce limits. Since all virtual cluster workloads run within the same namespace on the host cluster, ResourceQuotas are applied to this namespace to limit the total resources consumed by a virtual cluster. LimitRanges are used to set default resource requests and limits for pods, ensuring that workloads have reasonable resource allocations even if they don't explicitly specify them.
Each pod in a virtual cluster is assigned a unique name that incorporates the pod name, namespace, and cluster name. This prevents naming collisions in the shared host cluster namespace.
It's important to understand that ResourceQuotas are applied at the namespace level. This means that all pods within a virtual cluster share the same quota. While this provides overall limits for the virtual cluster, it also means that resource allocation is dynamic. If one workload isn't using its full resource allocation, other workloads within the *same* virtual cluster can utilize those resources, even if they belong to different deployments or services.
This dynamic sharing can be both a benefit and a challenge. It allows for efficient resource utilization, but it can also lead to unpredictable performance if workloads have varying resource demands. Furthermore, this approach makes it difficult to guarantee strict resource isolation between workloads within the same virtual cluster.
GPU resource sharing is an area of ongoing investigation. K3k is actively exploring potential solutions in this area.
### Isolation and Security
Isolation between virtual clusters in shared mode relies heavily on Kubernetes Network Policies. Network Policies define rules that control the network traffic allowed to and from pods. K3k configures Network Policies to ensure that pods in one virtual cluster cannot communicate with pods in other virtual clusters or with pods in the host cluster itself, providing a strong foundation for network isolation.
While Network Policies offer robust isolation capabilities, it's important to understand their characteristics:
* **CNI Integration:** Network Policies integrate seamlessly with supported CNI plugins. K3k leverages this integration to enforce network isolation.
* **Granular Control:** Network Policies provide granular control over network traffic, allowing for fine-tuned security policies.
* **Scalability:** Network Policies scale well with the number of virtual clusters and applications, ensuring consistent isolation as the environment grows.
K3k also utilizes Kubernetes Pod Security Admission (PSA) to enforce security policies within virtual clusters based on Pod Security Standards (PSS). PSS define different levels of security for pods, restricting what actions pods can perform. By configuring PSA to enforce a specific PSS level (e.g., `baseline` or `restricted`) for a virtual cluster, K3k ensures that pods adhere to established security best practices and prevents them from using privileged features or performing potentially dangerous operations.
Key aspects of PSA integration include:
* **Namespace-Level Enforcement:** PSA configuration is applied at the namespace level, providing a consistent security posture for all pods within the virtual cluster.
* **Standardized Profiles:** PSS offers a set of predefined security profiles aligned with industry best practices, simplifying security configuration and ensuring a baseline level of security.
The shared mode architecture is designed with security in mind. K3k employs multiple layers of security controls, including Network Policies and PSA, to protect virtual clusters and the host cluster. While the shared namespace model requires careful configuration and management, these controls provide a robust security foundation for running workloads in a multi-tenant environment. K3k continuously evaluates and enhances its security mechanisms to address evolving threats and ensure the highest level of protection for its users.
## Virtual Mode
The `virtual` mode in K3k deploys fully functional K3s clusters (including both server and agent components) as virtual clusters. These K3s clusters run as pods within the host cluster. Each virtual cluster has its own dedicated K3s server and one or more K3s agents acting as worker nodes. This approach provides strong isolation, as each virtual cluster operates independently with its own control plane and worker nodes. While these virtual clusters run as pods on the host cluster, they function as complete and separate Kubernetes environments.
![Virtual Mode](./images/architecture/virtual-mode.png)
### Networking and Storage
Virtual clusters in `virtual` mode each have their own independent networking configuration managed by their respective K3s servers. Each virtual cluster runs its own CNI plugin, configured within its K3s server, providing complete network isolation from other virtual clusters and the host cluster. While the virtual cluster networks ultimately operate on top of the host cluster's network infrastructure, the networking configuration and traffic management are entirely separate.
### Resource Sharing and Limits
Resource sharing in `virtual` mode is managed by applying resource limits to the pods that make up the virtual cluster (both the K3s server pod and the K3s agent pods). Each pod is assigned a specific amount of CPU, memory, and other resources. The workloads running *within* the virtual cluster then utilize these allocated resources. This means that the virtual cluster as a whole has a defined resource pool determined by the limits on its constituent pods.
This approach provides a clear and direct way to control the resources available to each virtual cluster. However, it requires careful resource planning to ensure that each virtual cluster has sufficient capacity for its workloads.
### Isolation and Security
The `virtual` mode offers strong isolation due to the dedicated K3s clusters deployed for each virtual cluster. Because each virtual cluster runs its own separate control plane and worker nodes, workloads are effectively isolated from each other and from the host cluster. This architecture minimizes the risk of one virtual cluster impacting others or the host cluster.
Security in `virtual` mode benefits from the inherent isolation provided by the separate K3s clusters. However, standard Kubernetes security best practices still apply, and K3k emphasizes a layered security approach. While the K3s server pods often run with elevated privileges (due to the nature of their function, requiring access to system resources), K3k recommends minimizing these privileges whenever possible and adhering to the principle of least privilege. This can be achieved by carefully configuring the necessary capabilities instead of relying on full `privileged` mode. Further information on K3s security best practices can be found in the official K3s documentation: [https://docs.k3s.io/security](https://docs.k3s.io/security) (This link provides general security guidance, including discussions of capabilities and other relevant topics).
Currently security in virtual mode has a risk of privilege escalation as the server pods run with elevated privileges (due to the nature of their function, requiring access to system resources).
## K3k Components
K3k consists of two main components:
* **Controller:** The K3k controller is a core component that runs on the host cluster. It watches for `Cluster` custom resources (CRs) and manages the lifecycle of virtual clusters. When a new `Cluster` CR is created, the controller provisions the necessary resources, including namespaces, K3s server and agent pods, and network configurations, to create the virtual cluster.
* **CLI:** The K3k CLI provides a command-line interface for interacting with K3k. It allows users to easily create, manage, and access virtual clusters. The CLI simplifies common tasks such as creating `Cluster` CRs, retrieving kubeconfigs for accessing virtual clusters, and performing other management operations.
## VirtualClusterPolicy
K3k introduces the VirtualClusterPolicy Custom Resource, a way to set up and apply common configurations and how your virtual clusters operate within the K3k environment.
The primary goal of VCPs is to allow administrators to centrally manage and apply consistent policies. This reduces repetitive configuration, helps meet organizational standards, and enhances the security and operational consistency of virtual clusters managed by K3k.
A VirtualClusterPolicy is bound to one or more Kubernetes Namespaces. Once bound, the rules defined in the VCP apply to all K3k virtual clusters that are running or get created in that Namespace. This allows for flexible policy application, meaning different Namespaces can use their own unique VCPs, while others can share a single VCP for a consistent setup.
Common use cases for administrators leveraging VirtualClusterPolicy include:
- Defining the operational mode (like "shared" or "virtual") for virtual clusters.
- Setting up resource quotas and limit ranges to effectively manage how much resources virtual clusters and their workloads can use.
- Enforcing security standards, for example, by configuring Pod Security Admission (PSA) labels for Namespaces.
The K3k controller actively monitors VirtualClusterPolicy resources and the corresponding Namespace bindings. When a VCP is applied or updated, the controller ensures that the defined configurations are enforced on the relevant virtual clusters and their associated resources within the targeted Namespaces.
For a deep dive into what VirtualClusterPolicy can do, along with more examples, check out the [VirtualClusterPolicy Concepts](./virtualclusterpolicy.md) page. For a full list of all the spec fields, see the [API Reference for VirtualClusterPolicy](./crds/crd-docs.md#virtualclusterpolicy).
## Comparison and Trade-offs
K3k offers two distinct modes for deploying virtual clusters: `shared` and `virtual`. Each mode has its own strengths and weaknesses, and the best choice depends on the specific needs and priorities of the user. Here's a comparison to help you make an informed decision:
| Feature | Shared Mode | Virtual Mode |
|---|---|---|
| **Architecture** | Agentless K3s server with Virtual Kubelet | Full K3s cluster (server and agents) as pods |
| **Isolation** | Network Policies | Dedicated control plane and worker nodes |
| **Resource Sharing** | Dynamic, namespace-level ResourceQuotas | Resource limits on virtual cluster pods |
| **Networking** | Host cluster's CNI | Virtual cluster's own CNI |
| **Storage** | Host cluster's storage | *Under development* |
| **Security** | Pod Security Admission (PSA), Network Policies | Inherent isolation, PSA, Network Policies, secure host configuration |
| **Performance** | Smaller footprint, more efficient due to running directly on the host | Higher overhead due to running full K3s clusters |
**Trade-offs:**
* **Isolation vs. Overhead:** The `shared` mode has lower overhead but weaker isolation, while the `virtual` mode provides stronger isolation but potentially higher overhead due to running full K3s clusters.
* **Resource Sharing:** The `shared` mode offers dynamic resource sharing within a namespace, which can be efficient but less predictable. The `virtual` mode provides dedicated resources to each virtual cluster, offering more control but requiring careful planning.
**Choosing the right mode:**
* **Choose `shared` mode if:**
* You prioritize low overhead and resource efficiency.
* You need a simple setup and don't require strong isolation between virtual clusters.
* Your workloads don't have strict performance requirements.
* Your workloads needs host capacities (GPU)
* **Choose `virtual` mode if:**
* You prioritize strong isolation.
* You need dedicated resources and predictable performance for your virtual clusters.
Ultimately, the best choice depends on your specific requirements and priorities. Consider the trade-offs carefully and choose the mode that best aligns with your needs.

31
docs/cli/genclidoc.go Normal file
View File

@@ -0,0 +1,31 @@
package main
import (
"fmt"
"os"
"path"
"github.com/spf13/cobra/doc"
"github.com/rancher/k3k/cli/cmds"
)
func main() {
// Instantiate the CLI application
k3kcli := cmds.NewRootCmd()
wd, err := os.Getwd()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
outputDir := path.Join(wd, "docs/cli")
if err := doc.GenMarkdownTree(k3kcli, outputDir); err != nil {
fmt.Println("Error generating documentation:", err)
os.Exit(1)
}
fmt.Println("Documentation generated at " + outputDir)
}

18
docs/cli/k3kcli.md Normal file
View File

@@ -0,0 +1,18 @@
## k3kcli
CLI for K3K
### Options
```
--debug Turn on debug logs
-h, --help help for k3kcli
--kubeconfig string kubeconfig path ($HOME/.kube/config or $KUBECONFIG if set)
```
### SEE ALSO
* [k3kcli cluster](k3kcli_cluster.md) - cluster command
* [k3kcli kubeconfig](k3kcli_kubeconfig.md) - Manage kubeconfig for clusters
* [k3kcli policy](k3kcli_policy.md) - policy command

View File

@@ -0,0 +1,24 @@
## k3kcli cluster
cluster command
### Options
```
-h, --help help for cluster
```
### Options inherited from parent commands
```
--debug Turn on debug logs
--kubeconfig string kubeconfig path ($HOME/.kube/config or $KUBECONFIG if set)
```
### SEE ALSO
* [k3kcli](k3kcli.md) - CLI for K3K
* [k3kcli cluster create](k3kcli_cluster_create.md) - Create new cluster
* [k3kcli cluster delete](k3kcli_cluster_delete.md) - Delete an existing cluster
* [k3kcli cluster list](k3kcli_cluster_list.md) - List all the existing cluster

View File

@@ -0,0 +1,50 @@
## k3kcli cluster create
Create new cluster
```
k3kcli cluster create [flags]
```
### Examples
```
k3kcli cluster create [command options] NAME
```
### Options
```
--agent-args strings agents extra arguments
--agent-envs strings agents extra Envs
--agents int number of agents
--cluster-cidr string cluster CIDR
--custom-certs string The path for custom certificate directory
-h, --help help for create
--kubeconfig-server string override the kubeconfig server host
--mirror-host-nodes Mirror Host Cluster Nodes
--mode string k3k mode type (shared, virtual) (default "shared")
-n, --namespace string namespace of the k3k cluster
--persistence-type string persistence mode for the nodes (dynamic, ephemeral, static) (default "dynamic")
--policy string The policy to create the cluster in
--server-args strings servers extra arguments
--server-envs strings servers extra Envs
--servers int number of servers (default 1)
--service-cidr string service CIDR
--storage-class-name string storage class name for dynamic persistence type
--storage-request-size string storage size for dynamic persistence type
--token string token of the cluster
--version string k3s version
```
### Options inherited from parent commands
```
--debug Turn on debug logs
--kubeconfig string kubeconfig path ($HOME/.kube/config or $KUBECONFIG if set)
```
### SEE ALSO
* [k3kcli cluster](k3kcli_cluster.md) - cluster command

View File

@@ -0,0 +1,33 @@
## k3kcli cluster delete
Delete an existing cluster
```
k3kcli cluster delete [flags]
```
### Examples
```
k3kcli cluster delete [command options] NAME
```
### Options
```
-h, --help help for delete
--keep-data keeps persistence volumes created for the cluster after deletion
-n, --namespace string namespace of the k3k cluster
```
### Options inherited from parent commands
```
--debug Turn on debug logs
--kubeconfig string kubeconfig path ($HOME/.kube/config or $KUBECONFIG if set)
```
### SEE ALSO
* [k3kcli cluster](k3kcli_cluster.md) - cluster command

View File

@@ -0,0 +1,32 @@
## k3kcli cluster list
List all the existing cluster
```
k3kcli cluster list [flags]
```
### Examples
```
k3kcli cluster list [command options]
```
### Options
```
-h, --help help for list
-n, --namespace string namespace of the k3k cluster
```
### Options inherited from parent commands
```
--debug Turn on debug logs
--kubeconfig string kubeconfig path ($HOME/.kube/config or $KUBECONFIG if set)
```
### SEE ALSO
* [k3kcli cluster](k3kcli_cluster.md) - cluster command

View File

@@ -0,0 +1,22 @@
## k3kcli kubeconfig
Manage kubeconfig for clusters
### Options
```
-h, --help help for kubeconfig
```
### Options inherited from parent commands
```
--debug Turn on debug logs
--kubeconfig string kubeconfig path ($HOME/.kube/config or $KUBECONFIG if set)
```
### SEE ALSO
* [k3kcli](k3kcli.md) - CLI for K3K
* [k3kcli kubeconfig generate](k3kcli_kubeconfig_generate.md) - Generate kubeconfig for clusters

View File

@@ -0,0 +1,33 @@
## k3kcli kubeconfig generate
Generate kubeconfig for clusters
```
k3kcli kubeconfig generate [flags]
```
### Options
```
--altNames strings altNames of the generated certificates for the kubeconfig
--cn string Common name (CN) of the generated certificates for the kubeconfig (default "system:admin")
--config-name string the name of the generated kubeconfig file
--expiration-days int Expiration date of the certificates used for the kubeconfig (default 365)
-h, --help help for generate
--kubeconfig-server string override the kubeconfig server host
--name string cluster name
-n, --namespace string namespace of the k3k cluster
--org strings Organization name (ORG) of the generated certificates for the kubeconfig
```
### Options inherited from parent commands
```
--debug Turn on debug logs
--kubeconfig string kubeconfig path ($HOME/.kube/config or $KUBECONFIG if set)
```
### SEE ALSO
* [k3kcli kubeconfig](k3kcli_kubeconfig.md) - Manage kubeconfig for clusters

24
docs/cli/k3kcli_policy.md Normal file
View File

@@ -0,0 +1,24 @@
## k3kcli policy
policy command
### Options
```
-h, --help help for policy
```
### Options inherited from parent commands
```
--debug Turn on debug logs
--kubeconfig string kubeconfig path ($HOME/.kube/config or $KUBECONFIG if set)
```
### SEE ALSO
* [k3kcli](k3kcli.md) - CLI for K3K
* [k3kcli policy create](k3kcli_policy_create.md) - Create new policy
* [k3kcli policy delete](k3kcli_policy_delete.md) - Delete an existing policy
* [k3kcli policy list](k3kcli_policy_list.md) - List all the existing policies

View File

@@ -0,0 +1,32 @@
## k3kcli policy create
Create new policy
```
k3kcli policy create [flags]
```
### Examples
```
k3kcli policy create [command options] NAME
```
### Options
```
-h, --help help for create
--mode string The allowed mode type of the policy (default "shared")
```
### Options inherited from parent commands
```
--debug Turn on debug logs
--kubeconfig string kubeconfig path ($HOME/.kube/config or $KUBECONFIG if set)
```
### SEE ALSO
* [k3kcli policy](k3kcli_policy.md) - policy command

View File

@@ -0,0 +1,31 @@
## k3kcli policy delete
Delete an existing policy
```
k3kcli policy delete [flags]
```
### Examples
```
k3kcli policy delete [command options] NAME
```
### Options
```
-h, --help help for delete
```
### Options inherited from parent commands
```
--debug Turn on debug logs
--kubeconfig string kubeconfig path ($HOME/.kube/config or $KUBECONFIG if set)
```
### SEE ALSO
* [k3kcli policy](k3kcli_policy.md) - policy command

View File

@@ -0,0 +1,31 @@
## k3kcli policy list
List all the existing policies
```
k3kcli policy list [flags]
```
### Examples
```
k3kcli policy list [command options]
```
### Options
```
-h, --help help for list
```
### Options inherited from parent commands
```
--debug Turn on debug logs
--kubeconfig string kubeconfig path ($HOME/.kube/config or $KUBECONFIG if set)
```
### SEE ALSO
* [k3kcli policy](k3kcli_policy.md) - policy command

9
docs/crds/config.yaml Normal file
View File

@@ -0,0 +1,9 @@
processor:
# RE2 regular expressions describing type fields that should be excluded from the generated documentation.
ignoreFields:
- "status$"
- "TypeMeta$"
render:
# Version of Kubernetes to use when generating links to Kubernetes API documentation.
kubernetesVersion: "1.31"

372
docs/crds/crd-docs.md Normal file
View File

@@ -0,0 +1,372 @@
# API Reference
## Packages
- [k3k.io/v1alpha1](#k3kiov1alpha1)
## k3k.io/v1alpha1
### Resource Types
- [Cluster](#cluster)
- [ClusterList](#clusterlist)
- [VirtualClusterPolicy](#virtualclusterpolicy)
- [VirtualClusterPolicyList](#virtualclusterpolicylist)
#### Addon
Addon specifies a Secret containing YAML to be deployed on cluster startup.
_Appears in:_
- [ClusterSpec](#clusterspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `secretNamespace` _string_ | SecretNamespace is the namespace of the Secret. | | |
| `secretRef` _string_ | SecretRef is the name of the Secret. | | |
#### Cluster
Cluster defines a virtual Kubernetes cluster managed by k3k.
It specifies the desired state of a virtual cluster, including version, node configuration, and networking.
k3k uses this to provision and manage these virtual clusters.
_Appears in:_
- [ClusterList](#clusterlist)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `apiVersion` _string_ | `k3k.io/v1alpha1` | | |
| `kind` _string_ | `Cluster` | | |
| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
| `spec` _[ClusterSpec](#clusterspec)_ | Spec defines the desired state of the Cluster. | \{ \} | |
#### ClusterList
ClusterList is a list of Cluster resources.
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `apiVersion` _string_ | `k3k.io/v1alpha1` | | |
| `kind` _string_ | `ClusterList` | | |
| `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
| `items` _[Cluster](#cluster) array_ | | | |
#### ClusterMode
_Underlying type:_ _string_
ClusterMode is the possible provisioning mode of a Cluster.
_Validation:_
- Enum: [shared virtual]
_Appears in:_
- [ClusterSpec](#clusterspec)
- [VirtualClusterPolicySpec](#virtualclusterpolicyspec)
#### ClusterPhase
_Underlying type:_ _string_
ClusterPhase is a high-level summary of the cluster's current lifecycle state.
_Appears in:_
- [ClusterStatus](#clusterstatus)
#### ClusterSpec
ClusterSpec defines the desired state of a virtual Kubernetes cluster.
_Appears in:_
- [Cluster](#cluster)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `version` _string_ | Version is the K3s version to use for the virtual nodes.<br />It should follow the K3s versioning convention (e.g., v1.28.2-k3s1).<br />If not specified, the Kubernetes version of the host node will be used. | | |
| `mode` _[ClusterMode](#clustermode)_ | Mode specifies the cluster provisioning mode: "shared" or "virtual".<br />Defaults to "shared". This field is immutable. | shared | Enum: [shared virtual] <br /> |
| `servers` _integer_ | Servers specifies the number of K3s pods to run in server (control plane) mode.<br />Must be at least 1. Defaults to 1. | 1 | |
| `agents` _integer_ | Agents specifies the number of K3s pods to run in agent (worker) mode.<br />Must be 0 or greater. Defaults to 0.<br />This field is ignored in "shared" mode. | 0 | |
| `clusterCIDR` _string_ | ClusterCIDR is the CIDR range for pod IPs.<br />Defaults to 10.42.0.0/16 in shared mode and 10.52.0.0/16 in virtual mode.<br />This field is immutable. | | |
| `serviceCIDR` _string_ | ServiceCIDR is the CIDR range for service IPs.<br />Defaults to 10.43.0.0/16 in shared mode and 10.53.0.0/16 in virtual mode.<br />This field is immutable. | | |
| `clusterDNS` _string_ | ClusterDNS is the IP address for the CoreDNS service.<br />Must be within the ServiceCIDR range. Defaults to 10.43.0.10.<br />This field is immutable. | | |
| `persistence` _[PersistenceConfig](#persistenceconfig)_ | Persistence specifies options for persisting etcd data.<br />Defaults to dynamic persistence, which uses a PersistentVolumeClaim to provide data persistence.<br />A default StorageClass is required for dynamic persistence. | | |
| `expose` _[ExposeConfig](#exposeconfig)_ | Expose specifies options for exposing the API server.<br />By default, it's only exposed as a ClusterIP. | | |
| `nodeSelector` _object (keys:string, values:string)_ | NodeSelector specifies node labels to constrain where server/agent pods are scheduled.<br />In "shared" mode, this also applies to workloads. | | |
| `priorityClass` _string_ | PriorityClass specifies the priorityClassName for server/agent pods.<br />In "shared" mode, this also applies to workloads. | | |
| `tokenSecretRef` _[SecretReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#secretreference-v1-core)_ | TokenSecretRef is a Secret reference containing the token used by worker nodes to join the cluster.<br />The Secret must have a "token" field in its data. | | |
| `tlsSANs` _string array_ | TLSSANs specifies subject alternative names for the K3s server certificate. | | |
| `serverArgs` _string array_ | ServerArgs specifies ordered key-value pairs for K3s server pods.<br />Example: ["--tls-san=example.com"] | | |
| `agentArgs` _string array_ | AgentArgs specifies ordered key-value pairs for K3s agent pods.<br />Example: ["--node-name=my-agent-node"] | | |
| `serverEnvs` _[EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#envvar-v1-core) array_ | ServerEnvs specifies list of environment variables to set in the server pod. | | |
| `agentEnvs` _[EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#envvar-v1-core) array_ | AgentEnvs specifies list of environment variables to set in the agent pod. | | |
| `addons` _[Addon](#addon) array_ | Addons specifies secrets containing raw YAML to deploy on cluster startup. | | |
| `serverLimit` _[ResourceList](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#resourcelist-v1-core)_ | ServerLimit specifies resource limits for server nodes. | | |
| `workerLimit` _[ResourceList](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#resourcelist-v1-core)_ | WorkerLimit specifies resource limits for agent nodes. | | |
| `mirrorHostNodes` _boolean_ | MirrorHostNodes controls whether node objects from the host cluster<br />are mirrored into the virtual cluster. | | |
| `customCAs` _[CustomCAs](#customcas)_ | CustomCAs specifies the cert/key pairs for custom CA certificates. | | |
#### CredentialSource
CredentialSource defines where to get a credential from.
It can represent either a TLS key pair or a single private key.
_Appears in:_
- [CredentialSources](#credentialsources)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `secretName` _string_ | SecretName specifies the name of an existing secret to use.<br />The controller expects specific keys inside based on the credential type:<br />- For TLS pairs (e.g., ServerCA): 'tls.crt' and 'tls.key'.<br />- For ServiceAccountTokenKey: 'tls.key'. | | |
#### CredentialSources
CredentialSources lists all the required credentials, including both
TLS key pairs and single signing keys.
_Appears in:_
- [CustomCAs](#customcas)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `serverCA` _[CredentialSource](#credentialsource)_ | ServerCA specifies the server-ca cert/key pair. | | |
| `clientCA` _[CredentialSource](#credentialsource)_ | ClientCA specifies the client-ca cert/key pair. | | |
| `requestHeaderCA` _[CredentialSource](#credentialsource)_ | RequestHeaderCA specifies the request-header-ca cert/key pair. | | |
| `etcdServerCA` _[CredentialSource](#credentialsource)_ | ETCDServerCA specifies the etcd-server-ca cert/key pair. | | |
| `etcdPeerCA` _[CredentialSource](#credentialsource)_ | ETCDPeerCA specifies the etcd-peer-ca cert/key pair. | | |
| `serviceAccountToken` _[CredentialSource](#credentialsource)_ | ServiceAccountToken specifies the service-account-token key. | | |
#### CustomCAs
CustomCAs specifies the cert/key pairs for custom CA certificates.
_Appears in:_
- [ClusterSpec](#clusterspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `enabled` _boolean_ | Enabled toggles this feature on or off. | | |
| `sources` _[CredentialSources](#credentialsources)_ | Sources defines the sources for all required custom CA certificates. | | |
#### ExposeConfig
ExposeConfig specifies options for exposing the API server.
_Appears in:_
- [ClusterSpec](#clusterspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `ingress` _[IngressConfig](#ingressconfig)_ | Ingress specifies options for exposing the API server through an Ingress. | | |
| `loadbalancer` _[LoadBalancerConfig](#loadbalancerconfig)_ | LoadBalancer specifies options for exposing the API server through a LoadBalancer service. | | |
| `nodePort` _[NodePortConfig](#nodeportconfig)_ | NodePort specifies options for exposing the API server through NodePort. | | |
#### IngressConfig
IngressConfig specifies options for exposing the API server through an Ingress.
_Appears in:_
- [ExposeConfig](#exposeconfig)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `annotations` _object (keys:string, values:string)_ | Annotations specifies annotations to add to the Ingress. | | |
| `ingressClassName` _string_ | IngressClassName specifies the IngressClass to use for the Ingress. | | |
#### LoadBalancerConfig
LoadBalancerConfig specifies options for exposing the API server through a LoadBalancer service.
_Appears in:_
- [ExposeConfig](#exposeconfig)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `serverPort` _integer_ | ServerPort is the port on which the K3s server is exposed when type is LoadBalancer.<br />If not specified, the default https 443 port will be allocated.<br />If 0 or negative, the port will not be exposed. | | |
| `etcdPort` _integer_ | ETCDPort is the port on which the ETCD service is exposed when type is LoadBalancer.<br />If not specified, the default etcd 2379 port will be allocated.<br />If 0 or negative, the port will not be exposed. | | |
#### NodePortConfig
NodePortConfig specifies options for exposing the API server through NodePort.
_Appears in:_
- [ExposeConfig](#exposeconfig)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `serverPort` _integer_ | ServerPort is the port on each node on which the K3s server is exposed when type is NodePort.<br />If not specified, a random port between 30000-32767 will be allocated.<br />If out of range, the port will not be exposed. | | |
| `etcdPort` _integer_ | ETCDPort is the port on each node on which the ETCD service is exposed when type is NodePort.<br />If not specified, a random port between 30000-32767 will be allocated.<br />If out of range, the port will not be exposed. | | |
#### PersistenceConfig
PersistenceConfig specifies options for persisting etcd data.
_Appears in:_
- [ClusterSpec](#clusterspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `type` _[PersistenceMode](#persistencemode)_ | Type specifies the persistence mode. | dynamic | |
| `storageClassName` _string_ | StorageClassName is the name of the StorageClass to use for the PVC.<br />This field is only relevant in "dynamic" mode. | | |
| `storageRequestSize` _string_ | StorageRequestSize is the requested size for the PVC.<br />This field is only relevant in "dynamic" mode. | 1G | |
#### PersistenceMode
_Underlying type:_ _string_
PersistenceMode is the storage mode of a Cluster.
_Appears in:_
- [PersistenceConfig](#persistenceconfig)
#### PodSecurityAdmissionLevel
_Underlying type:_ _string_
PodSecurityAdmissionLevel is the policy level applied to the pods in the namespace.
_Validation:_
- Enum: [privileged baseline restricted]
_Appears in:_
- [VirtualClusterPolicySpec](#virtualclusterpolicyspec)
#### VirtualClusterPolicy
VirtualClusterPolicy allows defining common configurations and constraints
for clusters within a clusterpolicy.
_Appears in:_
- [VirtualClusterPolicyList](#virtualclusterpolicylist)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `apiVersion` _string_ | `k3k.io/v1alpha1` | | |
| `kind` _string_ | `VirtualClusterPolicy` | | |
| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
| `spec` _[VirtualClusterPolicySpec](#virtualclusterpolicyspec)_ | Spec defines the desired state of the VirtualClusterPolicy. | \{ \} | |
#### VirtualClusterPolicyList
VirtualClusterPolicyList is a list of VirtualClusterPolicy resources.
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `apiVersion` _string_ | `k3k.io/v1alpha1` | | |
| `kind` _string_ | `VirtualClusterPolicyList` | | |
| `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
| `items` _[VirtualClusterPolicy](#virtualclusterpolicy) array_ | | | |
#### VirtualClusterPolicySpec
VirtualClusterPolicySpec defines the desired state of a VirtualClusterPolicy.
_Appears in:_
- [VirtualClusterPolicy](#virtualclusterpolicy)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `quota` _[ResourceQuotaSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#resourcequotaspec-v1-core)_ | Quota specifies the resource limits for clusters within a clusterpolicy. | | |
| `limit` _[LimitRangeSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#limitrangespec-v1-core)_ | Limit specifies the LimitRange that will be applied to all pods within the VirtualClusterPolicy<br />to set defaults and constraints (min/max) | | |
| `defaultNodeSelector` _object (keys:string, values:string)_ | DefaultNodeSelector specifies the node selector that applies to all clusters (server + agent) in the target Namespace. | | |
| `defaultPriorityClass` _string_ | DefaultPriorityClass specifies the priorityClassName applied to all pods of all clusters in the target Namespace. | | |
| `allowedMode` _[ClusterMode](#clustermode)_ | AllowedMode specifies the allowed cluster provisioning mode. Defaults to "shared". | shared | Enum: [shared virtual] <br /> |
| `disableNetworkPolicy` _boolean_ | DisableNetworkPolicy indicates whether to disable the creation of a default network policy for cluster isolation. | | |
| `podSecurityAdmissionLevel` _[PodSecurityAdmissionLevel](#podsecurityadmissionlevel)_ | PodSecurityAdmissionLevel specifies the pod security admission level applied to the pods in the namespace. | | Enum: [privileged baseline restricted] <br /> |

155
docs/development.md Normal file
View File

@@ -0,0 +1,155 @@
# Development
## Prerequisites
To start developing K3k you will need:
- Go
- Docker
- Helm
- A running Kubernetes cluster
### TLDR
```shell
#!/bin/bash
set -euo pipefail
# These environment variables configure the image repository and tag.
export REPO=ghcr.io/myuser
export VERSION=dev-$(date -u '+%Y%m%d%H%M')
make
make push
make install
```
### Makefile
To see all the available Make commands you can run `make help`, i.e:
```
-> % make help
all Run 'make' or 'make all' to run 'version', 'generate', 'build' and 'package'
version Print the current version
build Build the the K3k binaries (k3k, k3k-kubelet and k3kcli)
package Package the k3k and k3k-kubelet Docker images
push Push the K3k images to the registry
test Run all the tests
test-unit Run the unit tests (skips the e2e)
test-controller Run the controller tests (pkg/controller)
test-kubelet-controller Run the controller tests (pkg/controller)
test-e2e Run the e2e tests
generate Generate the CRDs specs
docs Build the CRDs and CLI docs
lint Find any linting issues in the project
validate Validate the project checking for any dependency or doc mismatch
install Install K3k with Helm on the targeted Kubernetes cluster
help Show this help.
```
### Build
To build the needed binaries (`k3k`, `k3k-kubelet` and the `k3kcli`) and package the images you can simply run `make`.
By default the `rancher` repository will be used, but you can customize this to your registry with the `REPO` env var:
```
REPO=ghcr.io/userorg make
```
To customize the tag you can also explicitly set the VERSION:
```
VERSION=dev-$(date -u '+%Y%m%d%H%M') make
```
### Push
You will need to push the built images to your registry, and you can use the `make push` command to do this.
### Install
Once you have your images available you can install K3k with the `make install` command. This will use `helm` to install the release.
## Tests
To run the tests you can just run `make test`, or one of the other available "sub-tests" targets (`test-unit`, `test-controller`, `test-e2e`).
We use [Ginkgo](https://onsi.github.io/ginkgo/), and [`envtest`](https://book.kubebuilder.io/reference/envtest) for testing the controllers.
The required binaries for `envtest` are installed with [`setup-envtest`](https://pkg.go.dev/sigs.k8s.io/controller-runtime/tools/setup-envtest), in the `.envtest` folder.
## CRDs and Docs
We are using Kubebuilder and `controller-gen` to build the needed CRDs. To generate the specs you can run `make generate`.
Remember also to update the CRDs documentation running the `make docs` command.
## How to install k3k on k3d
This document provides a guide on how to install k3k on [k3d](https://k3d.io).
### Installing k3d
Since k3d uses docker under the hood, we need to expose the ports on the host that we'll then use for the NodePort in virtual cluster creation.
Create the k3d cluster in the following way:
```bash
k3d cluster create k3k -p "30000-30010:30000-30010@server:0"
```
With this syntax ports from 30000 to 30010 will be exposed on the host.
### Install k3k
Install now k3k as usual:
```bash
helm repo update
helm install --namespace k3k-system --create-namespace k3k k3k/k3k
```
### Create a virtual cluster
Once the k3k controller is up and running, create a namespace where to create our first virtual cluster.
```bash
kubectl create ns k3k-mycluster
```
Create then the virtual cluster exposing through NodePort one of the ports that we set up in the previous step:
```bash
cat <<EOF | kubectl apply -f -
apiVersion: k3k.io/v1alpha1
kind: Cluster
metadata:
name: mycluster
namespace: k3k-mycluster
spec:
expose:
nodePort:
serverPort: 30001
EOF
```
Check when the cluster is ready:
```bash
kubectl get po -n k3k-mycluster
```
Last thing to do is to get the kubeconfig to connect to the virtual cluster we've just created:
```bash
k3kcli kubeconfig generate --name mycluster --namespace k3k-mycluster --kubeconfig-server localhost:30001
```

83
docs/howtos/airgap.md Normal file
View File

@@ -0,0 +1,83 @@
# K3k Air Gap Installation Guide
Applicable K3k modes: `virtual`, `shared`
This guide describes how to deploy **K3k** in an **air-gapped environment**, including the packaging of required images, Helm chart configurations, and cluster creation using a private container registry.
---
## 1. Package Required Container Images
### 1.1: Follow K3s Air Gap Preparation
Begin with the official K3s air gap packaging instructions:
[K3s Air Gap Installation Docs](https://docs.k3s.io/installation/airgap)
### 1.2: Include K3k-Specific Images
In addition to the K3s images, make sure to include the following in your image bundle:
| Image Names | Descriptions |
| --------------------------- | --------------------------------------------------------------- |
| `rancher/k3k:<tag>` | K3k controller image (replace `<tag>` with the desired version) |
| `rancher/k3k-kubelet:<tag>` | K3k agent image for shared mode |
| `rancher/k3s:<tag>` | K3s server/agent image for virtual clusters |
Load these images into your internal (air-gapped) registry.
---
## 2. Configure Helm Chart for Air Gap installation
Update the `values.yaml` file in the K3k Helm chart with air gap settings:
```yaml
image:
repository: rancher/k3k
tag: "" # Specify the version tag
pullPolicy: "" # Optional: "IfNotPresent", "Always", etc.
sharedAgent:
image:
repository: rancher/k3k-kubelet
tag: "" # Specify the version tag
pullPolicy: "" # Optional
k3sServer:
image:
repository: rancher/k3s
pullPolicy: "" # Optional
```
These values enforce the use of internal image repositories for the K3k controller, the agent and the server.
**Note** : All virtual clusters will use automatically those settings.
---
## 3. Enforce Registry in Virtual Clusters
When creating a virtual cluster, use the `--system-default-registry` flag to ensure all system components (e.g., CoreDNS) pull from your internal registry:
```bash
k3kcli cluster create \
--server-args "--system-default-registry=registry.internal.domain" \
my-cluster
```
This flag is passed directly to the K3s server in the virtual cluster, influencing all system workload image pulls.
[K3s Server CLI Reference](https://docs.k3s.io/cli/server#k3s-server-cli-help)
---
## 4. Specify K3s Version for Virtual Clusters
K3k allows specifying the K3s version used in each virtual cluster:
```bash
k3kcli cluster create \
--k3s-version v1.29.4+k3s1 \
my-cluster
```
- If omitted, the **host clusters K3s version** will be used by default, which might not exist if it's not part of the air gap package.

View File

@@ -0,0 +1,79 @@
# How to Choose Between Shared and Virtual Mode
This guide helps you choose the right mode for your virtual cluster: **Shared** or **Virtual**.
If you're unsure, start with **Shared mode** — it's the default and fits most common scenarios.
---
## Shared Mode (default)
**Best for:**
- Developers who want to run workloads quickly without managing Kubernetes internals
- Platform teams that require visibility and control over all workloads
- Users who need access to host-level resources (e.g., GPUs)
In **Shared mode**, the virtual cluster runs its own K3s server but relies on the host to execute workloads. The virtual kubelet syncs resources, enabling lightweight, fast provisioning with support for cluster resource isolation. More details on the [architecture](./../architecture.md#shared-mode).
---
### Use Cases by Persona
#### 👩‍💻 Developer
*"Im building a web app that should be exposed outside the virtual cluster."*
→ Use **Shared mode**. It allows you to [expose](./expose-workloads.md) your application.
#### 👩‍🔬 Data Scientist:
*“I need to run Jupyter notebooks that leverage the cluster's GPU.”*
→ Use **Shared mode**. It gives access to physical devices while keeping overhead low.
#### 🧑‍💼 Platform Admin
*"I want to monitor and secure all tenant workloads from a central location."*
→ Use **Shared mode**. Host-level agents (e.g., observability, policy enforcement) work across all virtual clusters.
#### 🔒 Security Engineer
*"I need to enforce security policies like network policies or runtime scanning across all workloads."*
→ Use **Shared mode**. The platform can enforce policies globally without tenant bypass.
*"I need to test a new admission controller or policy engine."*
→ Use **Shared mode**, if it's scoped to your virtual cluster. You can run tools like Kubewarden without affecting the host.
#### 🔁 CI/CD Engineer
*"I want to spin up disposable virtual clusters per pipeline run, fast and with low resource cost."*
→ Use **Shared mode**. It's quick to provision and ideal for short-lived, namespace-scoped environments.
---
## Virtual Mode
**Best for:**
- Advanced users who need full Kubernetes isolation
- Developers testing experimental or cluster-wide features
- Use cases requiring control over the entire Kubernetes control plane
In **Virtual mode**, the virtual cluster runs its own isolated Kubernetes control plane. It supports different CNIs, and API configurations — ideal for deep experimentation or advanced workloads. More details on the [architecture](./../architecture.md#virtual-mode).
---
### Use Cases by Persona
#### 👩‍💻 Developer
*"I need to test a new Kubernetes feature gate thats disabled in the host cluster."*
→ Use **Virtual mode**. You can configure your own control plane flags and API features.
#### 🧑‍💼 Platform Admin
*"Were testing upgrades across Kubernetes versions, including new API behaviors."*
→ Use Virtual mode. You can run different Kubernetes versions and safely validate upgrade paths.
#### 🌐 Network Engineer
*"Im evaluating a new CNI that needs full control of the clusters networking."*
→ Use **Virtual mode**. You can run a separate CNI stack without affecting the host or other tenants.
#### 🔒 Security Engineer
*"Im testing a new admission controller and policy engine before rolling it out cluster-wide."*
→ Use **Virtual mode**, if you need to test cluster-wide policies, custom admission flow, or advanced extensions with full control.
---
## Still Not Sure?
If you're evaluating more advanced use cases or want a deeper comparison, see the full trade-off breakdown in the [Architecture documentation](../architecture.md).

View File

@@ -0,0 +1,302 @@
# How to: Create a Virtual Cluster
This guide walks through the various ways to create and manage virtual clusters in K3K. We'll cover common use cases using both the **Custom Resource Definitions (CRDs)** and the **K3K CLI**, so you can choose the method that fits your workflow.
> 📘 For full reference:
> - [CRD Reference Documentation](../crds/crd-docs.md)
> - [CLI Reference Documentation](../cli/cli-docs.md)
> - [Full example](../advanced-usage.md)
> [!NOTE]
> 🚧 Some features are currently only available via the CRD interface. CLI support may be added in the future.
---
## Use Case: Create and Expose a Basic Virtual Cluster
### CRD Method
```yaml
apiVersion: k3k.io/v1alpha1
kind: Cluster
metadata:
name: k3kcluster-ingress
spec:
tlsSANs:
- my-cluster.example.com
expose:
ingress:
ingressClassName: nginx
annotations:
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
nginx.ingress.kubernetes.io/ssl-redirect: "HTTPS"
```
This will create a virtual cluster in `shared` mode and expose it via an ingress with the specified hostname.
### CLI Method
*No CLI method available yet*
---
## Use Case: Create a Virtual Cluster with Persistent Storage (**Default**)
### CRD Method
```yaml
apiVersion: k3k.io/v1alpha1
kind: Cluster
metadata:
name: k3kcluster-persistent
spec:
persistence:
type: dynamic
storageClassName: local-path
storageRequestSize: 30Gi
```
This ensures that the virtual cluster stores its state persistently with a 30Gi volume.
If `storageClassName` is not set it will default to the default StorageClass.
If `storageRequestSize` is not set it will request a 1Gi volume by default.
### CLI Method
```sh
k3kcli cluster create \
--persistence-type dynamic \
--storage-class-name local-path \
k3kcluster-persistent
```
> [!NOTE]
> The `k3kcli` does not support configuring the `storageRequestSize` yet.
---
## Use Case: Create a Highly Available Virtual Cluster in `shared` mode
### CRD Method
```yaml
apiVersion: k3k.io/v1alpha1
kind: Cluster
metadata:
name: k3kcluster-ha
spec:
servers: 3
```
This will create a virtual cluster with 3 servers and a default 1Gi volume for persistence.
### CLI Method
```sh
k3kcli cluster create \
--servers 3 \
k3kcluster-ha
```
---
## Use Case: Create a Highly Available Virtual Cluster in `virtual` mode
### CRD Method
```yaml
apiVersion: k3k.io/v1alpha1
kind: Cluster
metadata:
name: k3kcluster-virtual
spec:
mode: virtual
servers: 3
agents: 3
```
This will create a virtual cluster with 3 servers and 3 agents and a default 1Gi volume for persistence.
> [!NOTE]
> Agents only exist for `virtual` mode.
### CLI Method
```sh
k3kcli cluster create \
--agents 3 \
--servers 3 \
--mode virtual \
k3kcluster-virtual
```
---
## Use Case: Create an Ephemeral Virtual Cluster
### CRD Method
```yaml
apiVersion: k3k.io/v1alpha1
kind: Cluster
metadata:
name: k3kcluster-ephemeral
spec:
persistence:
type: ephemeral
```
This will create an ephemeral virtual cluster with no persistence and a single server.
### CLI Method
```sh
k3kcli cluster create \
--persistence-type ephemeral \
k3kcluster-ephemeral
```
---
## Use Case: Create a Virtual Cluster with a Custom Kubernetes Version
### CRD Method
```yaml
apiVersion: k3k.io/v1alpha1
kind: Cluster
metadata:
name: k3kcluster-custom-k8s
spec:
version: "v1.33.1-k3s1"
```
This sets the virtual cluster's Kubernetes version explicitly.
> [!NOTE]
> Only [K3s](https://k3s.io) distributions are supported. You can find compatible versions on the K3s GitHub [release page](https://github.com/k3s-io/k3s/releases).
### CLI Method
```sh
k3kcli cluster create \
--version v1.33.1-k3s1 \
k3kcluster-custom-k8s
```
---
## Use Case: Create a Virtual Cluster with Custom Resource Limits
### CRD Method
```yaml
apiVersion: k3k.io/v1alpha1
kind: Cluster
metadata:
name: k3kcluster-resourced
spec:
mode: virtual
serverLimit:
cpu: "1"
memory: "2Gi"
workerLimit:
cpu: "1"
memory: "2Gi"
```
This configures the CPU and memory limit for the virtual cluster.
### CLI Method
*No CLI method available yet*
---
## Use Case: Create a Virtual Cluster on specific host nodes
### CRD Method
```yaml
apiVersion: k3k.io/v1alpha1
kind: Cluster
metadata:
name: k3kcluster-node-placed
spec:
nodeSelector:
disktype: ssd
```
This places the virtual cluster on nodes with the label `disktype: ssd`.
> [!NOTE]
> In `shared` mode workloads are also scheduled on the selected nodes
### CLI Method
*No CLI method available yet*
---
## Use Case: Create a Virtual Cluster with a Rancher Host Cluster Kubeconfig
When using a `kubeconfig` generated with Rancher, you need to specify with the CLI the desired host for the virtual cluster `kubeconfig`.
By default, `k3kcli` uses the current host `kubeconfig` to determine the target cluster.
### CRD Method
*Not applicable*
### CLI Method
```sh
k3kcli cluster create \
--kubeconfig-server https://abc.xyz \
k3kcluster-host-rancher
```
---
## Use Case: Create a Virtual Cluster Behind an HTTP Proxy
### CRD Method
```yaml
apiVersion: k3k.io/v1alpha1
kind: Cluster
metadata:
name: k3kcluster-http-proxy
spec:
serverEnvs:
- name: HTTP_PROXY
value: "http://abc.xyz"
agentEnvs:
- name: HTTP_PROXY
value: "http://abc.xyz"
```
This configures an HTTP proxy for both servers and agents in the virtual cluster.
> [!NOTE]
> This can be leveraged to pass **any custom environment variables** to the servers and agents — not just proxy settings.
### CLI Method
```sh
k3kcli cluster create \
--server-envs HTTP_PROXY=http://abc.xyz \
--agent-envs HTTP_PROXY=http://abc.xyz \
k3kcluster-http-proxy
```
---
## How to: Connect to a Virtual Cluster
Once the virtual cluster is running, you can connect to it using the CLI:
### CLI Method
```sh
k3kcli kubeconfig generate --namespace k3k-mycluster --name mycluster
export KUBECONFIG=$PWD/mycluster-kubeconfig.yaml
kubectl get nodes
```
This command generates a `kubeconfig` file, which you can use to access your virtual cluster via `kubectl`.

View File

@@ -0,0 +1,52 @@
# How-to: Expose Workloads Outside the Virtual Cluster
This guide explains how to expose workloads running in k3k-managed virtual clusters to external networks. Behavior varies depending on the operating mode of the virtual cluster.
## Virtual Mode
> [!CAUTION]
> **Not Supported**
> In *virtual mode*, direct external exposure of workloads is **not available**.
> This mode is designed for strong isolation and does not expose the virtual cluster's network directly.
## Shared Mode
In *shared mode*, workloads can be exposed to the external network using standard Kubernetes service types or an ingress controller, depending on your requirements.
> [!NOTE]
> *`Services`* are always synced from the virtual cluster to the host cluster following the same principle described [here](../architecture.md#shared-mode) for pods.
### Option 1: Use `NodePort` or `LoadBalancer`
To expose a service such as a web application outside the host cluster:
- **`NodePort`**:
Exposes the service on a static port on each nodes IP.
Access the service at `http://<NodeIP>:<NodePort>`.
- **`LoadBalancer`**:
Provisions an external load balancer (if supported by the environment) and exposes the service via the load balancers IP.
> **Note**
> The `LoadBalancer` IP is currently not reflected back to the virtual cluster service.
> [k3k issue #365](https://github.com/rancher/k3k/issues/365)
### Option 2: Use `ClusterIP` for Internal Communication
If the workload should only be accessible to other services or pods *within* the host cluster:
- Use the `ClusterIP` service type.
This exposes the service on an internal IP, only reachable inside the host cluster.
### Option 3: Use Ingress for HTTP/HTTPS Routing
For more advanced routing (e.g., hostname- or path-based routing), deploy an **Ingress controller** in the virtual cluster, and expose it via `NodePort` or `LoadBalancer`.
This allows you to:
- Define Ingress resources in the virtual cluster.
- Route external traffic to services within the virtual cluster.
>**Note**
> Support for using the host cluster's Ingress controller from a virtual cluster is being tracked in
> [k3k issue #356](https://github.com/rancher/k3k/issues/356)

View File

@@ -0,0 +1,147 @@
# Troubleshooting
This guide walks through common troubleshooting steps for working with K3K virtual clusters.
---
## `too many open files` error
The `k3k-kubelet` or `k3kcluster-server-` run into the following issue:
```sh
E0604 13:14:53.369369 1 leaderelection.go:336] error initially creating leader election record: Post "https://k3k-http-proxy-k3kcluster-service/apis/coordination.k8s.io/v1/namespaces/kube-system/leases": context canceled
{"level":"fatal","timestamp":"2025-06-04T13:14:53.369Z","logger":"k3k-kubelet","msg":"virtual manager stopped","error":"too many open files"}
```
This typically indicates a low limit on inotify watchers or file descriptors on the host system.
To increase the inotify limits connect to the host nodes and run:
```sh
sudo sysctl -w fs.inotify.max_user_watches=2099999999
sudo sysctl -w fs.inotify.max_user_instances=2099999999
sudo sysctl -w fs.inotify.max_queued_events=2099999999
```
You can persist these settings by adding them to `/etc/sysctl.conf`:
```sh
fs.inotify.max_user_watches=2099999999
fs.inotify.max_user_instances=2099999999
fs.inotify.max_queued_events=2099999999
```
Apply the changes:
```sh
sudo sysctl -p
```
You can find more details in this [KB document](https://www.suse.com/support/kb/doc/?id=000020048).
---
## Inspect Controller Logs for Failure Diagnosis
To view logs for a failed virtual cluster:
```sh
kubectl logs -n k3k-system -l app.kubernetes.io/name=k3k
```
This retrieves logs from K3k controller components.
---
## Inspect Cluster Logs for Failure Diagnosis
To view logs for a failed virtual cluster:
```sh
kubectl logs -n <cluster_namespace> -l cluster=<cluster_name>
```
This retrieves logs from K3k cluster components (`agents, server and virtual-kubelet`).
> 💡 You can also use `kubectl describe cluster <cluster_name>` to check for recent events and status conditions.
---
## Virtual Cluster Not Starting or Stuck in Pending
Some of the most common causes are related to missing prerequisites or wrong configuration.
### Storage class not available
When creating a Virtual Cluster with `dynamic` persistence, a PVC is needed. You can check if the PVC was claimed but not bound with `kubectl get pvc -n <cluster_namespace>`. If you see a pending PVC you probably don't have a default storage class defined, or you have specified a wrong one.
#### Example with wrong storage class
The `pvc` is pending:
```bash
kubectl get pvc -n k3k-test-storage
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
varlibrancherk3s-k3k-test-storage-server-0 Pending not-available <unset> 4s
```
The `server` is pending:
```bash
kubectl get po -n k3k-test-storage
NAME READY STATUS RESTARTS AGE
k3k-test-storage-kubelet-j4zn5 1/1 Running 0 54s
k3k-test-storage-server-0 0/1 Pending 0 54s
```
To fix this you should use a valid storage class, you can list existing storage class using:
```bash
kubectl get storageclasses.storage.k8s.io
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path (default) rancher.io/local-path Delete WaitForFirstConsumer false 3d6h
```
### Wrong node selector
When creating a Virtual Cluster with `defaultNodeSelector`, if the selector is not valid all pods will be pending.
#### Example
The `server` is pending:
```bash
kubectl get po
NAME READY STATUS RESTARTS AGE
k3k-k3kcluster-node-placed-server-0 0/1 Pending 0 58s
```
The description of the pod provide the reason:
```bash
kubectl describe po k3k-k3kcluster-node-placed-server-0
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 84s default-scheduler 0/1 nodes are available: 1 node(s) didn't match Pod's node affinity/selector. preemption: 0/1 nodes are available: 1 Preemption is not helpful for scheduling.
```
To fix this you should use a valid node affinity/selector.
### Image pull issues (airgapped setup)
When creating a Virtual Cluster in air-gapped environment, images need to be available in the configured registry. You can check for `ImagePullBackOff` status when getting the pods in the virtual cluster namespace.
#### Example
The `server` is failing:
```bash
kubectl get po -n k3k-test-registry
NAME READY STATUS RESTARTS AGE
k3k-test-registry-kubelet-r4zh5 1/1 Running 0 54s
k3k-test-registry-server-0 0/1 ImagePullBackOff 0 54s
```
To fix this make sure the failing image is available. You can describe the failing pod to get more details.

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

View File

@@ -0,0 +1,147 @@
# VirtualClusterPolicy
The VirtualClusterPolicy Custom Resource in K3k provides a way to define and enforce consistent configurations, security settings, and resource management rules for your virtual clusters and the Namespaces they operate within.
By using VCPs, administrators can centrally manage these aspects, reducing manual configuration, ensuring alignment with organizational standards, and enhancing the overall security and operational consistency of the K3k environment.
## Core Concepts
### What is a VirtualClusterPolicy?
A `VirtualClusterPolicy` is a cluster-scoped Kubernetes Custom Resource that specifies a set of rules and configurations. These policies are then applied to K3k virtual clusters (`Cluster` resources) operating within Kubernetes Namespaces that are explicitly bound to a VCP.
### Binding a Policy to a Namespace
To apply a `VirtualClusterPolicy` to one or more Namespaces (and thus to all K3k `Cluster` resources within those Namespaces), you need to label the desired Namespace(s). Add the following label to your Namespace metadata:
`policy.k3k.io/policy-name: <YOUR_POLICY_NAME>`
**Example: Labeling a Namespace**
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: my-app-namespace
labels:
policy.k3k.io/policy-name: "standard-dev-policy"
```
In this example, `my-app-namespace` will adhere to the rules defined in the `VirtualClusterPolicy` named `standard-dev-policy`. Multiple Namespaces can be bound to the same policy for uniform configuration, or different Namespaces can be bound to distinct policies.
It's also important to note what happens when a Namespace's policy binding changes. If a Namespace is unbound from a VirtualClusterPolicy (by removing the policy.k3k.io/policy-name label), K3k will clean up and remove the resources (such as ResourceQuotas, LimitRanges, and managed Namespace labels) that were originally applied by that policy. Similarly, if the label is changed to bind the Namespace to a new VirtualClusterPolicy, K3k will first remove the resources associated with the old policy before applying the configurations from the new one, ensuring a clean transition.
### Default Policy Values
If you create a `VirtualClusterPolicy` without specifying any `spec` fields (e.g., using `k3kcli policy create my-default-policy`), it will be created with default settings. Currently, this includes `spec.allowedMode` being set to `"shared"`.
```yaml
# Example of a minimal VCP (after creation with defaults)
apiVersion: k3k.io/v1alpha1
kind: VirtualClusterPolicy
metadata:
name: my-default-policy
spec:
allowedMode: shared
```
## Key Capabilities & Examples
A `VirtualClusterPolicy` can configure several aspects of the Namespaces it's bound to and the virtual clusters operating within them.
### 1. Restricting Allowed Virtual Cluster Modes (`AllowedMode`)
You can restrict the `mode` (e.g., "shared" or "virtual") in which K3k `Cluster` resources can be provisioned within bound Namespaces. If a `Cluster` is created in a bound Namespace with a mode not allowed in `allowedMode`, its creation might proceed but an error should be reported in the `Cluster` resource's status.
**Example:** Allow only "shared" mode clusters.
```yaml
apiVersion: k3k.io/v1alpha1
kind: VirtualClusterPolicy
metadata:
name: shared-only-policy
spec:
allowedModeTypes:
- shared
```
You can also specify this using the CLI: `k3kcli policy create --mode shared shared-only-policy` (or `--mode virtual`).
### 2. Defining Resource Quotas (`quota`)
You can define resource consumption limits for bound Namespaces by specifying a `ResourceQuota`. K3k will create a `ResourceQuota` object in each bound Namespace with the provided specifications.
**Example:** Set CPU, memory, and pod limits.
```yaml
apiVersion: k3k.io/v1alpha1
kind: VirtualClusterPolicy
metadata:
name: quota-policy
spec:
quota:
hard:
cpu: "10"
memory: "20Gi"
pods: "10"
```
### 3. Setting Limit Ranges (`limit`)
You can define default resource requests/limits and min/max constraints for containers running in bound Namespaces by specifying a `LimitRange`. K3k will create a `LimitRange` object in each bound Namespace.
**Example:** Define default CPU requests/limits and min/max CPU.
```yaml
apiVersion: k3k.io/v1alpha1
kind: VirtualClusterPolicy
metadata:
name: limit-policy
spec:
limit:
limits:
- default:
cpu: "500m"
defaultRequest:
cpu: "500m"
max:
cpu: "1"
min:
cpu: "100m"
type: Container
```
### 4. Managing Network Isolation (`disableNetworkPolicy`)
By default, K3k creates a `NetworkPolicy` in bound Namespaces to provide network isolation for virtual clusters (especially in shared mode). You can disable the creation of this default policy.
**Example:** Disable the default NetworkPolicy.
```yaml
apiVersion: k3k.io/v1alpha1
kind: VirtualClusterPolicy
metadata:
name: no-default-netpol-policy
spec:
disableNetworkPolicy: true
```
### 5. Enforcing Pod Security Admission (`podSecurityAdmissionLevel`)
You can enforce Pod Security Standards (PSS) by specifying a Pod Security Admission (PSA) level. K3k will apply the corresponding PSA labels to each bound Namespace. The allowed values are `privileged`, `baseline`, `restricted`, and this will add labels like `pod-security.kubernetes.io/enforce: <level>` to the bound Namespace.
**Example:** Enforce the "baseline" PSS level.
```yaml
apiVersion: k3k.io/v1alpha1
kind: VirtualClusterPolicy
metadata:
name: baseline-psa-policy
spec:
podSecurityAdmissionLevel: baseline
```
## Further Reading
* For a complete reference of all `VirtualClusterPolicy` spec fields, see the [API Reference for VirtualClusterPolicy](./crds/crd-docs.md#virtualclusterpolicy).
* To understand how VCPs fit into the overall K3k system, see the [Architecture](./architecture.md) document.

9
examples/clusterset.yaml Normal file
View File

@@ -0,0 +1,9 @@
apiVersion: k3k.io/v1alpha1
kind: VirtualClusterPolicy
metadata:
name: policy-example
# spec:
# disableNetworkPolicy: false
# allowedMode: "shared"
# podSecurityAdmissionLevel: "baseline"
# defaultPriorityClass: "lowpriority"

View File

@@ -3,6 +3,7 @@ kind: Cluster
metadata:
name: example1
spec:
mode: "shared"
servers: 1
agents: 3
token: test

View File

@@ -3,6 +3,7 @@ kind: Cluster
metadata:
name: single-server
spec:
mode: "shared"
servers: 1
agents: 3
token: test

277
go.mod
View File

@@ -1,74 +1,231 @@
module github.com/rancher/k3k
go 1.19
go 1.24.2
require (
github.com/sirupsen/logrus v1.8.1
github.com/urfave/cli v1.22.12
k8s.io/api v0.26.1
k8s.io/apimachinery v0.26.1
k8s.io/client-go v0.26.1
k8s.io/klog v1.0.0
replace (
github.com/google/cel-go => github.com/google/cel-go v0.20.1
github.com/prometheus/client_golang => github.com/prometheus/client_golang v1.16.0
github.com/prometheus/client_model => github.com/prometheus/client_model v0.6.1
github.com/prometheus/common => github.com/prometheus/common v0.64.0
golang.org/x/term => golang.org/x/term v0.15.0
)
require (
github.com/go-logr/zapr v1.3.0
github.com/google/go-cmp v0.7.0
github.com/onsi/ginkgo/v2 v2.21.0
github.com/onsi/gomega v1.36.0
github.com/rancher/dynamiclistener v1.27.5
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.10.0
github.com/testcontainers/testcontainers-go v0.35.0
github.com/testcontainers/testcontainers-go/modules/k3s v0.35.0
github.com/virtual-kubelet/virtual-kubelet v1.11.1-0.20250530103808-c9f64e872803
go.etcd.io/etcd/api/v3 v3.5.16
go.etcd.io/etcd/client/v3 v3.5.16
go.uber.org/zap v1.27.0
gopkg.in/yaml.v2 v2.4.0
helm.sh/helm/v3 v3.14.4
k8s.io/api v0.31.4
k8s.io/apiextensions-apiserver v0.31.4
k8s.io/apimachinery v0.31.4
k8s.io/apiserver v0.31.4
k8s.io/cli-runtime v0.31.4
k8s.io/client-go v0.31.4
k8s.io/component-base v0.31.4
k8s.io/component-helpers v0.31.4
k8s.io/kubectl v0.31.4
k8s.io/kubelet v0.31.4
k8s.io/kubernetes v1.31.4
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
sigs.k8s.io/controller-runtime v0.19.4
)
require (
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
)
require (
dario.cat/mergo v1.0.1 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/uuid v1.1.2 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/term v0.3.0 // indirect
golang.org/x/time v0.3.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.26.0 // indirect
k8s.io/component-base v0.26.1 // indirect
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
require (
github.com/go-logr/logr v1.2.3 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/containerd/containerd v1.7.24 // indirect
github.com/containerd/errdefs v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v25.0.1+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v27.1.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch v5.9.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // 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-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/cel-go v0.22.0 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/gosuri/uitable v0.0.4 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/moby/sys/mountinfo v0.7.2 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.3.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/rancher/dynamiclistener v0.3.5
golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 // indirect
golang.org/x/text v0.5.0 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.64.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rubenv/sql-migrate v1.7.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
go.opentelemetry.io/otel v1.33.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
go.opentelemetry.io/otel/metric v1.33.0 // indirect
go.opentelemetry.io/otel/sdk v1.33.0 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.26.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
google.golang.org/grpc v1.67.3 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/apiserver v0.26.1
k8s.io/klog/v2 v2.80.1
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448
sigs.k8s.io/controller-runtime v0.14.1
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kms v0.31.4 // indirect
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
oras.land/oras-go v1.2.5 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/kustomize/api v0.18.0 // indirect
sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.3 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

1049
go.sum

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

View File

@@ -1,21 +0,0 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
CODEGEN_GIT_PKG=https://github.com/kubernetes/code-generator.git
git clone --depth 1 ${CODEGEN_GIT_PKG} || true
SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/..
CODEGEN_PKG=./code-generator
"${CODEGEN_PKG}/generate-groups.sh" \
"deepcopy" \
github.com/rancher/k3k/pkg/generated \
github.com/rancher/k3k/pkg/apis \
"k3k.io:v1alpha1" \
--go-header-file "${SCRIPT_ROOT}"/hack/boilerplate.go.txt \
--output-base "$(dirname "${BASH_SOURCE[0]}")/../../../.."
rm -rf code-generator

34
k3k-kubelet/README.md Normal file
View File

@@ -0,0 +1,34 @@
## Virtual Kubelet
This package provides an impelementation of a virtual cluster node using [virtual-kubelet](https://github.com/virtual-kubelet/virtual-kubelet).
The implementation is based on several projects, including:
- [Virtual Kubelet](https://github.com/virtual-kubelet/virtual-kubelet)
- [Kubectl](https://github.com/kubernetes/kubectl)
- [Client-go](https://github.com/kubernetes/client-go)
- [Azure-Aci](https://github.com/virtual-kubelet/azure-aci)
## Overview
This project creates a node that registers itself in the virtual cluster. When workloads are scheduled to this node, it simply creates/updates the workload on the host cluster.
## Usage
Build/Push the image using (from the root of rancher/k3k):
```
make build
docker buildx build -f package/Dockerfile . -t $REPO/$IMAGE:$TAG
```
When running, it is recommended to deploy a k3k cluster with 1 server (with `--disable-agent` as a server arg) and no agents (so that the workloads can only be scheduled on the virtual node/host cluster).
After the image is built, it should be deployed with the following ENV vars set:
- `CLUSTER_NAME` should be the name of the cluster.
- `CLUSTER_NAMESPACE` should be the namespace the cluster is running in.
- `HOST_KUBECONFIG` should be the path on the local filesystem (in container) to a kubeconfig for the host cluster (likely stored in a secret/mounted as a volume).
- `VIRT_KUBECONFIG`should be the path on the local filesystem (in container) to a kubeconfig for the virtual cluster (likely stored in a secret/mounted as a volume).
- `VIRT_POD_IP` should be the IP that the container is accessible from.
This project is still under development and there are many features yet to be implemented, but it can run a basic nginx pod.

37
k3k-kubelet/config.go Normal file
View File

@@ -0,0 +1,37 @@
package main
import (
"errors"
)
// config has all virtual-kubelet startup options
type config struct {
ClusterName string `mapstructure:"clusterName"`
ClusterNamespace string `mapstructure:"clusterNamespace"`
ServiceName string `mapstructure:"serviceName"`
Token string `mapstructure:"token"`
AgentHostname string `mapstructure:"agentHostname"`
HostKubeconfig string `mapstructure:"hostKubeconfig"`
VirtKubeconfig string `mapstructure:"virtKubeconfig"`
KubeletPort int `mapstructure:"kubeletPort"`
WebhookPort int `mapstructure:"webhookPort"`
ServerIP string `mapstructure:"serverIP"`
Version string `mapstructure:"version"`
MirrorHostNodes bool `mapstructure:"mirrorHostNodes"`
}
func (c *config) validate() error {
if c.ClusterName == "" {
return errors.New("cluster name is not provided")
}
if c.ClusterNamespace == "" {
return errors.New("cluster namespace is not provided")
}
if c.AgentHostname == "" {
return errors.New("agent Hostname is not provided")
}
return nil
}

View File

@@ -0,0 +1,198 @@
package controller
import (
"context"
"fmt"
"sync"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"github.com/rancher/k3k/pkg/controller"
k3klog "github.com/rancher/k3k/pkg/log"
)
const ConfigMapSyncerName = "configmap-syncer"
type ConfigMapSyncer struct {
mutex sync.RWMutex
// VirtualClient is the client for the virtual cluster
VirtualClient client.Client
// CoreClient is the client for the host cluster
HostClient client.Client
// TranslateFunc is the function that translates a given resource from it's virtual representation to the host
// representation
TranslateFunc func(*corev1.ConfigMap) (*corev1.ConfigMap, error)
// Logger is the logger that the controller will use
Logger *k3klog.Logger
// objs are the objects that the syncer should watch/syncronize. Should only be manipulated
// through add/remove
objs sets.Set[types.NamespacedName]
}
func (c *ConfigMapSyncer) Name() string {
return ConfigMapSyncerName
}
// Reconcile implements reconcile.Reconciler and synchronizes the objects in objs to the host cluster
func (c *ConfigMapSyncer) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
if !c.isWatching(req.NamespacedName) {
// return immediately without re-enqueueing. We aren't watching this resource
return reconcile.Result{}, nil
}
var virtual corev1.ConfigMap
if err := c.VirtualClient.Get(ctx, req.NamespacedName, &virtual); err != nil {
return reconcile.Result{
Requeue: true,
}, fmt.Errorf("unable to get configmap %s/%s from virtual cluster: %w", req.Namespace, req.Name, err)
}
translated, err := c.TranslateFunc(&virtual)
if err != nil {
return reconcile.Result{
Requeue: true,
}, fmt.Errorf("unable to translate configmap %s/%s from virtual cluster: %w", req.Namespace, req.Name, err)
}
translatedKey := types.NamespacedName{
Namespace: translated.Namespace,
Name: translated.Name,
}
var host corev1.ConfigMap
if err = c.HostClient.Get(ctx, translatedKey, &host); err != nil {
if apierrors.IsNotFound(err) {
err = c.HostClient.Create(ctx, translated)
// for simplicity's sake, we don't check for conflict errors. The existing object will get
// picked up on in the next re-enqueue
return reconcile.Result{
Requeue: true,
}, fmt.Errorf("unable to create host configmap %s/%s for virtual configmap %s/%s: %w",
translated.Namespace, translated.Name, req.Namespace, req.Name, err)
}
return reconcile.Result{Requeue: true}, fmt.Errorf("unable to get host configmap %s/%s: %w", translated.Namespace, translated.Name, err)
}
// we are going to use the host in order to avoid conflicts on update
host.Data = translated.Data
if host.Labels == nil {
host.Labels = make(map[string]string, len(translated.Labels))
}
// we don't want to override labels made on the host cluster by other applications
// but we do need to make sure the labels that the kubelet uses to track host cluster values
// are being tracked appropriately
for key, value := range translated.Labels {
host.Labels[key] = value
}
if err = c.HostClient.Update(ctx, &host); err != nil {
return reconcile.Result{
Requeue: true,
}, fmt.Errorf("unable to update host configmap %s/%s for virtual configmap %s/%s: %w",
translated.Namespace, translated.Name, req.Namespace, req.Name, err)
}
return reconcile.Result{}, nil
}
// isWatching is a utility method to determine if a key is in objs without the caller needing
// to handle mutex lock/unlock.
func (c *ConfigMapSyncer) isWatching(key types.NamespacedName) bool {
c.mutex.RLock()
defer c.mutex.RUnlock()
return c.objs.Has(key)
}
// AddResource adds a given resource to the list of resources that will be synced. Safe to call multiple times for the
// same resource.
func (c *ConfigMapSyncer) AddResource(ctx context.Context, namespace, name string) error {
objKey := types.NamespacedName{
Namespace: namespace,
Name: name,
}
// if we already sync this object, no need to writelock/add it
if c.isWatching(objKey) {
return nil
}
// lock in write mode since we are now adding the key
c.mutex.Lock()
if c.objs == nil {
c.objs = sets.Set[types.NamespacedName]{}
}
c.objs = c.objs.Insert(objKey)
c.mutex.Unlock()
_, err := c.Reconcile(ctx, reconcile.Request{
NamespacedName: objKey,
})
if err != nil {
return fmt.Errorf("unable to reconcile new object %s/%s: %w", objKey.Namespace, objKey.Name, err)
}
return nil
}
// RemoveResource removes a given resource from the list of resources that will be synced. Safe to call for an already
// removed resource.
func (c *ConfigMapSyncer) RemoveResource(ctx context.Context, namespace, name string) error {
objKey := types.NamespacedName{
Namespace: namespace,
Name: name,
}
// if we don't sync this object, no need to writelock/add it
if !c.isWatching(objKey) {
return nil
}
if err := retry.OnError(controller.Backoff, func(err error) bool {
return err != nil
}, func() error {
return c.removeHostConfigMap(ctx, namespace, name)
}); err != nil {
return fmt.Errorf("unable to remove configmap: %w", err)
}
c.mutex.Lock()
if c.objs == nil {
c.objs = sets.Set[types.NamespacedName]{}
}
c.objs = c.objs.Delete(objKey)
c.mutex.Unlock()
return nil
}
func (c *ConfigMapSyncer) removeHostConfigMap(ctx context.Context, virtualNamespace, virtualName string) error {
var vConfigMap corev1.ConfigMap
key := types.NamespacedName{
Namespace: virtualNamespace,
Name: virtualName,
}
if err := c.VirtualClient.Get(ctx, key, &vConfigMap); err != nil {
return fmt.Errorf("unable to get virtual configmap %s/%s: %w", virtualNamespace, virtualName, err)
}
translated, err := c.TranslateFunc(&vConfigMap)
if err != nil {
return fmt.Errorf("unable to translate virtual secret: %s/%s: %w", virtualNamespace, virtualName, err)
}
return c.HostClient.Delete(ctx, translated)
}

View File

@@ -0,0 +1,169 @@
package controller_test
import (
"context"
"errors"
"os"
"path"
"path/filepath"
"testing"
"github.com/go-logr/zapr"
"go.uber.org/zap"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestController(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Cluster Controller Suite")
}
type TestEnv struct {
*envtest.Environment
k8s *kubernetes.Clientset
k8sClient client.Client
}
var (
hostTestEnv *TestEnv
hostManager ctrl.Manager
virtTestEnv *TestEnv
virtManager ctrl.Manager
)
var _ = BeforeSuite(func() {
hostTestEnv = NewTestEnv()
By("HOST testEnv running at :" + hostTestEnv.ControlPlane.APIServer.Port)
virtTestEnv = NewTestEnv()
By("VIRT testEnv running at :" + virtTestEnv.ControlPlane.APIServer.Port)
ctrl.SetLogger(zapr.NewLogger(zap.NewNop()))
ctrl.SetupSignalHandler()
})
var _ = AfterSuite(func() {
By("tearing down the test environment")
err := hostTestEnv.Stop()
Expect(err).NotTo(HaveOccurred())
err = virtTestEnv.Stop()
Expect(err).NotTo(HaveOccurred())
tmpKubebuilderDir := path.Join(os.TempDir(), "kubebuilder")
err = os.RemoveAll(tmpKubebuilderDir)
Expect(err).NotTo(HaveOccurred())
})
func NewTestEnv() *TestEnv {
GinkgoHelper()
binaryAssetsDirectory := os.Getenv("KUBEBUILDER_ASSETS")
if binaryAssetsDirectory == "" {
binaryAssetsDirectory = "/usr/local/kubebuilder/bin"
}
tmpKubebuilderDir := path.Join(os.TempDir(), "kubebuilder")
if err := os.Mkdir(tmpKubebuilderDir, 0o755); !errors.Is(err, os.ErrExist) {
Expect(err).NotTo(HaveOccurred())
}
tempDir, err := os.MkdirTemp(tmpKubebuilderDir, "envtest-*")
Expect(err).NotTo(HaveOccurred())
err = os.CopyFS(tempDir, os.DirFS(binaryAssetsDirectory))
Expect(err).NotTo(HaveOccurred())
By("bootstrapping test environment")
testEnv := &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "charts", "k3k", "crds")},
ErrorIfCRDPathMissing: true,
BinaryAssetsDirectory: tempDir,
Scheme: buildScheme(),
}
cfg, err := testEnv.Start()
Expect(err).NotTo(HaveOccurred())
k8s, err := kubernetes.NewForConfig(cfg)
Expect(err).NotTo(HaveOccurred())
k8sClient, err := client.New(cfg, client.Options{Scheme: testEnv.Scheme})
Expect(err).NotTo(HaveOccurred())
return &TestEnv{
Environment: testEnv,
k8s: k8s,
k8sClient: k8sClient,
}
}
func buildScheme() *runtime.Scheme {
scheme := runtime.NewScheme()
err := clientgoscheme.AddToScheme(scheme)
Expect(err).NotTo(HaveOccurred())
err = v1alpha1.AddToScheme(scheme)
Expect(err).NotTo(HaveOccurred())
return scheme
}
var _ = Describe("Kubelet Controller", func() {
var (
ctx context.Context
cancel context.CancelFunc
)
BeforeEach(func() {
var err error
ctx, cancel = context.WithCancel(context.Background())
hostManager, err = ctrl.NewManager(hostTestEnv.Config, ctrl.Options{
// disable the metrics server
Metrics: metricsserver.Options{BindAddress: "0"},
Scheme: hostTestEnv.Scheme,
})
Expect(err).NotTo(HaveOccurred())
virtManager, err = ctrl.NewManager(virtTestEnv.Config, ctrl.Options{
// disable the metrics server
Metrics: metricsserver.Options{BindAddress: "0"},
Scheme: virtTestEnv.Scheme,
})
Expect(err).NotTo(HaveOccurred())
go func() {
defer GinkgoRecover()
err := hostManager.Start(ctx)
Expect(err).NotTo(HaveOccurred(), "failed to run host manager")
}()
go func() {
defer GinkgoRecover()
err := virtManager.Start(ctx)
Expect(err).NotTo(HaveOccurred(), "failed to run virt manager")
}()
})
AfterEach(func() {
cancel()
})
Describe("PriorityClass", PriorityClassTests)
})

View File

@@ -0,0 +1,135 @@
package controller
import (
"context"
"fmt"
"sync"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
v1 "k8s.io/api/core/v1"
ctrl "sigs.k8s.io/controller-runtime"
"github.com/rancher/k3k/k3k-kubelet/translate"
k3klog "github.com/rancher/k3k/pkg/log"
)
type ControllerHandler struct {
sync.RWMutex
// Mgr is the manager used to run new controllers - from the virtual cluster
Mgr manager.Manager
// Scheme is the scheme used to run new controllers - from the virtual cluster
Scheme runtime.Scheme
// HostClient is the client used to communicate with the host cluster
HostClient client.Client
// VirtualClient is the client used to communicate with the virtual cluster
VirtualClient client.Client
// Translator is the translator that will be used to adjust objects before they
// are made on the host cluster
Translator translate.ToHostTranslator
// Logger is the logger that the controller will use to log errors
Logger *k3klog.Logger
// controllers are the controllers which are currently running
controllers map[schema.GroupVersionKind]updateableReconciler
}
// updateableReconciler is a reconciler that only syncs specific resources (by name/namespace). This list can
// be altered through the Add and Remove methods
type updateableReconciler interface {
reconcile.Reconciler
Name() string
AddResource(ctx context.Context, namespace string, name string) error
RemoveResource(ctx context.Context, namespace string, name string) error
}
func (c *ControllerHandler) AddResource(ctx context.Context, obj client.Object) error {
c.RLock()
controllers := c.controllers
if controllers != nil {
if r, ok := c.controllers[obj.GetObjectKind().GroupVersionKind()]; ok {
err := r.AddResource(ctx, obj.GetNamespace(), obj.GetName())
c.RUnlock()
return err
}
}
// we need to manually lock/unlock since we intned on write locking to add a new controller
c.RUnlock()
var r updateableReconciler
switch obj.(type) {
case *v1.Secret:
r = &SecretSyncer{
HostClient: c.HostClient,
VirtualClient: c.VirtualClient,
// TODO: Need actual function
TranslateFunc: func(s *v1.Secret) (*v1.Secret, error) {
// note that this doesn't do any type safety - fix this
// when generics work
c.Translator.TranslateTo(s)
// Remove service-account-token types when synced to the host
if s.Type == v1.SecretTypeServiceAccountToken {
s.Type = v1.SecretTypeOpaque
}
return s, nil
},
Logger: c.Logger,
}
case *v1.ConfigMap:
r = &ConfigMapSyncer{
HostClient: c.HostClient,
VirtualClient: c.VirtualClient,
// TODO: Need actual function
TranslateFunc: func(s *v1.ConfigMap) (*v1.ConfigMap, error) {
c.Translator.TranslateTo(s)
return s, nil
},
Logger: c.Logger,
}
default:
// TODO: Technically, the configmap/secret syncers are relatively generic, and this
// logic could be used for other types.
return fmt.Errorf("unrecognized type: %T", obj)
}
err := ctrl.NewControllerManagedBy(c.Mgr).
Named(r.Name()).
For(&v1.ConfigMap{}).
Complete(r)
if err != nil {
return fmt.Errorf("unable to start configmap controller: %w", err)
}
c.Lock()
if c.controllers == nil {
c.controllers = map[schema.GroupVersionKind]updateableReconciler{}
}
c.controllers[obj.GetObjectKind().GroupVersionKind()] = r
c.Unlock()
return r.AddResource(ctx, obj.GetNamespace(), obj.GetName())
}
func (c *ControllerHandler) RemoveResource(ctx context.Context, obj client.Object) error {
// since we aren't adding a new controller, we don't need to lock
c.RLock()
ctrl, ok := c.controllers[obj.GetObjectKind().GroupVersionKind()]
c.RUnlock()
if !ok {
return fmt.Errorf("no controller found for gvk %s", obj.GetObjectKind().GroupVersionKind())
}
return ctrl.RemoveResource(ctx, obj.GetNamespace(), obj.GetName())
}

View File

@@ -0,0 +1,120 @@
package controller
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
ctrl "sigs.k8s.io/controller-runtime"
ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/rancher/k3k/k3k-kubelet/translate"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
)
const (
pvcController = "pvc-syncer-controller"
pvcFinalizerName = "pvc.k3k.io/finalizer"
)
type PVCReconciler struct {
clusterName string
clusterNamespace string
virtualClient ctrlruntimeclient.Client
hostClient ctrlruntimeclient.Client
Scheme *runtime.Scheme
HostScheme *runtime.Scheme
Translator translate.ToHostTranslator
}
// AddPVCSyncer adds persistentvolumeclaims syncer controller to k3k-kubelet
func AddPVCSyncer(ctx context.Context, virtMgr, hostMgr manager.Manager, clusterName, clusterNamespace string) error {
translator := translate.ToHostTranslator{
ClusterName: clusterName,
ClusterNamespace: clusterNamespace,
}
// initialize a new Reconciler
reconciler := PVCReconciler{
clusterName: clusterName,
clusterNamespace: clusterNamespace,
virtualClient: virtMgr.GetClient(),
hostClient: hostMgr.GetClient(),
Scheme: virtMgr.GetScheme(),
HostScheme: hostMgr.GetScheme(),
Translator: translator,
}
return ctrl.NewControllerManagedBy(virtMgr).
Named(pvcController).
For(&v1.PersistentVolumeClaim{}).
Complete(&reconciler)
}
func (r *PVCReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
log := ctrl.LoggerFrom(ctx).WithValues("cluster", r.clusterName, "clusterNamespace", r.clusterNamespace)
ctx = ctrl.LoggerInto(ctx, log)
var (
virtPVC v1.PersistentVolumeClaim
cluster v1alpha1.Cluster
)
if err := r.hostClient.Get(ctx, types.NamespacedName{Name: r.clusterName, Namespace: r.clusterNamespace}, &cluster); err != nil {
return reconcile.Result{}, err
}
if err := r.virtualClient.Get(ctx, req.NamespacedName, &virtPVC); err != nil {
return reconcile.Result{}, ctrlruntimeclient.IgnoreNotFound(err)
}
syncedPVC := r.pvc(&virtPVC)
if err := controllerutil.SetControllerReference(&cluster, syncedPVC, r.HostScheme); err != nil {
return reconcile.Result{}, err
}
// handle deletion
if !virtPVC.DeletionTimestamp.IsZero() {
// deleting the synced service if exists
if err := r.hostClient.Delete(ctx, syncedPVC); !apierrors.IsNotFound(err) {
return reconcile.Result{}, err
}
// remove the finalizer after cleaning up the synced service
if controllerutil.RemoveFinalizer(&virtPVC, pvcFinalizerName) {
if err := r.virtualClient.Update(ctx, &virtPVC); err != nil {
return reconcile.Result{}, err
}
}
return reconcile.Result{}, nil
}
// Add finalizer if it does not exist
if controllerutil.AddFinalizer(&virtPVC, pvcFinalizerName) {
if err := r.virtualClient.Update(ctx, &virtPVC); err != nil {
return reconcile.Result{}, err
}
}
// create the pvc on host
log.Info("creating the persistent volume for the first time on the host cluster")
// note that we dont need to update the PVC on the host cluster, only syncing the PVC to allow being
// handled by the host cluster.
return reconcile.Result{}, ctrlruntimeclient.IgnoreAlreadyExists(r.hostClient.Create(ctx, syncedPVC))
}
func (r *PVCReconciler) pvc(obj *v1.PersistentVolumeClaim) *v1.PersistentVolumeClaim {
hostPVC := obj.DeepCopy()
r.Translator.TranslateTo(hostPVC)
return hostPVC
}

View File

@@ -0,0 +1,184 @@
package controller
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/component-helpers/storage/volume"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrl "sigs.k8s.io/controller-runtime"
ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/rancher/k3k/k3k-kubelet/translate"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
)
const (
podController = "pod-pvc-controller"
pseudoPVLabel = "pod.k3k.io/pseudoPV"
)
type PodReconciler struct {
clusterName string
clusterNamespace string
virtualClient ctrlruntimeclient.Client
hostClient ctrlruntimeclient.Client
Scheme *runtime.Scheme
HostScheme *runtime.Scheme
Translator translate.ToHostTranslator
}
// AddPodPVCController adds pod controller to k3k-kubelet
func AddPodPVCController(ctx context.Context, virtMgr, hostMgr manager.Manager, clusterName, clusterNamespace string) error {
translator := translate.ToHostTranslator{
ClusterName: clusterName,
ClusterNamespace: clusterNamespace,
}
// initialize a new Reconciler
reconciler := PodReconciler{
clusterName: clusterName,
clusterNamespace: clusterNamespace,
virtualClient: virtMgr.GetClient(),
hostClient: hostMgr.GetClient(),
Scheme: virtMgr.GetScheme(),
HostScheme: hostMgr.GetScheme(),
Translator: translator,
}
return ctrl.NewControllerManagedBy(virtMgr).
Named(podController).
For(&v1.Pod{}).
Complete(&reconciler)
}
func (r *PodReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
log := ctrl.LoggerFrom(ctx).WithValues("cluster", r.clusterName, "clusterNamespace", r.clusterNamespace)
ctx = ctrl.LoggerInto(ctx, log)
var (
virtPod v1.Pod
cluster v1alpha1.Cluster
)
if err := r.hostClient.Get(ctx, types.NamespacedName{Name: r.clusterName, Namespace: r.clusterNamespace}, &cluster); err != nil {
return reconcile.Result{}, err
}
if err := r.virtualClient.Get(ctx, req.NamespacedName, &virtPod); err != nil {
return reconcile.Result{}, ctrlruntimeclient.IgnoreNotFound(err)
}
// reconcile pods with pvcs
for _, vol := range virtPod.Spec.Volumes {
if vol.PersistentVolumeClaim != nil {
log.Info("Handling pod with pvc")
if err := r.reconcilePodWithPVC(ctx, &virtPod, vol.PersistentVolumeClaim); err != nil {
return reconcile.Result{}, err
}
}
}
return reconcile.Result{}, nil
}
// reconcilePodWithPVC will make sure to create a fake PV for each PVC for any pod so that it can be scheduled on the virtual-kubelet
// and then created on the host, the PV is not synced to the host cluster.
func (r *PodReconciler) reconcilePodWithPVC(ctx context.Context, pod *v1.Pod, pvcSource *v1.PersistentVolumeClaimVolumeSource) error {
log := ctrl.LoggerFrom(ctx).WithValues("PersistentVolumeClaim", pvcSource.ClaimName)
ctx = ctrl.LoggerInto(ctx, log)
var pvc v1.PersistentVolumeClaim
key := types.NamespacedName{
Name: pvcSource.ClaimName,
Namespace: pod.Namespace,
}
if err := r.virtualClient.Get(ctx, key, &pvc); err != nil {
return ctrlruntimeclient.IgnoreNotFound(err)
}
log.Info("Creating pseudo Persistent Volume")
pv := r.pseudoPV(&pvc)
if err := r.virtualClient.Create(ctx, pv); err != nil {
return ctrlruntimeclient.IgnoreAlreadyExists(err)
}
orig := pv.DeepCopy()
pv.Status = v1.PersistentVolumeStatus{
Phase: v1.VolumeBound,
}
if err := r.virtualClient.Status().Patch(ctx, pv, ctrlruntimeclient.MergeFrom(orig)); err != nil {
return err
}
log.Info("Patch the status of PersistentVolumeClaim to Bound")
pvcPatch := pvc.DeepCopy()
if pvcPatch.Annotations == nil {
pvcPatch.Annotations = make(map[string]string)
}
pvcPatch.Annotations[volume.AnnBoundByController] = "yes"
pvcPatch.Annotations[volume.AnnBindCompleted] = "yes"
pvcPatch.Status.Phase = v1.ClaimBound
pvcPatch.Status.AccessModes = pvcPatch.Spec.AccessModes
return r.virtualClient.Status().Update(ctx, pvcPatch)
}
func (r *PodReconciler) pseudoPV(obj *v1.PersistentVolumeClaim) *v1.PersistentVolume {
var storageClass string
if obj.Spec.StorageClassName != nil {
storageClass = *obj.Spec.StorageClassName
}
return &v1.PersistentVolume{
ObjectMeta: metav1.ObjectMeta{
Name: obj.Name,
Labels: map[string]string{
pseudoPVLabel: "true",
},
Annotations: map[string]string{
volume.AnnBoundByController: "true",
volume.AnnDynamicallyProvisioned: "k3k-kubelet",
},
},
TypeMeta: metav1.TypeMeta{
Kind: "PersistentVolume",
APIVersion: "v1",
},
Spec: v1.PersistentVolumeSpec{
PersistentVolumeSource: v1.PersistentVolumeSource{
FlexVolume: &v1.FlexPersistentVolumeSource{
Driver: "pseudopv",
},
},
StorageClassName: storageClass,
VolumeMode: obj.Spec.VolumeMode,
PersistentVolumeReclaimPolicy: v1.PersistentVolumeReclaimDelete,
AccessModes: obj.Spec.AccessModes,
Capacity: obj.Spec.Resources.Requests,
ClaimRef: &v1.ObjectReference{
APIVersion: obj.APIVersion,
UID: obj.UID,
ResourceVersion: obj.ResourceVersion,
Kind: obj.Kind,
Namespace: obj.Namespace,
Name: obj.Name,
},
},
}
}

View File

@@ -0,0 +1,229 @@
package controller_test
import (
"context"
"fmt"
"time"
"sigs.k8s.io/controller-runtime/pkg/client"
v1 "k8s.io/api/core/v1"
schedulingv1 "k8s.io/api/scheduling/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/rancher/k3k/k3k-kubelet/controller"
"github.com/rancher/k3k/k3k-kubelet/translate"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var PriorityClassTests = func() {
var (
namespace string
cluster v1alpha1.Cluster
)
BeforeEach(func() {
ctx := context.Background()
ns := v1.Namespace{
ObjectMeta: metav1.ObjectMeta{GenerateName: "ns-"},
}
err := hostTestEnv.k8sClient.Create(ctx, &ns)
Expect(err).NotTo(HaveOccurred())
namespace = ns.Name
cluster = v1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "cluster-",
Namespace: namespace,
},
}
err = hostTestEnv.k8sClient.Create(ctx, &cluster)
Expect(err).NotTo(HaveOccurred())
err = controller.AddPriorityClassReconciler(ctx, virtManager, hostManager, cluster.Name, cluster.Namespace)
Expect(err).NotTo(HaveOccurred())
})
AfterEach(func() {
ns := v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}
err := hostTestEnv.k8sClient.Delete(context.Background(), &ns)
Expect(err).NotTo(HaveOccurred())
})
It("creates a priorityClass on the host cluster", func() {
ctx := context.Background()
priorityClass := &schedulingv1.PriorityClass{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "pc-",
Labels: map[string]string{
"foo": "bar",
},
},
Value: 1001,
}
err := virtTestEnv.k8sClient.Create(ctx, priorityClass)
Expect(err).NotTo(HaveOccurred())
By(fmt.Sprintf("Created priorityClass %s in virtual cluster", priorityClass.Name))
var hostPriorityClass schedulingv1.PriorityClass
hostPriorityClassName := translateName(cluster, priorityClass.Namespace, priorityClass.Name)
Eventually(func() error {
key := client.ObjectKey{Name: hostPriorityClassName}
return hostTestEnv.k8sClient.Get(ctx, key, &hostPriorityClass)
}).
WithPolling(time.Millisecond * 300).
WithTimeout(time.Second * 10).
Should(BeNil())
By(fmt.Sprintf("Created priorityClass %s in host cluster", hostPriorityClassName))
Expect(hostPriorityClass.Value).To(Equal(priorityClass.Value))
Expect(hostPriorityClass.Labels).To(ContainElement("bar"))
GinkgoWriter.Printf("labels: %v\n", hostPriorityClass.Labels)
})
It("updates a priorityClass on the host cluster", func() {
ctx := context.Background()
priorityClass := &schedulingv1.PriorityClass{
ObjectMeta: metav1.ObjectMeta{GenerateName: "pc-"},
Value: 1001,
}
err := virtTestEnv.k8sClient.Create(ctx, priorityClass)
Expect(err).NotTo(HaveOccurred())
By(fmt.Sprintf("Created priorityClass %s in virtual cluster", priorityClass.Name))
var hostPriorityClass schedulingv1.PriorityClass
hostPriorityClassName := translateName(cluster, priorityClass.Namespace, priorityClass.Name)
Eventually(func() error {
key := client.ObjectKey{Name: hostPriorityClassName}
return hostTestEnv.k8sClient.Get(ctx, key, &hostPriorityClass)
}).
WithPolling(time.Millisecond * 300).
WithTimeout(time.Second * 10).
Should(BeNil())
By(fmt.Sprintf("Created priorityClass %s in host cluster", hostPriorityClassName))
Expect(hostPriorityClass.Value).To(Equal(priorityClass.Value))
Expect(hostPriorityClass.Labels).NotTo(ContainElement("bar"))
key := client.ObjectKeyFromObject(priorityClass)
err = virtTestEnv.k8sClient.Get(ctx, key, priorityClass)
Expect(err).NotTo(HaveOccurred())
priorityClass.Labels = map[string]string{"foo": "bar"}
// update virtual priorityClass
err = virtTestEnv.k8sClient.Update(ctx, priorityClass)
Expect(err).NotTo(HaveOccurred())
Expect(priorityClass.Labels).To(ContainElement("bar"))
// check hostPriorityClass
Eventually(func() map[string]string {
key := client.ObjectKey{Name: hostPriorityClassName}
err = hostTestEnv.k8sClient.Get(ctx, key, &hostPriorityClass)
Expect(err).NotTo(HaveOccurred())
return hostPriorityClass.Labels
}).
WithPolling(time.Millisecond * 300).
WithTimeout(time.Second * 10).
Should(ContainElement("bar"))
})
It("deletes a priorityClass on the host cluster", func() {
ctx := context.Background()
priorityClass := &schedulingv1.PriorityClass{
ObjectMeta: metav1.ObjectMeta{GenerateName: "pc-"},
Value: 1001,
}
err := virtTestEnv.k8sClient.Create(ctx, priorityClass)
Expect(err).NotTo(HaveOccurred())
By(fmt.Sprintf("Created priorityClass %s in virtual cluster", priorityClass.Name))
var hostPriorityClass schedulingv1.PriorityClass
hostPriorityClassName := translateName(cluster, priorityClass.Namespace, priorityClass.Name)
Eventually(func() error {
key := client.ObjectKey{Name: hostPriorityClassName}
return hostTestEnv.k8sClient.Get(ctx, key, &hostPriorityClass)
}).
WithPolling(time.Millisecond * 300).
WithTimeout(time.Second * 10).
Should(BeNil())
By(fmt.Sprintf("Created priorityClass %s in host cluster", hostPriorityClassName))
Expect(hostPriorityClass.Value).To(Equal(priorityClass.Value))
err = virtTestEnv.k8sClient.Delete(ctx, priorityClass)
Expect(err).NotTo(HaveOccurred())
Eventually(func() bool {
key := client.ObjectKey{Name: hostPriorityClassName}
err := hostTestEnv.k8sClient.Get(ctx, key, &hostPriorityClass)
return apierrors.IsNotFound(err)
}).
WithPolling(time.Millisecond * 300).
WithTimeout(time.Second * 10).
Should(BeTrue())
})
It("creates a priorityClass on the host cluster with the globalDefault annotation", func() {
ctx := context.Background()
priorityClass := &schedulingv1.PriorityClass{
ObjectMeta: metav1.ObjectMeta{GenerateName: "pc-"},
Value: 1001,
GlobalDefault: true,
}
err := virtTestEnv.k8sClient.Create(ctx, priorityClass)
Expect(err).NotTo(HaveOccurred())
By(fmt.Sprintf("Created priorityClass %s in virtual cluster", priorityClass.Name))
var hostPriorityClass schedulingv1.PriorityClass
hostPriorityClassName := translateName(cluster, priorityClass.Namespace, priorityClass.Name)
Eventually(func() error {
key := client.ObjectKey{Name: hostPriorityClassName}
return hostTestEnv.k8sClient.Get(ctx, key, &hostPriorityClass)
}).
WithPolling(time.Millisecond * 300).
WithTimeout(time.Second * 10).
Should(BeNil())
By(fmt.Sprintf("Created priorityClass %s in host cluster without the GlobalDefault value", hostPriorityClassName))
Expect(hostPriorityClass.Value).To(Equal(priorityClass.Value))
Expect(hostPriorityClass.GlobalDefault).To(BeFalse())
Expect(hostPriorityClass.Annotations[controller.PriorityClassGlobalDefaultAnnotation]).To(Equal("true"))
})
}
func translateName(cluster v1alpha1.Cluster, namespace, name string) string {
translator := translate.ToHostTranslator{
ClusterName: cluster.Name,
ClusterNamespace: cluster.Namespace,
}
return translator.TranslateName(namespace, name)
}

View File

@@ -0,0 +1,159 @@
package controller
import (
"context"
"strings"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
schedulingv1 "k8s.io/api/scheduling/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
ctrl "sigs.k8s.io/controller-runtime"
ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/rancher/k3k/k3k-kubelet/translate"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
)
const (
PriorityClassGlobalDefaultAnnotation = "priorityclass.k3k.io/globalDefault"
priorityClassControllerName = "priorityclass-syncer-controller"
priorityClassFinalizerName = "priorityclass.k3k.io/finalizer"
)
type PriorityClassReconciler struct {
clusterName string
clusterNamespace string
virtualClient ctrlruntimeclient.Client
hostClient ctrlruntimeclient.Client
Scheme *runtime.Scheme
HostScheme *runtime.Scheme
Translator translate.ToHostTranslator
}
// AddPriorityClassReconciler adds a PriorityClass reconciler to k3k-kubelet
func AddPriorityClassReconciler(ctx context.Context, virtMgr, hostMgr manager.Manager, clusterName, clusterNamespace string) error {
translator := translate.ToHostTranslator{
ClusterName: clusterName,
ClusterNamespace: clusterNamespace,
}
// initialize a new Reconciler
reconciler := PriorityClassReconciler{
clusterName: clusterName,
clusterNamespace: clusterNamespace,
virtualClient: virtMgr.GetClient(),
hostClient: hostMgr.GetClient(),
Scheme: virtMgr.GetScheme(),
HostScheme: hostMgr.GetScheme(),
Translator: translator,
}
name := translator.TranslateName("", priorityClassControllerName)
return ctrl.NewControllerManagedBy(virtMgr).
Named(name).
For(&schedulingv1.PriorityClass{}).
WithEventFilter(ignoreSystemPrefixPredicate).
Complete(&reconciler)
}
// IgnoreSystemPrefixPredicate filters out resources whose names start with "system-".
var ignoreSystemPrefixPredicate = predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
return !strings.HasPrefix(e.ObjectOld.GetName(), "system-")
},
CreateFunc: func(e event.CreateEvent) bool {
return !strings.HasPrefix(e.Object.GetName(), "system-")
},
DeleteFunc: func(e event.DeleteEvent) bool {
return !strings.HasPrefix(e.Object.GetName(), "system-")
},
GenericFunc: func(e event.GenericEvent) bool {
return !strings.HasPrefix(e.Object.GetName(), "system-")
},
}
func (r *PriorityClassReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
log := ctrl.LoggerFrom(ctx).WithValues("cluster", r.clusterName, "clusterNamespace", r.clusterNamespace)
ctx = ctrl.LoggerInto(ctx, log)
var (
priorityClass schedulingv1.PriorityClass
cluster v1alpha1.Cluster
)
if err := r.hostClient.Get(ctx, types.NamespacedName{Name: r.clusterName, Namespace: r.clusterNamespace}, &cluster); err != nil {
return reconcile.Result{}, err
}
if err := r.virtualClient.Get(ctx, req.NamespacedName, &priorityClass); err != nil {
return reconcile.Result{}, ctrlruntimeclient.IgnoreNotFound(err)
}
hostPriorityClass := r.translatePriorityClass(priorityClass)
// handle deletion
if !priorityClass.DeletionTimestamp.IsZero() {
// deleting the synced service if exists
// TODO add test for previous implementation without err != nil check, and also check the other controllers
if err := r.hostClient.Delete(ctx, hostPriorityClass); err != nil && !apierrors.IsNotFound(err) {
return reconcile.Result{}, err
}
// remove the finalizer after cleaning up the synced service
if controllerutil.RemoveFinalizer(&priorityClass, priorityClassFinalizerName) {
if err := r.virtualClient.Update(ctx, &priorityClass); err != nil {
return reconcile.Result{}, err
}
}
return reconcile.Result{}, nil
}
// Add finalizer if it does not exist
if controllerutil.AddFinalizer(&priorityClass, priorityClassFinalizerName) {
if err := r.virtualClient.Update(ctx, &priorityClass); err != nil {
return reconcile.Result{}, err
}
}
// create the priorityClass on the host
log.Info("creating the priorityClass for the first time on the host cluster")
err := r.hostClient.Create(ctx, hostPriorityClass)
if err != nil {
if !apierrors.IsAlreadyExists(err) {
return reconcile.Result{}, err
}
return reconcile.Result{}, r.hostClient.Update(ctx, hostPriorityClass)
}
return reconcile.Result{}, nil
}
func (r *PriorityClassReconciler) translatePriorityClass(priorityClass schedulingv1.PriorityClass) *schedulingv1.PriorityClass {
hostPriorityClass := priorityClass.DeepCopy()
r.Translator.TranslateTo(hostPriorityClass)
if hostPriorityClass.Annotations == nil {
hostPriorityClass.Annotations = make(map[string]string)
}
if hostPriorityClass.GlobalDefault {
hostPriorityClass.GlobalDefault = false
hostPriorityClass.Annotations[PriorityClassGlobalDefaultAnnotation] = "true"
}
return hostPriorityClass
}

View File

@@ -0,0 +1,197 @@
package controller
import (
"context"
"fmt"
"sync"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"github.com/rancher/k3k/pkg/controller"
k3klog "github.com/rancher/k3k/pkg/log"
)
const SecretSyncerName = "secret-syncer"
type SecretSyncer struct {
mutex sync.RWMutex
// VirtualClient is the client for the virtual cluster
VirtualClient client.Client
// CoreClient is the client for the host cluster
HostClient client.Client
// TranslateFunc is the function that translates a given resource from it's virtual representation to the host
// representation
TranslateFunc func(*corev1.Secret) (*corev1.Secret, error)
// Logger is the logger that the controller will use
Logger *k3klog.Logger
// objs are the objects that the syncer should watch/syncronize. Should only be manipulated
// through add/remove
objs sets.Set[types.NamespacedName]
}
func (s *SecretSyncer) Name() string {
return SecretSyncerName
}
// Reconcile implements reconcile.Reconciler and synchronizes the objects in objs to the host cluster
func (s *SecretSyncer) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
if !s.isWatching(req.NamespacedName) {
// return immediately without re-enqueueing. We aren't watching this resource
return reconcile.Result{}, nil
}
var virtual corev1.Secret
if err := s.VirtualClient.Get(ctx, req.NamespacedName, &virtual); err != nil {
return reconcile.Result{
Requeue: true,
}, fmt.Errorf("unable to get secret %s/%s from virtual cluster: %w", req.Namespace, req.Name, err)
}
translated, err := s.TranslateFunc(&virtual)
if err != nil {
return reconcile.Result{
Requeue: true,
}, fmt.Errorf("unable to translate secret %s/%s from virtual cluster: %w", req.Namespace, req.Name, err)
}
translatedKey := types.NamespacedName{
Namespace: translated.Namespace,
Name: translated.Name,
}
var host corev1.Secret
if err = s.HostClient.Get(ctx, translatedKey, &host); err != nil {
if apierrors.IsNotFound(err) {
err = s.HostClient.Create(ctx, translated)
// for simplicity's sake, we don't check for conflict errors. The existing object will get
// picked up on in the next re-enqueue
return reconcile.Result{
Requeue: true,
}, fmt.Errorf("unable to create host secret %s/%s for virtual secret %s/%s: %w",
translated.Namespace, translated.Name, req.Namespace, req.Name, err)
}
return reconcile.Result{Requeue: true}, fmt.Errorf("unable to get host secret %s/%s: %w", translated.Namespace, translated.Name, err)
}
// we are going to use the host in order to avoid conflicts on update
host.Data = translated.Data
if host.Labels == nil {
host.Labels = make(map[string]string, len(translated.Labels))
}
// we don't want to override labels made on the host cluster by other applications
// but we do need to make sure the labels that the kubelet uses to track host cluster values
// are being tracked appropriately
for key, value := range translated.Labels {
host.Labels[key] = value
}
if err = s.HostClient.Update(ctx, &host); err != nil {
return reconcile.Result{
Requeue: true,
}, fmt.Errorf("unable to update host secret %s/%s for virtual secret %s/%s: %w",
translated.Namespace, translated.Name, req.Namespace, req.Name, err)
}
return reconcile.Result{}, nil
}
// isWatching is a utility method to determine if a key is in objs without the caller needing
// to handle mutex lock/unlock.
func (s *SecretSyncer) isWatching(key types.NamespacedName) bool {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.objs.Has(key)
}
// AddResource adds a given resource to the list of resources that will be synced. Safe to call multiple times for the
// same resource.
func (s *SecretSyncer) AddResource(ctx context.Context, namespace, name string) error {
objKey := types.NamespacedName{
Namespace: namespace,
Name: name,
}
// if we already sync this object, no need to writelock/add it
if s.isWatching(objKey) {
return nil
}
// lock in write mode since we are now adding the key
s.mutex.Lock()
if s.objs == nil {
s.objs = sets.Set[types.NamespacedName]{}
}
s.objs = s.objs.Insert(objKey)
s.mutex.Unlock()
_, err := s.Reconcile(ctx, reconcile.Request{
NamespacedName: objKey,
})
if err != nil {
return fmt.Errorf("unable to reconcile new object %s/%s: %w", objKey.Namespace, objKey.Name, err)
}
return nil
}
// RemoveResource removes a given resource from the list of resources that will be synced. Safe to call for an already
// removed resource.
func (s *SecretSyncer) RemoveResource(ctx context.Context, namespace, name string) error {
objKey := types.NamespacedName{
Namespace: namespace,
Name: name,
}
// if we don't sync this object, no need to writelock/add it
if !s.isWatching(objKey) {
return nil
}
// lock in write mode since we are now adding the key
if err := retry.OnError(controller.Backoff, func(err error) bool {
return err != nil
}, func() error {
return s.removeHostSecret(ctx, namespace, name)
}); err != nil {
return fmt.Errorf("unable to remove secret: %w", err)
}
s.mutex.Lock()
if s.objs == nil {
s.objs = sets.Set[types.NamespacedName]{}
}
s.objs = s.objs.Delete(objKey)
s.mutex.Unlock()
return nil
}
func (s *SecretSyncer) removeHostSecret(ctx context.Context, virtualNamespace, virtualName string) error {
var vSecret corev1.Secret
err := s.VirtualClient.Get(ctx, types.NamespacedName{
Namespace: virtualNamespace,
Name: virtualName,
}, &vSecret)
if err != nil {
return fmt.Errorf("unable to get virtual secret %s/%s: %w", virtualNamespace, virtualName, err)
}
translated, err := s.TranslateFunc(&vSecret)
if err != nil {
return fmt.Errorf("unable to translate virtual secret: %s/%s: %w", virtualNamespace, virtualName, err)
}
return s.HostClient.Delete(ctx, translated)
}

View File

@@ -0,0 +1,136 @@
package controller
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
ctrl "sigs.k8s.io/controller-runtime"
ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/rancher/k3k/k3k-kubelet/translate"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
)
const (
serviceSyncerController = "service-syncer-controller"
serviceFinalizerName = "service.k3k.io/finalizer"
)
type ServiceReconciler struct {
clusterName string
clusterNamespace string
virtualClient ctrlruntimeclient.Client
hostClient ctrlruntimeclient.Client
Scheme *runtime.Scheme
HostScheme *runtime.Scheme
Translator translate.ToHostTranslator
}
// AddServiceSyncer adds service syncer controller to the manager of the virtual cluster
func AddServiceSyncer(ctx context.Context, virtMgr, hostMgr manager.Manager, clusterName, clusterNamespace string) error {
translator := translate.ToHostTranslator{
ClusterName: clusterName,
ClusterNamespace: clusterNamespace,
}
reconciler := ServiceReconciler{
clusterName: clusterName,
clusterNamespace: clusterNamespace,
virtualClient: virtMgr.GetClient(),
hostClient: hostMgr.GetClient(),
Scheme: virtMgr.GetScheme(),
HostScheme: hostMgr.GetScheme(),
Translator: translator,
}
return ctrl.NewControllerManagedBy(virtMgr).
Named(serviceSyncerController).
For(&v1.Service{}).
Complete(&reconciler)
}
func (r *ServiceReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
log := ctrl.LoggerFrom(ctx).WithValues("cluster", r.clusterName, "clusterNamespace", r.clusterNamespace)
ctx = ctrl.LoggerInto(ctx, log)
if req.Name == "kubernetes" || req.Name == "kube-dns" {
return reconcile.Result{}, nil
}
var (
virtService v1.Service
cluster v1alpha1.Cluster
)
if err := r.hostClient.Get(ctx, types.NamespacedName{Name: r.clusterName, Namespace: r.clusterNamespace}, &cluster); err != nil {
return reconcile.Result{}, err
}
if err := r.virtualClient.Get(ctx, req.NamespacedName, &virtService); err != nil {
return reconcile.Result{}, ctrlruntimeclient.IgnoreNotFound(err)
}
syncedService := r.service(&virtService)
if err := controllerutil.SetControllerReference(&cluster, syncedService, r.HostScheme); err != nil {
return reconcile.Result{}, err
}
// handle deletion
if !virtService.DeletionTimestamp.IsZero() {
// deleting the synced service if exists
if err := r.hostClient.Delete(ctx, syncedService); err != nil {
return reconcile.Result{}, ctrlruntimeclient.IgnoreNotFound(err)
}
// remove the finalizer after cleaning up the synced service
if controllerutil.ContainsFinalizer(&virtService, serviceFinalizerName) {
controllerutil.RemoveFinalizer(&virtService, serviceFinalizerName)
if err := r.virtualClient.Update(ctx, &virtService); err != nil {
return reconcile.Result{}, err
}
}
return reconcile.Result{}, nil
}
// Add finalizer if it does not exist
if !controllerutil.ContainsFinalizer(&virtService, serviceFinalizerName) {
controllerutil.AddFinalizer(&virtService, serviceFinalizerName)
if err := r.virtualClient.Update(ctx, &virtService); err != nil {
return reconcile.Result{}, err
}
}
// create or update the service on host
var hostService v1.Service
if err := r.hostClient.Get(ctx, types.NamespacedName{Name: syncedService.Name, Namespace: r.clusterNamespace}, &hostService); err != nil {
if apierrors.IsNotFound(err) {
log.Info("creating the service for the first time on the host cluster")
return reconcile.Result{}, r.hostClient.Create(ctx, syncedService)
}
return reconcile.Result{}, err
}
log.Info("updating service on the host cluster")
return reconcile.Result{}, r.hostClient.Update(ctx, syncedService)
}
func (s *ServiceReconciler) service(obj *v1.Service) *v1.Service {
hostService := obj.DeepCopy()
s.Translator.TranslateTo(hostService)
// don't sync finalizers to the host
return hostService
}

View File

@@ -0,0 +1,173 @@
package webhook
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/manager"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrl "sigs.k8s.io/controller-runtime"
ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/rancher/k3k/pkg/controller/cluster/agent"
"github.com/rancher/k3k/pkg/log"
)
const (
webhookName = "podmutator.k3k.io"
webhookTimeout = int32(10)
webhookPath = "/mutate--v1-pod"
FieldpathField = "k3k.io/fieldpath"
)
type webhookHandler struct {
client ctrlruntimeclient.Client
scheme *runtime.Scheme
serviceName string
clusterName string
clusterNamespace string
logger *log.Logger
webhookPort int
}
// AddPodMutatorWebhook will add a mutator webhook to the virtual cluster to
// modify the nodeName of the created pods with the name of the virtual kubelet node name
// as well as remove any status fields of the downward apis env fields
func AddPodMutatorWebhook(ctx context.Context, mgr manager.Manager, hostClient ctrlruntimeclient.Client, clusterName, clusterNamespace, serviceName string, logger *log.Logger, webhookPort int) error {
handler := webhookHandler{
client: mgr.GetClient(),
scheme: mgr.GetScheme(),
logger: logger,
serviceName: serviceName,
clusterName: clusterName,
clusterNamespace: clusterNamespace,
webhookPort: webhookPort,
}
// create mutator webhook configuration to the cluster
config, err := handler.configuration(ctx, hostClient)
if err != nil {
return err
}
if err := handler.client.Create(ctx, config); err != nil {
if !apierrors.IsAlreadyExists(err) {
return err
}
}
// register webhook with the manager
return ctrl.NewWebhookManagedBy(mgr).For(&v1.Pod{}).WithDefaulter(&handler).Complete()
}
func (w *webhookHandler) Default(ctx context.Context, obj runtime.Object) error {
pod, ok := obj.(*v1.Pod)
if !ok {
return fmt.Errorf("invalid request: object was type %t not cluster", obj)
}
w.logger.Infow("mutator webhook request", "Pod", pod.Name, "Namespace", pod.Namespace)
// look for status.* fields in the env
if pod.Annotations == nil {
pod.Annotations = make(map[string]string)
}
for i, container := range pod.Spec.Containers {
for j, env := range container.Env {
if env.ValueFrom == nil || env.ValueFrom.FieldRef == nil {
continue
}
fieldPath := env.ValueFrom.FieldRef.FieldPath
if strings.Contains(fieldPath, "status.") {
annotationKey := fmt.Sprintf("%s_%d_%s", FieldpathField, i, env.Name)
pod.Annotations[annotationKey] = fieldPath
pod.Spec.Containers[i].Env = removeEnv(pod.Spec.Containers[i].Env, j)
}
}
}
return nil
}
func (w *webhookHandler) configuration(ctx context.Context, hostClient ctrlruntimeclient.Client) (*admissionregistrationv1.MutatingWebhookConfiguration, error) {
w.logger.Infow("extracting webhook tls from host cluster")
var webhookTLSSecret v1.Secret
if err := hostClient.Get(ctx, types.NamespacedName{Name: agent.WebhookSecretName(w.clusterName), Namespace: w.clusterNamespace}, &webhookTLSSecret); err != nil {
return nil, err
}
caBundle, ok := webhookTLSSecret.Data["ca.crt"]
if !ok {
return nil, errors.New("webhook CABundle does not exist in secret")
}
webhookURL := fmt.Sprintf("https://%s:%d%s", w.serviceName, w.webhookPort, webhookPath)
return &admissionregistrationv1.MutatingWebhookConfiguration{
TypeMeta: metav1.TypeMeta{
APIVersion: "admissionregistration.k8s.io/v1",
Kind: "MutatingWebhookConfiguration",
},
ObjectMeta: metav1.ObjectMeta{
Name: webhookName + "-configuration",
},
Webhooks: []admissionregistrationv1.MutatingWebhook{
{
Name: webhookName,
AdmissionReviewVersions: []string{"v1"},
SideEffects: ptr.To(admissionregistrationv1.SideEffectClassNone),
TimeoutSeconds: ptr.To(webhookTimeout),
ClientConfig: admissionregistrationv1.WebhookClientConfig{
URL: ptr.To(webhookURL),
CABundle: caBundle,
},
Rules: []admissionregistrationv1.RuleWithOperations{
{
Operations: []admissionregistrationv1.OperationType{
"CREATE",
},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{""},
APIVersions: []string{"v1"},
Resources: []string{"pods"},
Scope: ptr.To(admissionregistrationv1.NamespacedScope),
},
},
},
},
},
}, nil
}
func removeEnv(envs []v1.EnvVar, i int) []v1.EnvVar {
envs[i] = envs[len(envs)-1]
return envs[:len(envs)-1]
}
func ParseFieldPathAnnotationKey(annotationKey string) (int, string, error) {
s := strings.SplitN(annotationKey, "_", 3)
if len(s) != 3 {
return -1, "", errors.New("fieldpath annotation is not set correctly")
}
containerIndex, err := strconv.Atoi(s[1])
if err != nil {
return -1, "", err
}
envName := s[2]
return containerIndex, envName, nil
}

449
k3k-kubelet/kubelet.go Normal file
View File

@@ -0,0 +1,449 @@
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"net/http"
"os"
"time"
"github.com/go-logr/zapr"
"github.com/virtual-kubelet/virtual-kubelet/log"
"github.com/virtual-kubelet/virtual-kubelet/node"
"github.com/virtual-kubelet/virtual-kubelet/node/nodeutil"
"go.uber.org/zap"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/webhook"
certutil "github.com/rancher/dynamiclistener/cert"
v1 "k8s.io/api/core/v1"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
ctrl "sigs.k8s.io/controller-runtime"
ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
ctrlserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
k3kkubeletcontroller "github.com/rancher/k3k/k3k-kubelet/controller"
k3kwebhook "github.com/rancher/k3k/k3k-kubelet/controller/webhook"
"github.com/rancher/k3k/k3k-kubelet/provider"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
"github.com/rancher/k3k/pkg/controller"
"github.com/rancher/k3k/pkg/controller/certs"
"github.com/rancher/k3k/pkg/controller/cluster/server"
"github.com/rancher/k3k/pkg/controller/cluster/server/bootstrap"
k3klog "github.com/rancher/k3k/pkg/log"
)
var (
baseScheme = runtime.NewScheme()
k3kKubeletName = "k3k-kubelet"
)
func init() {
_ = clientgoscheme.AddToScheme(baseScheme)
_ = v1alpha1.AddToScheme(baseScheme)
}
type kubelet struct {
virtualCluster v1alpha1.Cluster
name string
port int
hostConfig *rest.Config
virtConfig *rest.Config
agentIP string
dnsIP string
hostClient ctrlruntimeclient.Client
virtClient kubernetes.Interface
hostMgr manager.Manager
virtualMgr manager.Manager
node *nodeutil.Node
logger *k3klog.Logger
token string
}
func newKubelet(ctx context.Context, c *config, logger *k3klog.Logger) (*kubelet, error) {
hostConfig, err := clientcmd.BuildConfigFromFlags("", c.HostKubeconfig)
if err != nil {
return nil, err
}
hostClient, err := ctrlruntimeclient.New(hostConfig, ctrlruntimeclient.Options{
Scheme: baseScheme,
})
if err != nil {
return nil, err
}
virtConfig, err := virtRestConfig(ctx, c.VirtKubeconfig, hostClient, c.ClusterName, c.ClusterNamespace, c.Token, logger)
if err != nil {
return nil, err
}
virtClient, err := kubernetes.NewForConfig(virtConfig)
if err != nil {
return nil, err
}
ctrl.SetLogger(zapr.NewLogger(logger.Desugar().WithOptions(zap.AddCallerSkip(1))))
hostMetricsBindAddress := ":8083"
virtualMetricsBindAddress := ":8084"
if c.MirrorHostNodes {
hostMetricsBindAddress = "0"
virtualMetricsBindAddress = "0"
}
hostMgr, err := ctrl.NewManager(hostConfig, manager.Options{
Scheme: baseScheme,
LeaderElection: true,
LeaderElectionNamespace: c.ClusterNamespace,
LeaderElectionID: c.ClusterName,
Metrics: ctrlserver.Options{
BindAddress: hostMetricsBindAddress,
},
Cache: cache.Options{
DefaultNamespaces: map[string]cache.Config{
c.ClusterNamespace: {},
},
},
})
if err != nil {
return nil, errors.New("unable to create controller-runtime mgr for host cluster: " + err.Error())
}
// virtual client will only use core types (for now), no need to add anything other than the basics
virtualScheme := runtime.NewScheme()
if err := clientgoscheme.AddToScheme(virtualScheme); err != nil {
return nil, errors.New("unable to add client go types to virtual cluster scheme: " + err.Error())
}
webhookServer := webhook.NewServer(webhook.Options{
CertDir: "/opt/rancher/k3k-webhook",
Port: c.WebhookPort,
})
virtualMgr, err := ctrl.NewManager(virtConfig, manager.Options{
Scheme: virtualScheme,
WebhookServer: webhookServer,
LeaderElection: true,
LeaderElectionNamespace: "kube-system",
LeaderElectionID: c.ClusterName,
Metrics: ctrlserver.Options{
BindAddress: virtualMetricsBindAddress,
},
})
if err != nil {
return nil, errors.New("unable to create controller-runtime mgr for virtual cluster: " + err.Error())
}
logger.Info("adding pod mutator webhook")
if err := k3kwebhook.AddPodMutatorWebhook(ctx, virtualMgr, hostClient, c.ClusterName, c.ClusterNamespace, c.ServiceName, logger, c.WebhookPort); err != nil {
return nil, errors.New("unable to add pod mutator webhook for virtual cluster: " + err.Error())
}
logger.Info("adding service syncer controller")
if err := k3kkubeletcontroller.AddServiceSyncer(ctx, virtualMgr, hostMgr, c.ClusterName, c.ClusterNamespace); err != nil {
return nil, errors.New("failed to add service syncer controller: " + err.Error())
}
logger.Info("adding pvc syncer controller")
if err := k3kkubeletcontroller.AddPVCSyncer(ctx, virtualMgr, hostMgr, c.ClusterName, c.ClusterNamespace); err != nil {
return nil, errors.New("failed to add pvc syncer controller: " + err.Error())
}
logger.Info("adding pod pvc controller")
if err := k3kkubeletcontroller.AddPodPVCController(ctx, virtualMgr, hostMgr, c.ClusterName, c.ClusterNamespace); err != nil {
return nil, errors.New("failed to add pod pvc controller: " + err.Error())
}
logger.Info("adding priorityclass controller")
if err := k3kkubeletcontroller.AddPriorityClassReconciler(ctx, virtualMgr, hostMgr, c.ClusterName, c.ClusterNamespace); err != nil {
return nil, errors.New("failed to add priorityclass controller: " + err.Error())
}
clusterIP, err := clusterIP(ctx, c.ServiceName, c.ClusterNamespace, hostClient)
if err != nil {
return nil, errors.New("failed to extract the clusterIP for the server service: " + err.Error())
}
// get the cluster's DNS IP to be injected to pods
var dnsService v1.Service
dnsName := controller.SafeConcatNameWithPrefix(c.ClusterName, "kube-dns")
if err := hostClient.Get(ctx, types.NamespacedName{Name: dnsName, Namespace: c.ClusterNamespace}, &dnsService); err != nil {
return nil, errors.New("failed to get the DNS service for the cluster: " + err.Error())
}
var virtualCluster v1alpha1.Cluster
if err := hostClient.Get(ctx, types.NamespacedName{Name: c.ClusterName, Namespace: c.ClusterNamespace}, &virtualCluster); err != nil {
return nil, errors.New("failed to get virtualCluster spec: " + err.Error())
}
return &kubelet{
virtualCluster: virtualCluster,
name: c.AgentHostname,
hostConfig: hostConfig,
hostClient: hostClient,
virtConfig: virtConfig,
virtClient: virtClient,
hostMgr: hostMgr,
virtualMgr: virtualMgr,
agentIP: clusterIP,
logger: logger.Named(k3kKubeletName),
token: c.Token,
dnsIP: dnsService.Spec.ClusterIP,
port: c.KubeletPort,
}, nil
}
func clusterIP(ctx context.Context, serviceName, clusterNamespace string, hostClient ctrlruntimeclient.Client) (string, error) {
var service v1.Service
serviceKey := types.NamespacedName{
Namespace: clusterNamespace,
Name: serviceName,
}
if err := hostClient.Get(ctx, serviceKey, &service); err != nil {
return "", err
}
return service.Spec.ClusterIP, nil
}
func (k *kubelet) registerNode(ctx context.Context, agentIP string, cfg config) error {
providerFunc := k.newProviderFunc(cfg)
nodeOpts := k.nodeOpts(ctx, cfg.KubeletPort, cfg.ClusterNamespace, cfg.ClusterName, cfg.AgentHostname, agentIP)
var err error
k.node, err = nodeutil.NewNode(k.name, providerFunc, nodeutil.WithClient(k.virtClient), nodeOpts)
if err != nil {
return errors.New("unable to start kubelet: " + err.Error())
}
return nil
}
func (k *kubelet) start(ctx context.Context) {
// any one of the following 3 tasks (host manager, virtual manager, node) crashing will stop the
// program, and all 3 of them block on start, so we start them here in go-routines
go func() {
err := k.hostMgr.Start(ctx)
if err != nil {
k.logger.Fatalw("host manager stopped", zap.Error(err))
}
}()
go func() {
err := k.virtualMgr.Start(ctx)
if err != nil {
k.logger.Fatalw("virtual manager stopped", zap.Error(err))
}
}()
// run the node async so that we can wait for it to be ready in another call
go func() {
ctx = log.WithLogger(ctx, k.logger)
if err := k.node.Run(ctx); err != nil {
k.logger.Fatalw("node errored when running", zap.Error(err))
}
}()
if err := k.node.WaitReady(context.Background(), time.Minute*1); err != nil {
k.logger.Fatalw("node was not ready within timeout of 1 minute", zap.Error(err))
}
<-k.node.Done()
if err := k.node.Err(); err != nil {
k.logger.Fatalw("node stopped with an error", zap.Error(err))
}
k.logger.Info("node exited successfully")
}
func (k *kubelet) newProviderFunc(cfg config) nodeutil.NewProviderFunc {
return func(pc nodeutil.ProviderConfig) (nodeutil.Provider, node.NodeProvider, error) {
utilProvider, err := provider.New(*k.hostConfig, k.hostMgr, k.virtualMgr, k.logger, cfg.ClusterNamespace, cfg.ClusterName, cfg.ServerIP, k.dnsIP)
if err != nil {
return nil, nil, errors.New("unable to make nodeutil provider: " + err.Error())
}
provider.ConfigureNode(k.logger, pc.Node, cfg.AgentHostname, k.port, k.agentIP, utilProvider.CoreClient, utilProvider.VirtualClient, k.virtualCluster, cfg.Version, cfg.MirrorHostNodes)
return utilProvider, &provider.Node{}, nil
}
}
func (k *kubelet) nodeOpts(ctx context.Context, srvPort int, namespace, name, hostname, agentIP string) nodeutil.NodeOpt {
return func(c *nodeutil.NodeConfig) error {
c.HTTPListenAddr = fmt.Sprintf(":%d", srvPort)
// set up the routes
mux := http.NewServeMux()
if err := nodeutil.AttachProviderRoutes(mux)(c); err != nil {
return errors.New("unable to attach routes: " + err.Error())
}
c.Handler = mux
tlsConfig, err := loadTLSConfig(ctx, k.hostClient, name, namespace, k.name, hostname, k.token, agentIP)
if err != nil {
return errors.New("unable to get tls config: " + err.Error())
}
c.TLSConfig = tlsConfig
return nil
}
}
func virtRestConfig(ctx context.Context, virtualConfigPath string, hostClient ctrlruntimeclient.Client, clusterName, clusterNamespace, token string, logger *k3klog.Logger) (*rest.Config, error) {
if virtualConfigPath != "" {
return clientcmd.BuildConfigFromFlags("", virtualConfigPath)
}
// virtual kubeconfig file is empty, trying to fetch the k3k cluster kubeconfig
var cluster v1alpha1.Cluster
if err := hostClient.Get(ctx, types.NamespacedName{Namespace: clusterNamespace, Name: clusterName}, &cluster); err != nil {
return nil, err
}
endpoint := server.ServiceName(cluster.Name) + "." + cluster.Namespace
var b *bootstrap.ControlRuntimeBootstrap
if err := retry.OnError(controller.Backoff, func(err error) bool {
return err != nil
}, func() error {
var err error
b, err = bootstrap.DecodedBootstrap(token, endpoint)
logger.Infow("decoded bootstrap", zap.Error(err))
return err
}); err != nil {
return nil, errors.New("unable to decode bootstrap: " + err.Error())
}
adminCert, adminKey, err := certs.CreateClientCertKey(
controller.AdminCommonName,
[]string{user.SystemPrivilegedGroup},
nil, []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
time.Hour*24*time.Duration(356),
b.ClientCA.Content,
b.ClientCAKey.Content,
)
if err != nil {
return nil, err
}
url := "https://" + server.ServiceName(cluster.Name)
kubeconfigData, err := kubeconfigBytes(url, []byte(b.ServerCA.Content), adminCert, adminKey)
if err != nil {
return nil, err
}
return clientcmd.RESTConfigFromKubeConfig(kubeconfigData)
}
func kubeconfigBytes(url string, serverCA, clientCert, clientKey []byte) ([]byte, error) {
config := clientcmdapi.NewConfig()
cluster := clientcmdapi.NewCluster()
cluster.CertificateAuthorityData = serverCA
cluster.Server = url
authInfo := clientcmdapi.NewAuthInfo()
authInfo.ClientCertificateData = clientCert
authInfo.ClientKeyData = clientKey
context := clientcmdapi.NewContext()
context.AuthInfo = "default"
context.Cluster = "default"
config.Clusters["default"] = cluster
config.AuthInfos["default"] = authInfo
config.Contexts["default"] = context
config.CurrentContext = "default"
return clientcmd.Write(*config)
}
func loadTLSConfig(ctx context.Context, hostClient ctrlruntimeclient.Client, clusterName, clusterNamespace, nodeName, hostname, token, agentIP string) (*tls.Config, error) {
var (
cluster v1alpha1.Cluster
b *bootstrap.ControlRuntimeBootstrap
)
if err := hostClient.Get(ctx, types.NamespacedName{Name: clusterName, Namespace: clusterNamespace}, &cluster); err != nil {
return nil, err
}
endpoint := fmt.Sprintf("%s.%s", server.ServiceName(cluster.Name), cluster.Namespace)
if err := retry.OnError(controller.Backoff, func(err error) bool {
return err != nil
}, func() error {
var err error
b, err = bootstrap.DecodedBootstrap(token, endpoint)
return err
}); err != nil {
return nil, errors.New("unable to decode bootstrap: " + err.Error())
}
// POD IP
podIP := net.ParseIP(os.Getenv("POD_IP"))
ip := net.ParseIP(agentIP)
altNames := certutil.AltNames{
DNSNames: []string{hostname},
IPs: []net.IP{ip, podIP},
}
cert, key, err := certs.CreateClientCertKey(nodeName, nil, &altNames, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 0, b.ServerCA.Content, b.ServerCAKey.Content)
if err != nil {
return nil, errors.New("unable to get cert and key: " + err.Error())
}
clientCert, err := tls.X509KeyPair(cert, key)
if err != nil {
return nil, errors.New("unable to get key pair: " + err.Error())
}
// create rootCA CertPool
certs, err := certutil.ParseCertsPEM([]byte(b.ServerCA.Content))
if err != nil {
return nil, errors.New("unable to create ca certs: " + err.Error())
}
if len(certs) < 1 {
return nil, errors.New("ca cert is not parsed correctly")
}
pool := x509.NewCertPool()
pool.AddCert(certs[0])
return &tls.Config{
RootCAs: pool,
Certificates: []tls.Certificate{clientCert},
}, nil
}

127
k3k-kubelet/main.go Normal file
View File

@@ -0,0 +1,127 @@
package main
import (
"context"
"errors"
"fmt"
"os"
"strings"
"github.com/go-logr/zapr"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"go.uber.org/zap"
ctrlruntimelog "sigs.k8s.io/controller-runtime/pkg/log"
"github.com/rancher/k3k/pkg/log"
)
var (
configFile string
cfg config
logger *log.Logger
debug bool
)
func main() {
rootCmd := &cobra.Command{
Use: "k3k-kubelet",
Short: "virtual kubelet implementation k3k",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if err := InitializeConfig(cmd); err != nil {
return err
}
logger = log.New(debug)
ctrlruntimelog.SetLogger(zapr.NewLogger(logger.Desugar().WithOptions(zap.AddCallerSkip(1))))
return nil
},
RunE: run,
}
rootCmd.PersistentFlags().StringVar(&cfg.ClusterName, "cluster-name", "", "Name of the k3k cluster")
rootCmd.PersistentFlags().StringVar(&cfg.ClusterNamespace, "cluster-namespace", "", "Namespace of the k3k cluster")
rootCmd.PersistentFlags().StringVar(&cfg.Token, "token", "", "K3S token of the k3k cluster")
rootCmd.PersistentFlags().StringVar(&cfg.HostKubeconfig, "host-kubeconfig", "", "Path to the host kubeconfig, if empty then virtual-kubelet will use incluster config")
rootCmd.PersistentFlags().StringVar(&cfg.VirtKubeconfig, "virt-kubeconfig", "", "Path to the k3k cluster kubeconfig, if empty then virtual-kubelet will create its own config from k3k cluster")
rootCmd.PersistentFlags().IntVar(&cfg.KubeletPort, "kubelet-port", 0, "kubelet API port number")
rootCmd.PersistentFlags().IntVar(&cfg.WebhookPort, "webhook-port", 0, "Webhook port number")
rootCmd.PersistentFlags().StringVar(&cfg.ServiceName, "service-name", "", "The service name deployed by the k3k controller")
rootCmd.PersistentFlags().StringVar(&cfg.AgentHostname, "agent-hostname", "", "Agent Hostname used for TLS SAN for the kubelet server")
rootCmd.PersistentFlags().StringVar(&cfg.ServerIP, "server-ip", "", "Server IP used for registering the virtual kubelet to the cluster")
rootCmd.PersistentFlags().StringVar(&cfg.Version, "version", "", "Version of kubernetes server")
rootCmd.PersistentFlags().StringVar(&configFile, "config", "/opt/rancher/k3k/config.yaml", "Path to k3k-kubelet config file")
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug logging")
rootCmd.PersistentFlags().BoolVar(&cfg.MirrorHostNodes, "mirror-host-nodes", false, "Mirror real node objects from host cluster")
if err := rootCmd.Execute(); err != nil {
logrus.Fatal(err)
}
}
func run(cmd *cobra.Command, args []string) error {
ctx := context.Background()
if err := cfg.validate(); err != nil {
return fmt.Errorf("failed to validate config: %w", err)
}
k, err := newKubelet(ctx, &cfg, logger)
if err != nil {
return fmt.Errorf("failed to create new virtual kubelet instance: %w", err)
}
if err := k.registerNode(ctx, k.agentIP, cfg); err != nil {
return fmt.Errorf("failed to register new node: %w", err)
}
k.start(ctx)
return nil
}
// InitializeConfig sets up viper to read from config file, environment variables, and flags.
// It uses a `flatcase` convention for viper keys to match the (lowercased) config file keys,
// while flags remain in kebab-case.
func InitializeConfig(cmd *cobra.Command) error {
var err error
// Bind every cobra flag to a viper key.
// The viper key will be the flag name with dashes removed (flatcase).
// e.g. "cluster-name" becomes "clustername"
cmd.Flags().VisitAll(func(f *pflag.Flag) {
configName := strings.ReplaceAll(f.Name, "-", "")
envName := strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_"))
err = errors.Join(err, viper.BindPFlag(configName, f))
err = errors.Join(err, viper.BindEnv(configName, envName))
})
if err != nil {
return err
}
configFile = viper.GetString("config")
viper.SetConfigFile(configFile)
if err := viper.ReadInConfig(); err != nil {
var notFoundErr viper.ConfigFileNotFoundError
if errors.As(err, &notFoundErr) || errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("no config file found: %w", err)
} else {
return fmt.Errorf("failed to read config file: %w", err)
}
}
// Unmarshal all configuration into the global cfg struct.
// Viper correctly handles the precedence of flags > env > config.
if err := viper.Unmarshal(&cfg); err != nil {
return fmt.Errorf("failed to unmarshal config: %w", err)
}
// Separately get the debug flag, as it's not part of the main config struct.
debug = viper.GetBool("debug")
return nil
}

View File

@@ -0,0 +1,204 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the Apache 2.0 license.
See https://github.com/virtual-kubelet/azure-aci/tree/master/pkg/metrics/collectors
*/
package collectors
import (
"time"
compbasemetrics "k8s.io/component-base/metrics"
stats "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
)
// defining metrics
var (
nodeCPUUsageDesc = compbasemetrics.NewDesc("node_cpu_usage_seconds_total",
"Cumulative cpu time consumed by the node in core-seconds",
nil,
nil,
compbasemetrics.ALPHA,
"")
nodeMemoryUsageDesc = compbasemetrics.NewDesc("node_memory_working_set_bytes",
"Current working set of the node in bytes",
nil,
nil,
compbasemetrics.ALPHA,
"")
containerCPUUsageDesc = compbasemetrics.NewDesc("container_cpu_usage_seconds_total",
"Cumulative cpu time consumed by the container in core-seconds",
[]string{"container", "pod", "namespace"},
nil,
compbasemetrics.ALPHA,
"")
containerMemoryUsageDesc = compbasemetrics.NewDesc("container_memory_working_set_bytes",
"Current working set of the container in bytes",
[]string{"container", "pod", "namespace"},
nil,
compbasemetrics.ALPHA,
"")
podCPUUsageDesc = compbasemetrics.NewDesc("pod_cpu_usage_seconds_total",
"Cumulative cpu time consumed by the pod in core-seconds",
[]string{"pod", "namespace"},
nil,
compbasemetrics.ALPHA,
"")
podMemoryUsageDesc = compbasemetrics.NewDesc("pod_memory_working_set_bytes",
"Current working set of the pod in bytes",
[]string{"pod", "namespace"},
nil,
compbasemetrics.ALPHA,
"")
resourceScrapeResultDesc = compbasemetrics.NewDesc("scrape_error",
"1 if there was an error while getting container metrics, 0 otherwise",
nil,
nil,
compbasemetrics.ALPHA,
"")
containerStartTimeDesc = compbasemetrics.NewDesc("container_start_time_seconds",
"Start time of the container since unix epoch in seconds",
[]string{"container", "pod", "namespace"},
nil,
compbasemetrics.ALPHA,
"")
)
// NewResourceMetricsCollector returns a metrics.StableCollector which exports resource metrics
func NewKubeletResourceMetricsCollector(podStats *stats.Summary) compbasemetrics.StableCollector {
return &resourceMetricsCollector{
providerPodStats: podStats,
}
}
type resourceMetricsCollector struct {
compbasemetrics.BaseStableCollector
providerPodStats *stats.Summary
}
// Check if resourceMetricsCollector implements necessary interface
var _ compbasemetrics.StableCollector = &resourceMetricsCollector{}
// DescribeWithStability implements compbasemetrics.StableCollector
func (rc *resourceMetricsCollector) DescribeWithStability(ch chan<- *compbasemetrics.Desc) {
descs := []*compbasemetrics.Desc{
nodeCPUUsageDesc,
nodeMemoryUsageDesc,
containerStartTimeDesc,
containerCPUUsageDesc,
containerMemoryUsageDesc,
podCPUUsageDesc,
podMemoryUsageDesc,
resourceScrapeResultDesc,
}
for _, desc := range descs {
ch <- desc
}
}
// CollectWithStability implements compbasemetrics.StableCollector
// Since new containers are frequently created and removed, using the Gauge would
// leak metric collectors for containers or pods that no longer exist. Instead, implement
// custom collector in a way that only collects metrics for active containers.
func (rc *resourceMetricsCollector) CollectWithStability(ch chan<- compbasemetrics.Metric) {
var errorCount float64
defer func() {
ch <- compbasemetrics.NewLazyConstMetric(resourceScrapeResultDesc, compbasemetrics.GaugeValue, errorCount)
}()
statsSummary := *rc.providerPodStats
rc.collectNodeCPUMetrics(ch, statsSummary.Node)
rc.collectNodeMemoryMetrics(ch, statsSummary.Node)
for _, pod := range statsSummary.Pods {
for _, container := range pod.Containers {
rc.collectContainerStartTime(ch, pod, container)
rc.collectContainerCPUMetrics(ch, pod, container)
rc.collectContainerMemoryMetrics(ch, pod, container)
}
rc.collectPodCPUMetrics(ch, pod)
rc.collectPodMemoryMetrics(ch, pod)
}
}
// implement collector methods and validate that correct data is used
func (rc *resourceMetricsCollector) collectNodeCPUMetrics(ch chan<- compbasemetrics.Metric, s stats.NodeStats) {
if s.CPU == nil || s.CPU.UsageCoreNanoSeconds == nil {
return
}
ch <- compbasemetrics.NewLazyMetricWithTimestamp(s.CPU.Time.Time,
compbasemetrics.NewLazyConstMetric(nodeCPUUsageDesc, compbasemetrics.CounterValue, float64(*s.CPU.UsageCoreNanoSeconds)/float64(time.Second)))
}
func (rc *resourceMetricsCollector) collectNodeMemoryMetrics(ch chan<- compbasemetrics.Metric, s stats.NodeStats) {
if s.Memory == nil || s.Memory.WorkingSetBytes == nil {
return
}
ch <- compbasemetrics.NewLazyMetricWithTimestamp(s.Memory.Time.Time,
compbasemetrics.NewLazyConstMetric(nodeMemoryUsageDesc, compbasemetrics.GaugeValue, float64(*s.Memory.WorkingSetBytes)))
}
func (rc *resourceMetricsCollector) collectContainerStartTime(ch chan<- compbasemetrics.Metric, pod stats.PodStats, s stats.ContainerStats) {
if s.StartTime.Unix() <= 0 {
return
}
ch <- compbasemetrics.NewLazyMetricWithTimestamp(s.StartTime.Time,
compbasemetrics.NewLazyConstMetric(containerStartTimeDesc, compbasemetrics.GaugeValue, float64(s.StartTime.UnixNano())/float64(time.Second), s.Name, pod.PodRef.Name, pod.PodRef.Namespace))
}
func (rc *resourceMetricsCollector) collectContainerCPUMetrics(ch chan<- compbasemetrics.Metric, pod stats.PodStats, s stats.ContainerStats) {
if s.CPU == nil || s.CPU.UsageCoreNanoSeconds == nil {
return
}
ch <- compbasemetrics.NewLazyMetricWithTimestamp(s.CPU.Time.Time,
compbasemetrics.NewLazyConstMetric(containerCPUUsageDesc, compbasemetrics.CounterValue,
float64(*s.CPU.UsageCoreNanoSeconds)/float64(time.Second), s.Name, pod.PodRef.Name, pod.PodRef.Namespace))
}
func (rc *resourceMetricsCollector) collectContainerMemoryMetrics(ch chan<- compbasemetrics.Metric, pod stats.PodStats, s stats.ContainerStats) {
if s.Memory == nil || s.Memory.WorkingSetBytes == nil {
return
}
ch <- compbasemetrics.NewLazyMetricWithTimestamp(s.Memory.Time.Time,
compbasemetrics.NewLazyConstMetric(containerMemoryUsageDesc, compbasemetrics.GaugeValue,
float64(*s.Memory.WorkingSetBytes), s.Name, pod.PodRef.Name, pod.PodRef.Namespace))
}
func (rc *resourceMetricsCollector) collectPodCPUMetrics(ch chan<- compbasemetrics.Metric, pod stats.PodStats) {
if pod.CPU == nil || pod.CPU.UsageCoreNanoSeconds == nil {
return
}
ch <- compbasemetrics.NewLazyMetricWithTimestamp(pod.CPU.Time.Time,
compbasemetrics.NewLazyConstMetric(podCPUUsageDesc, compbasemetrics.CounterValue,
float64(*pod.CPU.UsageCoreNanoSeconds)/float64(time.Second), pod.PodRef.Name, pod.PodRef.Namespace))
}
func (rc *resourceMetricsCollector) collectPodMemoryMetrics(ch chan<- compbasemetrics.Metric, pod stats.PodStats) {
if pod.Memory == nil || pod.Memory.WorkingSetBytes == nil {
return
}
ch <- compbasemetrics.NewLazyMetricWithTimestamp(pod.Memory.Time.Time,
compbasemetrics.NewLazyConstMetric(podMemoryUsageDesc, compbasemetrics.GaugeValue,
float64(*pod.Memory.WorkingSetBytes), pod.PodRef.Name, pod.PodRef.Namespace))
}

View File

@@ -0,0 +1,180 @@
package provider
import (
"context"
"time"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
typedv1 "k8s.io/client-go/kubernetes/typed/core/v1"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
k3klog "github.com/rancher/k3k/pkg/log"
)
func ConfigureNode(logger *k3klog.Logger, node *corev1.Node, hostname string, servicePort int, ip string, coreClient typedv1.CoreV1Interface, virtualClient client.Client, virtualCluster v1alpha1.Cluster, version string, mirrorHostNodes bool) {
ctx := context.Background()
if mirrorHostNodes {
hostNode, err := coreClient.Nodes().Get(ctx, node.Name, metav1.GetOptions{})
if err != nil {
logger.Fatal("error getting host node for mirroring", err)
}
node.Spec = *hostNode.Spec.DeepCopy()
node.Status = *hostNode.Status.DeepCopy()
node.Labels = hostNode.GetLabels()
node.Annotations = hostNode.GetAnnotations()
node.Finalizers = hostNode.GetFinalizers()
node.Status.DaemonEndpoints.KubeletEndpoint.Port = int32(servicePort)
} else {
node.Status.Conditions = nodeConditions()
node.Status.DaemonEndpoints.KubeletEndpoint.Port = int32(servicePort)
node.Status.Addresses = []corev1.NodeAddress{
{
Type: corev1.NodeHostName,
Address: hostname,
},
{
Type: corev1.NodeInternalIP,
Address: ip,
},
}
node.Labels["node.kubernetes.io/exclude-from-external-load-balancers"] = "true"
node.Labels["kubernetes.io/os"] = "linux"
// configure versions
node.Status.NodeInfo.KubeletVersion = version
updateNodeCapacityInterval := 10 * time.Second
ticker := time.NewTicker(updateNodeCapacityInterval)
go func() {
for range ticker.C {
if err := updateNodeCapacity(ctx, coreClient, virtualClient, node.Name, virtualCluster.Spec.NodeSelector); err != nil {
logger.Error("error updating node capacity", err)
}
}
}()
}
}
// nodeConditions returns the basic conditions which mark the node as ready
func nodeConditions() []corev1.NodeCondition {
return []corev1.NodeCondition{
{
Type: "Ready",
Status: corev1.ConditionTrue,
LastHeartbeatTime: metav1.Now(),
LastTransitionTime: metav1.Now(),
Reason: "KubeletReady",
Message: "kubelet is ready.",
},
{
Type: "OutOfDisk",
Status: corev1.ConditionFalse,
LastHeartbeatTime: metav1.Now(),
LastTransitionTime: metav1.Now(),
Reason: "KubeletHasSufficientDisk",
Message: "kubelet has sufficient disk space available",
},
{
Type: "MemoryPressure",
Status: corev1.ConditionFalse,
LastHeartbeatTime: metav1.Now(),
LastTransitionTime: metav1.Now(),
Reason: "KubeletHasSufficientMemory",
Message: "kubelet has sufficient memory available",
},
{
Type: "DiskPressure",
Status: corev1.ConditionFalse,
LastHeartbeatTime: metav1.Now(),
LastTransitionTime: metav1.Now(),
Reason: "KubeletHasNoDiskPressure",
Message: "kubelet has no disk pressure",
},
{
Type: "NetworkUnavailable",
Status: corev1.ConditionFalse,
LastHeartbeatTime: metav1.Now(),
LastTransitionTime: metav1.Now(),
Reason: "RouteCreated",
Message: "RouteController created a route",
},
}
}
// updateNodeCapacity will update the virtual node capacity (and the allocatable field) with the sum of all the resource in the host nodes.
// If the nodeLabels are specified only the matching nodes will be considered.
func updateNodeCapacity(ctx context.Context, coreClient typedv1.CoreV1Interface, virtualClient client.Client, virtualNodeName string, nodeLabels map[string]string) error {
capacity, allocatable, err := getResourcesFromNodes(ctx, coreClient, nodeLabels)
if err != nil {
return err
}
var virtualNode corev1.Node
if err := virtualClient.Get(ctx, types.NamespacedName{Name: virtualNodeName}, &virtualNode); err != nil {
return err
}
virtualNode.Status.Capacity = capacity
virtualNode.Status.Allocatable = allocatable
return virtualClient.Status().Update(ctx, &virtualNode)
}
// getResourcesFromNodes will return a sum of all the resource capacity of the host nodes, and the allocatable resources.
// If some node labels are specified only the matching nodes will be considered.
func getResourcesFromNodes(ctx context.Context, coreClient typedv1.CoreV1Interface, nodeLabels map[string]string) (corev1.ResourceList, corev1.ResourceList, error) {
listOpts := metav1.ListOptions{}
if nodeLabels != nil {
labelSelector := metav1.LabelSelector{MatchLabels: nodeLabels}
listOpts.LabelSelector = labels.Set(labelSelector.MatchLabels).String()
}
nodeList, err := coreClient.Nodes().List(ctx, listOpts)
if err != nil {
return nil, nil, err
}
// sum all
virtualCapacityResources := corev1.ResourceList{}
virtualAvailableResources := corev1.ResourceList{}
for _, node := range nodeList.Items {
// check if the node is Ready
for _, condition := range node.Status.Conditions {
if condition.Type != corev1.NodeReady {
continue
}
// if the node is not Ready then we can skip it
if condition.Status != corev1.ConditionTrue {
break
}
}
// add all the available metrics to the virtual node
for resourceName, resourceQuantity := range node.Status.Capacity {
virtualResource := virtualCapacityResources[resourceName]
(&virtualResource).Add(resourceQuantity)
virtualCapacityResources[resourceName] = virtualResource
}
for resourceName, resourceQuantity := range node.Status.Allocatable {
virtualResource := virtualAvailableResources[resourceName]
(&virtualResource).Add(resourceQuantity)
virtualAvailableResources[resourceName] = virtualResource
}
}
return virtualCapacityResources, virtualAvailableResources, nil
}

View File

@@ -0,0 +1,22 @@
package provider
import (
"context"
corev1 "k8s.io/api/core/v1"
)
// Node implements the node.Provider interface from Virtual Kubelet
type Node struct {
notifyCallback func(*corev1.Node)
}
// Ping is called to check if the node is healthy - in the current format it always is
func (n *Node) Ping(context.Context) error {
return nil
}
// NotifyNodeStatus sets the callback function for a node being changed. As of now, no changes are made
func (n *Node) NotifyNodeStatus(ctx context.Context, cb func(*corev1.Node)) {
n.notifyCallback = cb
}

View File

@@ -0,0 +1,990 @@
package provider
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"maps"
"net/http"
"strconv"
"strings"
"time"
"github.com/google/go-cmp/cmp"
"github.com/virtual-kubelet/virtual-kubelet/node/api"
"github.com/virtual-kubelet/virtual-kubelet/node/nodeutil"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/portforward"
"k8s.io/client-go/tools/remotecommand"
"k8s.io/client-go/transport/spdy"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/manager"
dto "github.com/prometheus/client_model/go"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
cv1 "k8s.io/client-go/kubernetes/typed/core/v1"
compbasemetrics "k8s.io/component-base/metrics"
stats "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
"github.com/rancher/k3k/k3k-kubelet/controller"
"github.com/rancher/k3k/k3k-kubelet/controller/webhook"
"github.com/rancher/k3k/k3k-kubelet/provider/collectors"
"github.com/rancher/k3k/k3k-kubelet/translate"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
k3kcontroller "github.com/rancher/k3k/pkg/controller"
k3klog "github.com/rancher/k3k/pkg/log"
)
// check at compile time if the Provider implements the nodeutil.Provider interface
var _ nodeutil.Provider = (*Provider)(nil)
// Provider implements nodetuil.Provider from virtual Kubelet.
// TODO: Implement NotifyPods and the required usage so that this can be an async provider
type Provider struct {
Handler controller.ControllerHandler
Translator translate.ToHostTranslator
HostClient client.Client
VirtualClient client.Client
ClientConfig rest.Config
CoreClient cv1.CoreV1Interface
ClusterNamespace string
ClusterName string
serverIP string
dnsIP string
logger *k3klog.Logger
}
var ErrRetryTimeout = errors.New("provider timed out")
func New(hostConfig rest.Config, hostMgr, virtualMgr manager.Manager, logger *k3klog.Logger, namespace, name, serverIP, dnsIP string) (*Provider, error) {
coreClient, err := cv1.NewForConfig(&hostConfig)
if err != nil {
return nil, err
}
translator := translate.ToHostTranslator{
ClusterName: name,
ClusterNamespace: namespace,
}
p := Provider{
Handler: controller.ControllerHandler{
Mgr: virtualMgr,
Scheme: *virtualMgr.GetScheme(),
HostClient: hostMgr.GetClient(),
VirtualClient: virtualMgr.GetClient(),
Translator: translator,
Logger: logger,
},
HostClient: hostMgr.GetClient(),
VirtualClient: virtualMgr.GetClient(),
Translator: translator,
ClientConfig: hostConfig,
CoreClient: coreClient,
ClusterNamespace: namespace,
ClusterName: name,
logger: logger,
serverIP: serverIP,
dnsIP: dnsIP,
}
return &p, nil
}
// GetContainerLogs retrieves the logs of a container by name from the provider.
func (p *Provider) GetContainerLogs(ctx context.Context, namespace, podName, containerName string, opts api.ContainerLogOpts) (io.ReadCloser, error) {
hostPodName := p.Translator.TranslateName(namespace, podName)
options := corev1.PodLogOptions{
Container: containerName,
Timestamps: opts.Timestamps,
Follow: opts.Follow,
Previous: opts.Previous,
}
if opts.Tail != 0 {
tailLines := int64(opts.Tail)
options.TailLines = &tailLines
}
if opts.LimitBytes != 0 {
limitBytes := int64(opts.LimitBytes)
options.LimitBytes = &limitBytes
}
if opts.SinceSeconds != 0 {
sinceSeconds := int64(opts.SinceSeconds)
options.SinceSeconds = &sinceSeconds
}
if !opts.SinceTime.IsZero() {
sinceTime := metav1.NewTime(opts.SinceTime)
options.SinceTime = &sinceTime
}
closer, err := p.CoreClient.Pods(p.ClusterNamespace).GetLogs(hostPodName, &options).Stream(ctx)
p.logger.Infof("got error %s when getting logs for %s in %s", err, hostPodName, p.ClusterNamespace)
return closer, err
}
// RunInContainer executes a command in a container in the pod, copying data
// between in/out/err and the container's stdin/stdout/stderr.
func (p *Provider) RunInContainer(ctx context.Context, namespace, podName, containerName string, cmd []string, attach api.AttachIO) error {
hostPodName := p.Translator.TranslateName(namespace, podName)
req := p.CoreClient.RESTClient().Post().
Resource("pods").
Name(hostPodName).
Namespace(p.ClusterNamespace).
SubResource("exec")
req.VersionedParams(&corev1.PodExecOptions{
Container: containerName,
Command: cmd,
TTY: attach.TTY(),
Stdin: attach.Stdin() != nil,
Stdout: attach.Stdout() != nil,
Stderr: attach.Stderr() != nil,
}, scheme.ParameterCodec)
exec, err := remotecommand.NewSPDYExecutor(&p.ClientConfig, http.MethodPost, req.URL())
if err != nil {
return err
}
return exec.StreamWithContext(ctx, remotecommand.StreamOptions{
Stdin: attach.Stdin(),
Stdout: attach.Stdout(),
Stderr: attach.Stderr(),
Tty: attach.TTY(),
TerminalSizeQueue: &translatorSizeQueue{
resizeChan: attach.Resize(),
},
})
}
// AttachToContainer attaches to the executing process of a container in the pod, copying data
// between in/out/err and the container's stdin/stdout/stderr.
func (p *Provider) AttachToContainer(ctx context.Context, namespace, podName, containerName string, attach api.AttachIO) error {
hostPodName := p.Translator.TranslateName(namespace, podName)
req := p.CoreClient.RESTClient().Post().
Resource("pods").
Name(hostPodName).
Namespace(p.ClusterNamespace).
SubResource("attach")
req.VersionedParams(&corev1.PodAttachOptions{
Container: containerName,
TTY: attach.TTY(),
Stdin: attach.Stdin() != nil,
Stdout: attach.Stdout() != nil,
Stderr: attach.Stderr() != nil,
}, scheme.ParameterCodec)
exec, err := remotecommand.NewSPDYExecutor(&p.ClientConfig, http.MethodPost, req.URL())
if err != nil {
return err
}
return exec.StreamWithContext(ctx, remotecommand.StreamOptions{
Stdin: attach.Stdin(),
Stdout: attach.Stdout(),
Stderr: attach.Stderr(),
Tty: attach.TTY(),
TerminalSizeQueue: &translatorSizeQueue{
resizeChan: attach.Resize(),
},
})
}
// GetStatsSummary gets the stats for the node, including running pods
func (p *Provider) GetStatsSummary(ctx context.Context) (*stats.Summary, error) {
p.logger.Debug("GetStatsSummary")
nodeList := &corev1.NodeList{}
if err := p.CoreClient.RESTClient().Get().Resource("nodes").Do(ctx).Into(nodeList); err != nil {
return nil, fmt.Errorf("unable to get nodes of cluster %s in namespace %s: %w", p.ClusterName, p.ClusterNamespace, err)
}
// fetch the stats from all the nodes
var (
nodeStats stats.NodeStats
allPodsStats []stats.PodStats
)
for _, n := range nodeList.Items {
res, err := p.CoreClient.RESTClient().
Get().
Resource("nodes").
Name(n.Name).
SubResource("proxy").
Suffix("stats/summary").
DoRaw(ctx)
if err != nil {
return nil, fmt.Errorf(
"unable to get stats of node '%s', from cluster %s in namespace %s: %w",
n.Name, p.ClusterName, p.ClusterNamespace, err,
)
}
stats := &stats.Summary{}
if err := json.Unmarshal(res, stats); err != nil {
return nil, err
}
// TODO: we should probably calculate somehow the node stats from the different nodes of the host
// or reflect different nodes from the virtual kubelet.
// For the moment let's just pick one random node stats.
nodeStats = stats.Node
allPodsStats = append(allPodsStats, stats.Pods...)
}
pods, err := p.GetPods(ctx)
if err != nil {
return nil, err
}
podsNameMap := make(map[string]*corev1.Pod)
for _, pod := range pods {
hostPodName := p.Translator.TranslateName(pod.Namespace, pod.Name)
podsNameMap[hostPodName] = pod
}
filteredStats := &stats.Summary{
Node: nodeStats,
Pods: make([]stats.PodStats, 0),
}
for _, podStat := range allPodsStats {
// skip pods that are not in the cluster namespace
if podStat.PodRef.Namespace != p.ClusterNamespace {
continue
}
// rewrite the PodReference to match the data of the virtual cluster
if pod, found := podsNameMap[podStat.PodRef.Name]; found {
podStat.PodRef = stats.PodReference{
Name: pod.Name,
Namespace: pod.Namespace,
UID: string(pod.UID),
}
filteredStats.Pods = append(filteredStats.Pods, podStat)
}
}
return filteredStats, nil
}
// GetMetricsResource gets the metrics for the node, including running pods
func (p *Provider) GetMetricsResource(ctx context.Context) ([]*dto.MetricFamily, error) {
statsSummary, err := p.GetStatsSummary(ctx)
if err != nil {
return nil, errors.Join(err, errors.New("error fetching MetricsResource"))
}
registry := compbasemetrics.NewKubeRegistry()
registry.CustomMustRegister(collectors.NewKubeletResourceMetricsCollector(statsSummary))
metricFamily, err := registry.Gather()
if err != nil {
return nil, errors.Join(err, errors.New("error gathering metrics from collector"))
}
return metricFamily, nil
}
// PortForward forwards a local port to a port on the pod
func (p *Provider) PortForward(ctx context.Context, namespace, pod string, port int32, stream io.ReadWriteCloser) error {
hostPodName := p.Translator.TranslateName(namespace, pod)
req := p.CoreClient.RESTClient().Post().
Resource("pods").
Name(hostPodName).
Namespace(p.ClusterNamespace).
SubResource("portforward")
transport, upgrader, err := spdy.RoundTripperFor(&p.ClientConfig)
if err != nil {
return err
}
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, http.MethodPost, req.URL())
portAsString := strconv.Itoa(int(port))
readyChannel := make(chan struct{})
stopChannel := make(chan struct{}, 1)
// Today this doesn't work properly. When the port ward is supposed to stop, the caller (this provider)
// should send a value on stopChannel so that the PortForward is stopped. However, we only have a ReadWriteCloser
// so more work is needed to detect a close and handle that appropriately.
fw, err := portforward.New(dialer, []string{portAsString}, stopChannel, readyChannel, stream, stream)
if err != nil {
return err
}
return fw.ForwardPorts()
}
// CreatePod executes createPod with retry
func (p *Provider) CreatePod(ctx context.Context, pod *corev1.Pod) error {
return p.withRetry(ctx, p.createPod, pod)
}
// createPod takes a Kubernetes Pod and deploys it within the provider.
func (p *Provider) createPod(ctx context.Context, pod *corev1.Pod) error {
tPod := pod.DeepCopy()
p.Translator.TranslateTo(tPod)
// get Cluster definition
clusterKey := types.NamespacedName{
Namespace: p.ClusterNamespace,
Name: p.ClusterName,
}
var cluster v1alpha1.Cluster
if err := p.HostClient.Get(ctx, clusterKey, &cluster); err != nil {
return fmt.Errorf("unable to get cluster %s in namespace %s: %w", p.ClusterName, p.ClusterNamespace, err)
}
// these values shouldn't be set on create
tPod.UID = ""
tPod.ResourceVersion = ""
// the node was scheduled on the virtual kubelet, but leaving it this way will make it pending indefinitely
tPod.Spec.NodeName = ""
tPod.Spec.NodeSelector = cluster.Spec.NodeSelector
// setting the hostname for the pod if its not set
if pod.Spec.Hostname == "" {
tPod.Spec.Hostname = k3kcontroller.SafeConcatName(pod.Name)
}
// if the priorityClass for the virtual cluster is set then override the provided value
// Note: the core-dns and local-path-provisioner pod are scheduled by k3s with the
// 'system-cluster-critical' and 'system-node-critical' default priority classes.
if !strings.HasPrefix(tPod.Spec.PriorityClassName, "system-") {
if tPod.Spec.PriorityClassName != "" {
tPriorityClassName := p.Translator.TranslateName("", tPod.Spec.PriorityClassName)
tPod.Spec.PriorityClassName = tPriorityClassName
}
if cluster.Spec.PriorityClass != "" {
tPod.Spec.PriorityClassName = cluster.Spec.PriorityClass
tPod.Spec.Priority = nil
}
}
// fieldpath annotations
if err := p.configureFieldPathEnv(pod, tPod); err != nil {
return fmt.Errorf("unable to fetch fieldpath annotations for pod %s/%s: %w", pod.Namespace, pod.Name, err)
}
// volumes will often refer to resources in the virtual cluster, but instead need to refer to the sync'd
// host cluster version
if err := p.transformVolumes(ctx, pod.Namespace, tPod.Spec.Volumes); err != nil {
return fmt.Errorf("unable to sync volumes for pod %s/%s: %w", pod.Namespace, pod.Name, err)
}
// sync serviceaccount token to a the host cluster
if err := p.transformTokens(ctx, pod, tPod); err != nil {
return fmt.Errorf("unable to transform tokens for pod %s/%s: %w", pod.Namespace, pod.Name, err)
}
// inject networking information to the pod including the virtual cluster controlplane endpoint
configureNetworking(tPod, pod.Name, pod.Namespace, p.serverIP, p.dnsIP)
p.logger.Infow("creating pod",
"host_namespace", tPod.Namespace, "host_name", tPod.Name,
"virtual_namespace", pod.Namespace, "virtual_name", pod.Name,
)
// set ownerReference to the cluster object
if err := controllerutil.SetControllerReference(&cluster, tPod, p.HostClient.Scheme()); err != nil {
return err
}
return p.HostClient.Create(ctx, tPod)
}
// withRetry retries passed function with interval and timeout
func (p *Provider) withRetry(ctx context.Context, f func(context.Context, *corev1.Pod) error, pod *corev1.Pod) error {
const (
interval = 2 * time.Second
timeout = 10 * time.Second
)
var allErrors error
// retryFn will retry until the operation succeed, or the timeout occurs
retryFn := func(ctx context.Context) (bool, error) {
if lastErr := f(ctx, pod); lastErr != nil {
// log that the retry failed?
allErrors = errors.Join(allErrors, lastErr)
return false, nil
}
return true, nil
}
if err := wait.PollUntilContextTimeout(ctx, interval, timeout, true, retryFn); err != nil {
return errors.Join(allErrors, ErrRetryTimeout)
}
return nil
}
// transformVolumes changes the volumes to the representation in the host cluster. Will return an error
// if one/more volumes couldn't be transformed
func (p *Provider) transformVolumes(ctx context.Context, podNamespace string, volumes []corev1.Volume) error {
for _, volume := range volumes {
var optional bool
if strings.HasPrefix(volume.Name, kubeAPIAccessPrefix) {
continue
}
// note: this needs to handle downward api volumes as well, but more thought is needed on how to do that
if volume.ConfigMap != nil {
if volume.ConfigMap.Optional != nil {
optional = *volume.ConfigMap.Optional
}
if err := p.syncConfigmap(ctx, podNamespace, volume.ConfigMap.Name, optional); err != nil {
return fmt.Errorf("unable to sync configmap volume %s: %w", volume.Name, err)
}
volume.ConfigMap.Name = p.Translator.TranslateName(podNamespace, volume.ConfigMap.Name)
} else if volume.Secret != nil {
if volume.Secret.Optional != nil {
optional = *volume.Secret.Optional
}
if err := p.syncSecret(ctx, podNamespace, volume.Secret.SecretName, optional); err != nil {
return fmt.Errorf("unable to sync secret volume %s: %w", volume.Name, err)
}
volume.Secret.SecretName = p.Translator.TranslateName(podNamespace, volume.Secret.SecretName)
} else if volume.Projected != nil {
for _, source := range volume.Projected.Sources {
if source.ConfigMap != nil {
if source.ConfigMap.Optional != nil {
optional = *source.ConfigMap.Optional
}
configMapName := source.ConfigMap.Name
if err := p.syncConfigmap(ctx, podNamespace, configMapName, optional); err != nil {
return fmt.Errorf("unable to sync projected configmap %s: %w", configMapName, err)
}
source.ConfigMap.Name = p.Translator.TranslateName(podNamespace, configMapName)
} else if source.Secret != nil {
if source.Secret.Optional != nil {
optional = *source.Secret.Optional
}
secretName := source.Secret.Name
if err := p.syncSecret(ctx, podNamespace, secretName, optional); err != nil {
return fmt.Errorf("unable to sync projected secret %s: %w", secretName, err)
}
source.Secret.Name = p.Translator.TranslateName(podNamespace, secretName)
}
}
} else if volume.PersistentVolumeClaim != nil {
volume.PersistentVolumeClaim.ClaimName = p.Translator.TranslateName(podNamespace, volume.PersistentVolumeClaim.ClaimName)
} else if volume.DownwardAPI != nil {
for _, downwardAPI := range volume.DownwardAPI.Items {
if downwardAPI.FieldRef != nil {
if downwardAPI.FieldRef.FieldPath == translate.MetadataNameField {
downwardAPI.FieldRef.FieldPath = fmt.Sprintf("metadata.annotations['%s']", translate.ResourceNameAnnotation)
}
if downwardAPI.FieldRef.FieldPath == translate.MetadataNamespaceField {
downwardAPI.FieldRef.FieldPath = fmt.Sprintf("metadata.annotations['%s']", translate.ResourceNamespaceAnnotation)
}
}
}
}
}
return nil
}
// syncConfigmap will add the configmap object to the queue of the syncer controller to be synced to the host cluster
func (p *Provider) syncConfigmap(ctx context.Context, podNamespace string, configMapName string, optional bool) error {
var configMap corev1.ConfigMap
nsName := types.NamespacedName{
Namespace: podNamespace,
Name: configMapName,
}
if err := p.VirtualClient.Get(ctx, nsName, &configMap); err != nil {
// check if its optional configmap
if apierrors.IsNotFound(err) && optional {
return nil
}
return fmt.Errorf("unable to get configmap to sync %s/%s: %w", nsName.Namespace, nsName.Name, err)
}
if err := p.Handler.AddResource(ctx, &configMap); err != nil {
return fmt.Errorf("unable to add configmap to sync %s/%s: %w", nsName.Namespace, nsName.Name, err)
}
return nil
}
// syncSecret will add the secret object to the queue of the syncer controller to be synced to the host cluster
func (p *Provider) syncSecret(ctx context.Context, podNamespace string, secretName string, optional bool) error {
p.logger.Infow("Syncing secret", "Name", secretName, "Namespace", podNamespace, "optional", optional)
var secret corev1.Secret
nsName := types.NamespacedName{
Namespace: podNamespace,
Name: secretName,
}
if err := p.VirtualClient.Get(ctx, nsName, &secret); err != nil {
if apierrors.IsNotFound(err) && optional {
return nil
}
return fmt.Errorf("unable to get secret to sync %s/%s: %w", nsName.Namespace, nsName.Name, err)
}
if err := p.Handler.AddResource(ctx, &secret); err != nil {
return fmt.Errorf("unable to add secret to sync %s/%s: %w", nsName.Namespace, nsName.Name, err)
}
return nil
}
// UpdatePod executes updatePod with retry
func (p *Provider) UpdatePod(ctx context.Context, pod *corev1.Pod) error {
return p.withRetry(ctx, p.updatePod, pod)
}
func (p *Provider) updatePod(ctx context.Context, pod *corev1.Pod) error {
p.logger.Debugw("got a request for update pod")
// Once scheduled a Pod cannot update other fields than the image of the containers, initcontainers and a few others
// See: https://kubernetes.io/docs/concepts/workloads/pods/#pod-update-and-replacement
// Update Pod in the virtual cluster
var currentVirtualPod corev1.Pod
if err := p.VirtualClient.Get(ctx, client.ObjectKeyFromObject(pod), &currentVirtualPod); err != nil {
return fmt.Errorf("unable to get pod to update from virtual cluster: %w", err)
}
hostNamespaceName := types.NamespacedName{
Namespace: p.ClusterNamespace,
Name: p.Translator.TranslateName(pod.Namespace, pod.Name),
}
var currentHostPod corev1.Pod
if err := p.HostClient.Get(ctx, hostNamespaceName, &currentHostPod); err != nil {
return fmt.Errorf("unable to get pod to update from host cluster: %w", err)
}
// Handle ephemeral containers
if !cmp.Equal(currentHostPod.Spec.EphemeralContainers, pod.Spec.EphemeralContainers) {
p.logger.Info("Updating ephemeral containers")
currentHostPod.Spec.EphemeralContainers = pod.Spec.EphemeralContainers
if _, err := p.CoreClient.Pods(p.ClusterNamespace).UpdateEphemeralContainers(ctx, currentHostPod.Name, &currentHostPod, metav1.UpdateOptions{}); err != nil {
p.logger.Errorf("error when updating ephemeral containers: %v", err)
return err
}
return nil
}
currentVirtualPod.Spec.Containers = updateContainerImages(currentVirtualPod.Spec.Containers, pod.Spec.Containers)
currentVirtualPod.Spec.InitContainers = updateContainerImages(currentVirtualPod.Spec.InitContainers, pod.Spec.InitContainers)
currentVirtualPod.Spec.ActiveDeadlineSeconds = pod.Spec.ActiveDeadlineSeconds
currentVirtualPod.Spec.Tolerations = pod.Spec.Tolerations
// in the virtual cluster we can update also the labels and annotations
currentVirtualPod.Annotations = pod.Annotations
currentVirtualPod.Labels = pod.Labels
if err := p.VirtualClient.Update(ctx, &currentVirtualPod); err != nil {
return fmt.Errorf("unable to update pod in the virtual cluster: %w", err)
}
// Update Pod in the host cluster
currentHostPod.Spec.Containers = updateContainerImages(currentHostPod.Spec.Containers, pod.Spec.Containers)
currentHostPod.Spec.InitContainers = updateContainerImages(currentHostPod.Spec.InitContainers, pod.Spec.InitContainers)
// update ActiveDeadlineSeconds and Tolerations
currentHostPod.Spec.ActiveDeadlineSeconds = pod.Spec.ActiveDeadlineSeconds
currentHostPod.Spec.Tolerations = pod.Spec.Tolerations
// in the virtual cluster we can update also the labels and annotations
maps.Copy(currentHostPod.Annotations, pod.Annotations)
maps.Copy(currentHostPod.Labels, pod.Labels)
if err := p.HostClient.Update(ctx, &currentHostPod); err != nil {
return fmt.Errorf("unable to update pod in the host cluster: %w", err)
}
return nil
}
// updateContainerImages will update the images of the original container images with the same name
func updateContainerImages(original, updated []corev1.Container) []corev1.Container {
newImages := make(map[string]string)
for _, c := range updated {
newImages[c.Name] = c.Image
}
for i, c := range original {
if updatedImage, found := newImages[c.Name]; found {
original[i].Image = updatedImage
}
}
return original
}
// DeletePod executes deletePod with retry
func (p *Provider) DeletePod(ctx context.Context, pod *corev1.Pod) error {
return p.withRetry(ctx, p.deletePod, pod)
}
// deletePod takes a Kubernetes Pod and deletes it from the provider. Once a pod is deleted, the provider is
// expected to call the NotifyPods callback with a terminal pod status where all the containers are in a terminal
// state, as well as the pod. DeletePod may be called multiple times for the same pod.
func (p *Provider) deletePod(ctx context.Context, pod *corev1.Pod) error {
p.logger.Infof("Got request to delete pod %s", pod.Name)
hostName := p.Translator.TranslateName(pod.Namespace, pod.Name)
err := p.CoreClient.Pods(p.ClusterNamespace).Delete(ctx, hostName, metav1.DeleteOptions{})
if err != nil {
return fmt.Errorf("unable to delete pod %s/%s: %w", pod.Namespace, pod.Name, err)
}
if err = p.pruneUnusedVolumes(ctx, pod); err != nil {
// note that we don't return an error here. The pod was successfully deleted, another process
// should clean this without affecting the user
p.logger.Errorf("failed to prune leftover volumes for %s/%s: %w, resources may be left", pod.Namespace, pod.Name, err)
}
p.logger.Infof("Deleted pod %s", pod.Name)
return nil
}
// pruneUnusedVolumes removes volumes in use by pod that aren't used by any other pods
func (p *Provider) pruneUnusedVolumes(ctx context.Context, pod *corev1.Pod) error {
rawSecrets, rawConfigMaps := getSecretsAndConfigmaps(pod)
// since this pod was removed, originally mark all of the secrets/configmaps it uses as eligible
// for pruning
pruneSecrets := sets.Set[string]{}.Insert(rawSecrets...)
pruneConfigMap := sets.Set[string]{}.Insert(rawConfigMaps...)
var pods corev1.PodList
// only pods in the same namespace could be using secrets/configmaps that this pod is using
err := p.VirtualClient.List(ctx, &pods, &client.ListOptions{
Namespace: pod.Namespace,
})
if err != nil {
return fmt.Errorf("unable to list pods: %w", err)
}
for _, vPod := range pods.Items {
if vPod.Name == pod.Name {
continue
}
secrets, configMaps := getSecretsAndConfigmaps(&vPod)
pruneSecrets.Delete(secrets...)
pruneConfigMap.Delete(configMaps...)
}
for _, secretName := range pruneSecrets.UnsortedList() {
var secret corev1.Secret
key := types.NamespacedName{
Name: secretName,
Namespace: pod.Namespace,
}
if err := p.VirtualClient.Get(ctx, key, &secret); err != nil {
return fmt.Errorf("unable to get secret %s/%s for pod volume: %w", pod.Namespace, secretName, err)
}
if err = p.Handler.RemoveResource(ctx, &secret); err != nil {
return fmt.Errorf("unable to remove secret %s/%s for pod volume: %w", pod.Namespace, secretName, err)
}
}
for _, configMapName := range pruneConfigMap.UnsortedList() {
var configMap corev1.ConfigMap
key := types.NamespacedName{
Name: configMapName,
Namespace: pod.Namespace,
}
if err := p.VirtualClient.Get(ctx, key, &configMap); err != nil {
return fmt.Errorf("unable to get configMap %s/%s for pod volume: %w", pod.Namespace, configMapName, err)
}
if err = p.Handler.RemoveResource(ctx, &configMap); err != nil {
return fmt.Errorf("unable to remove configMap %s/%s for pod volume: %w", pod.Namespace, configMapName, err)
}
}
return nil
}
// GetPod retrieves a pod by name from the provider (can be cached).
// The Pod returned is expected to be immutable, and may be accessed
// concurrently outside of the calling goroutine. Therefore it is recommended
// to return a version after DeepCopy.
func (p *Provider) GetPod(ctx context.Context, namespace, name string) (*corev1.Pod, error) {
p.logger.Debugw("got a request for get pod", "Namespace", namespace, "Name", name)
hostNamespaceName := types.NamespacedName{
Namespace: p.ClusterNamespace,
Name: p.Translator.TranslateName(namespace, name),
}
var pod corev1.Pod
if err := p.HostClient.Get(ctx, hostNamespaceName, &pod); err != nil {
return nil, fmt.Errorf("error when retrieving pod: %w", err)
}
p.Translator.TranslateFrom(&pod)
return &pod, nil
}
// GetPodStatus retrieves the status of a pod by name from the provider.
// The PodStatus returned is expected to be immutable, and may be accessed
// concurrently outside of the calling goroutine. Therefore it is recommended
// to return a version after DeepCopy.
func (p *Provider) GetPodStatus(ctx context.Context, namespace, name string) (*corev1.PodStatus, error) {
p.logger.Debugw("got a request for pod status", "Namespace", namespace, "Name", name)
pod, err := p.GetPod(ctx, namespace, name)
if err != nil {
return nil, fmt.Errorf("unable to get pod for status: %w", err)
}
p.logger.Debugw("got pod status", "Namespace", namespace, "Name", name, "Status", pod.Status)
return pod.Status.DeepCopy(), nil
}
// GetPods retrieves a list of all pods running on the provider (can be cached).
// The Pods returned are expected to be immutable, and may be accessed
// concurrently outside of the calling goroutine. Therefore it is recommended
// to return a version after DeepCopy.
func (p *Provider) GetPods(ctx context.Context) ([]*corev1.Pod, error) {
selector := labels.NewSelector()
requirement, err := labels.NewRequirement(translate.ClusterNameLabel, selection.Equals, []string{p.ClusterName})
if err != nil {
return nil, fmt.Errorf("unable to create label selector: %w", err)
}
selector = selector.Add(*requirement)
var podList corev1.PodList
err = p.HostClient.List(ctx, &podList, &client.ListOptions{LabelSelector: selector})
if err != nil {
return nil, fmt.Errorf("unable to list pods: %w", err)
}
retPods := []*corev1.Pod{}
for _, pod := range podList.DeepCopy().Items {
p.Translator.TranslateFrom(&pod)
retPods = append(retPods, &pod)
}
return retPods, nil
}
// configureNetworking will inject network information to each pod to connect them to the
// virtual cluster api server, as well as confiugre DNS information to connect them to the
// synced coredns on the host cluster.
func configureNetworking(pod *corev1.Pod, podName, podNamespace, serverIP, dnsIP string) {
// inject serverIP to hostalias for the pod
pod.Spec.HostAliases = append(pod.Spec.HostAliases, corev1.HostAlias{
IP: serverIP,
Hostnames: []string{
"kubernetes",
"kubernetes.default",
"kubernetes.default.svc",
"kubernetes.default.svc.cluster",
"kubernetes.default.svc.cluster.local",
},
})
// injecting cluster DNS IP to the pods except for coredns pod
if !strings.HasPrefix(podName, "coredns") && pod.Spec.DNSConfig == nil {
pod.Spec.DNSPolicy = corev1.DNSNone
pod.Spec.DNSConfig = &corev1.PodDNSConfig{
Nameservers: []string{
dnsIP,
},
Searches: []string{
podNamespace + ".svc.cluster.local",
"svc.cluster.local",
"cluster.local",
},
Options: []corev1.PodDNSConfigOption{
{
Name: "ndots",
Value: ptr.To("5"),
},
},
}
}
updatedEnvVars := []corev1.EnvVar{
{Name: "KUBERNETES_SERVICE_HOST", Value: serverIP},
{Name: "KUBERNETES_PORT", Value: "tcp://" + serverIP + ":443"},
{Name: "KUBERNETES_PORT_443_TCP", Value: "tcp://" + serverIP + ":443"},
{Name: "KUBERNETES_PORT_443_TCP_ADDR", Value: serverIP},
}
// inject networking information to the pod's environment variables
for i := range pod.Spec.Containers {
pod.Spec.Containers[i].Env = overrideEnvVars(pod.Spec.Containers[i].Env, updatedEnvVars)
}
// handle init containers as well
for i := range pod.Spec.InitContainers {
pod.Spec.InitContainers[i].Env = overrideEnvVars(pod.Spec.InitContainers[i].Env, updatedEnvVars)
}
// handle ephemeral containers as well
for i := range pod.Spec.EphemeralContainers {
pod.Spec.EphemeralContainers[i].Env = overrideEnvVars(pod.Spec.EphemeralContainers[i].Env, updatedEnvVars)
}
}
// overrideEnvVars will override the orig environment variables if found in the updated list
func overrideEnvVars(orig, updated []corev1.EnvVar) []corev1.EnvVar {
if len(updated) == 0 {
return orig
}
// create map for single lookup
updatedEnvVarMap := make(map[string]corev1.EnvVar)
for _, updatedEnvVar := range updated {
updatedEnvVarMap[updatedEnvVar.Name] = updatedEnvVar
}
for i, origEnvVar := range orig {
if updatedEnvVar, found := updatedEnvVarMap[origEnvVar.Name]; found {
orig[i] = updatedEnvVar
}
}
return orig
}
// getSecretsAndConfigmaps retrieves a list of all secrets/configmaps that are in use by a given pod. Useful
// for removing/seeing which virtual cluster resources need to be in the host cluster.
func getSecretsAndConfigmaps(pod *corev1.Pod) ([]string, []string) {
var (
secrets []string
configMaps []string
)
for _, volume := range pod.Spec.Volumes {
if volume.Secret != nil {
secrets = append(secrets, volume.Secret.SecretName)
} else if volume.ConfigMap != nil {
configMaps = append(configMaps, volume.ConfigMap.Name)
} else if volume.Projected != nil {
for _, source := range volume.Projected.Sources {
if source.ConfigMap != nil {
configMaps = append(configMaps, source.ConfigMap.Name)
} else if source.Secret != nil {
secrets = append(secrets, source.Secret.Name)
}
}
}
}
return secrets, configMaps
}
// configureFieldPathEnv will retrieve all annotations created by the pod mutator webhook
// to assign env fieldpaths to pods, it will also make sure to change the metadata.name and metadata.namespace to the
// assigned annotations
func (p *Provider) configureFieldPathEnv(pod, tPod *corev1.Pod) error {
for _, container := range pod.Spec.EphemeralContainers {
addFieldPathAnnotationToEnv(container.Env)
}
// override metadata.name and metadata.namespace with pod annotations
for _, container := range pod.Spec.InitContainers {
addFieldPathAnnotationToEnv(container.Env)
}
for _, container := range pod.Spec.Containers {
addFieldPathAnnotationToEnv(container.Env)
}
for name, value := range pod.Annotations {
if strings.Contains(name, webhook.FieldpathField) {
containerIndex, envName, err := webhook.ParseFieldPathAnnotationKey(name)
if err != nil {
return err
}
// re-adding these envs to the pod
tPod.Spec.Containers[containerIndex].Env = append(tPod.Spec.Containers[containerIndex].Env, corev1.EnvVar{
Name: envName,
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: value,
},
},
})
// removing the annotation from the pod
delete(tPod.Annotations, name)
}
}
return nil
}
func addFieldPathAnnotationToEnv(envVars []corev1.EnvVar) {
for j, envVar := range envVars {
if envVar.ValueFrom == nil || envVar.ValueFrom.FieldRef == nil {
continue
}
fieldPath := envVar.ValueFrom.FieldRef.FieldPath
if fieldPath == translate.MetadataNameField {
envVar.ValueFrom.FieldRef.FieldPath = fmt.Sprintf("metadata.annotations['%s']", translate.ResourceNameAnnotation)
envVars[j] = envVar
}
if fieldPath == translate.MetadataNamespaceField {
envVar.ValueFrom.FieldRef.FieldPath = fmt.Sprintf("metadata.annotations['%s']", translate.ResourceNamespaceAnnotation)
envVars[j] = envVar
}
}
}

View File

@@ -0,0 +1,70 @@
package provider
import (
"reflect"
"testing"
corev1 "k8s.io/api/core/v1"
)
func Test_overrideEnvVars(t *testing.T) {
type args struct {
orig []corev1.EnvVar
new []corev1.EnvVar
}
tests := []struct {
name string
args args
want []corev1.EnvVar
}{
{
name: "orig and new are empty",
args: args{
orig: []corev1.EnvVar{},
new: []corev1.EnvVar{},
},
want: []corev1.EnvVar{},
},
{
name: "only orig is empty",
args: args{
orig: []corev1.EnvVar{},
new: []corev1.EnvVar{{Name: "FOO", Value: "new_val"}},
},
want: []corev1.EnvVar{},
},
{
name: "orig has a matching element",
args: args{
orig: []corev1.EnvVar{{Name: "FOO", Value: "old_val"}},
new: []corev1.EnvVar{{Name: "FOO", Value: "new_val"}},
},
want: []corev1.EnvVar{{Name: "FOO", Value: "new_val"}},
},
{
name: "orig have multiple elements",
args: args{
orig: []corev1.EnvVar{{Name: "FOO_0", Value: "old_val_0"}, {Name: "FOO_1", Value: "old_val_1"}},
new: []corev1.EnvVar{{Name: "FOO_1", Value: "new_val_1"}},
},
want: []corev1.EnvVar{{Name: "FOO_0", Value: "old_val_0"}, {Name: "FOO_1", Value: "new_val_1"}},
},
{
name: "orig and new have multiple elements and some not matching",
args: args{
orig: []corev1.EnvVar{{Name: "FOO_0", Value: "old_val_0"}, {Name: "FOO_1", Value: "old_val_1"}},
new: []corev1.EnvVar{{Name: "FOO_1", Value: "new_val_1"}, {Name: "FOO_2", Value: "val_1"}},
},
want: []corev1.EnvVar{{Name: "FOO_0", Value: "old_val_0"}, {Name: "FOO_1", Value: "new_val_1"}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := overrideEnvVars(tt.args.orig, tt.args.new); !reflect.DeepEqual(got, tt.want) {
t.Errorf("overrideEnvVars() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,173 @@
package provider
import (
"context"
"fmt"
"strings"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k3kcontroller "github.com/rancher/k3k/pkg/controller"
)
const (
kubeAPIAccessPrefix = "kube-api-access"
serviceAccountTokenMountPath = "/var/run/secrets/kubernetes.io/serviceaccount"
)
// transformTokens copies the serviceaccount tokens used by pod's serviceaccount to a secret on the host cluster and mount it
// to look like the serviceaccount token
func (p *Provider) transformTokens(ctx context.Context, pod, tPod *corev1.Pod) error {
p.logger.Infow("transforming token", "Pod", pod.Name, "Namespace", pod.Namespace, "serviceAccountName", pod.Spec.ServiceAccountName)
// skip this process if the kube-api-access is already removed from the pod
// this is needed in case users already adds their own custom tokens like in rancher imported clusters
if !isKubeAccessVolumeFound(pod) {
return nil
}
virtualSecretName := k3kcontroller.SafeConcatNameWithPrefix(pod.Spec.ServiceAccountName, "token")
virtualSecret := virtualSecret(virtualSecretName, pod.Namespace, pod.Spec.ServiceAccountName)
if err := p.VirtualClient.Create(ctx, virtualSecret); err != nil {
if !apierrors.IsAlreadyExists(err) {
return err
}
}
// extracting the tokens data from the secret we just created
virtualSecretKey := types.NamespacedName{
Name: virtualSecret.Name,
Namespace: virtualSecret.Namespace,
}
if err := p.VirtualClient.Get(ctx, virtualSecretKey, virtualSecret); err != nil {
return err
}
// To avoid race conditions we need to check if the secret's data has been populated
// including the token, ca.crt and namespace
if len(virtualSecret.Data) < 3 {
return fmt.Errorf("token secret %s/%s data is empty", virtualSecret.Namespace, virtualSecret.Name)
}
hostSecret := virtualSecret.DeepCopy()
hostSecret.Type = ""
hostSecret.Annotations = make(map[string]string)
p.Translator.TranslateTo(hostSecret)
if err := p.HostClient.Create(ctx, hostSecret); err != nil {
if !apierrors.IsAlreadyExists(err) {
return err
}
}
p.translateToken(tPod, hostSecret.Name)
return nil
}
func virtualSecret(name, namespace, serviceAccountName string) *corev1.Secret {
return &corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
corev1.ServiceAccountNameKey: serviceAccountName,
},
},
Type: corev1.SecretTypeServiceAccountToken,
}
}
// translateToken will remove the serviceaccount from the pod and replace the kube-api-access volume
// with a custom token volume and mount it to all containers within the pod
func (p *Provider) translateToken(pod *corev1.Pod, hostSecretName string) {
pod.Spec.ServiceAccountName = ""
pod.Spec.DeprecatedServiceAccount = ""
pod.Spec.AutomountServiceAccountToken = ptr.To(false)
removeKubeAccessVolume(pod)
addKubeAccessVolume(pod, hostSecretName)
}
func isKubeAccessVolumeFound(pod *corev1.Pod) bool {
for _, volume := range pod.Spec.Volumes {
if strings.HasPrefix(volume.Name, kubeAPIAccessPrefix) {
return true
}
}
return false
}
func removeKubeAccessVolume(pod *corev1.Pod) {
for i, volume := range pod.Spec.Volumes {
if strings.HasPrefix(volume.Name, kubeAPIAccessPrefix) {
pod.Spec.Volumes = append(pod.Spec.Volumes[:i], pod.Spec.Volumes[i+1:]...)
break
}
}
// init containers
for i, container := range pod.Spec.InitContainers {
for j, mountPath := range container.VolumeMounts {
if strings.HasPrefix(mountPath.Name, kubeAPIAccessPrefix) {
pod.Spec.InitContainers[i].VolumeMounts = append(pod.Spec.InitContainers[i].VolumeMounts[:j], pod.Spec.InitContainers[i].VolumeMounts[j+1:]...)
break
}
}
}
// ephemeral containers
for i, container := range pod.Spec.EphemeralContainers {
for j, mountPath := range container.VolumeMounts {
if strings.HasPrefix(mountPath.Name, kubeAPIAccessPrefix) {
pod.Spec.EphemeralContainers[i].VolumeMounts = append(pod.Spec.EphemeralContainers[i].VolumeMounts[:j], pod.Spec.EphemeralContainers[i].VolumeMounts[j+1:]...)
break
}
}
}
for i, container := range pod.Spec.Containers {
for j, mountPath := range container.VolumeMounts {
if strings.HasPrefix(mountPath.Name, kubeAPIAccessPrefix) {
pod.Spec.Containers[i].VolumeMounts = append(pod.Spec.Containers[i].VolumeMounts[:j], pod.Spec.Containers[i].VolumeMounts[j+1:]...)
break
}
}
}
}
func addKubeAccessVolume(pod *corev1.Pod, hostSecretName string) {
tokenVolumeName := k3kcontroller.SafeConcatNameWithPrefix(kubeAPIAccessPrefix)
pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{
Name: tokenVolumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: hostSecretName,
},
},
})
for i := range pod.Spec.InitContainers {
pod.Spec.InitContainers[i].VolumeMounts = append(pod.Spec.InitContainers[i].VolumeMounts, corev1.VolumeMount{
Name: tokenVolumeName,
MountPath: serviceAccountTokenMountPath,
})
}
for i := range pod.Spec.Containers {
pod.Spec.Containers[i].VolumeMounts = append(pod.Spec.Containers[i].VolumeMounts, corev1.VolumeMount{
Name: tokenVolumeName,
MountPath: serviceAccountTokenMountPath,
})
}
}

View File

@@ -0,0 +1,25 @@
package provider
import (
"github.com/virtual-kubelet/virtual-kubelet/node/api"
"k8s.io/client-go/tools/remotecommand"
)
// translatorSizeQueue feeds the size events from the WebSocket
// resizeChan into the SPDY client input. Implements TerminalSizeQueue
// interface.
type translatorSizeQueue struct {
resizeChan <-chan api.TermSize
}
func (t *translatorSizeQueue) Next() *remotecommand.TerminalSize {
size, ok := <-t.resizeChan
if !ok {
return nil
}
return &remotecommand.TerminalSize{
Width: size.Width,
Height: size.Height,
}
}

View File

@@ -0,0 +1,127 @@
package translate
import (
"encoding/hex"
"strings"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/rancher/k3k/pkg/controller"
)
const (
// ClusterNameLabel is the key for the label that contains the name of the virtual cluster
// this resource was made in
ClusterNameLabel = "k3k.io/clusterName"
// ResourceNameAnnotation is the key for the annotation that contains the original name of this
// resource in the virtual cluster
ResourceNameAnnotation = "k3k.io/name"
// ResourceNamespaceAnnotation is the key for the annotation that contains the original namespace of this
// resource in the virtual cluster
ResourceNamespaceAnnotation = "k3k.io/namespace"
// MetadataNameField is the downwardapi field for object's name
MetadataNameField = "metadata.name"
// MetadataNamespaceField is the downward field for the object's namespace
MetadataNamespaceField = "metadata.namespace"
)
type ToHostTranslator struct {
// ClusterName is the name of the virtual cluster whose resources we are
// translating to a host cluster
ClusterName string
// ClusterNamespace is the namespace of the virtual cluster whose resources
// we are translating to a host cluster
ClusterNamespace string
}
// Translate translates a virtual cluster object to a host cluster object. This should only be used for
// static resources such as configmaps/secrets, and not for things like pods (which can reference other
// objects). Note that this won't set host-cluster values (like resource version) so when updating you
// may need to fetch the existing value and do some combination before using this.
func (t *ToHostTranslator) TranslateTo(obj client.Object) {
// owning objects may be in the virtual cluster, but may not be in the host cluster
obj.SetOwnerReferences(nil)
// add some annotations to make it easier to track source object
annotations := obj.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[ResourceNameAnnotation] = obj.GetName()
annotations[ResourceNamespaceAnnotation] = obj.GetNamespace()
obj.SetAnnotations(annotations)
// add a label to quickly identify objects owned by a given virtual cluster
labels := obj.GetLabels()
if labels == nil {
labels = map[string]string{}
}
labels[ClusterNameLabel] = t.ClusterName
obj.SetLabels(labels)
// resource version/UID won't match what's in the host cluster.
obj.SetResourceVersion("")
obj.SetUID("")
// set the name and the namespace so that this goes in the proper host namespace
// and doesn't collide with other resources
obj.SetName(t.TranslateName(obj.GetNamespace(), obj.GetName()))
obj.SetNamespace(t.ClusterNamespace)
obj.SetFinalizers(nil)
}
func (t *ToHostTranslator) TranslateFrom(obj client.Object) {
// owning objects may be in the virtual cluster, but may not be in the host cluster
obj.SetOwnerReferences(nil)
// remove the annotations added to track original name
annotations := obj.GetAnnotations()
// TODO: It's possible that this was erased by a change on the host cluster
// In this case, we need to have some sort of fallback or error return
name := annotations[ResourceNameAnnotation]
namespace := annotations[ResourceNamespaceAnnotation]
obj.SetName(name)
obj.SetNamespace(namespace)
delete(annotations, ResourceNameAnnotation)
delete(annotations, ResourceNamespaceAnnotation)
obj.SetAnnotations(annotations)
// remove the clusteName tracking label
labels := obj.GetLabels()
delete(labels, ClusterNameLabel)
obj.SetLabels(labels)
// resource version/UID won't match what's in the virtual cluster.
obj.SetResourceVersion("")
obj.SetUID("")
}
// TranslateName returns the name of the resource in the host cluster. Will not update the object with this name.
func (t *ToHostTranslator) TranslateName(namespace string, name string) string {
var names []string
// some resources are not namespaced (i.e. priorityclasses)
/// for these resources we skip the namespace to avoid having a name like: prioritclass--cluster-123
if namespace == "" {
names = []string{name, t.ClusterName}
} else {
names = []string{name, namespace, t.ClusterName}
}
// we need to come up with a name which is:
// - somewhat connectable to the original resource
// - a valid k8s name
// - idempotently calculatable
// - unique for this combination of name/namespace/cluster
namePrefix := strings.Join(names, "-")
// use + as a separator since it can't be in an object name
nameKey := strings.Join(names, "+")
// it's possible that the suffix will be in the name, so we use hex to make it valid for k8s
nameSuffix := hex.EncodeToString([]byte(nameKey))
return controller.SafeConcatName(namePrefix, nameSuffix)
}

133
main.go
View File

@@ -1,52 +1,145 @@
//go:generate ./hack/update-codegen.sh
//go:generate ./scripts/generate
package main
import (
"context"
"flag"
"errors"
"fmt"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
"github.com/rancher/k3k/pkg/controller/cluster"
"github.com/go-logr/zapr"
"github.com/spf13/cobra"
"go.uber.org/zap"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/klog/v2"
ctrl "sigs.k8s.io/controller-runtime"
ctrlconfig "sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/controller-runtime/pkg/manager"
v1 "k8s.io/api/core/v1"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
ctrlruntimelog "sigs.k8s.io/controller-runtime/pkg/log"
"github.com/rancher/k3k/cli/cmds"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
"github.com/rancher/k3k/pkg/buildinfo"
"github.com/rancher/k3k/pkg/controller/cluster"
"github.com/rancher/k3k/pkg/controller/cluster/agent"
"github.com/rancher/k3k/pkg/controller/policy"
"github.com/rancher/k3k/pkg/log"
)
var Scheme = runtime.NewScheme()
var (
scheme = runtime.NewScheme()
clusterCIDR string
sharedAgentImage string
sharedAgentImagePullPolicy string
kubeconfig string
k3SImage string
k3SImagePullPolicy string
kubeletPortRange string
webhookPortRange string
maxConcurrentReconciles int
debug bool
logger *log.Logger
)
func init() {
_ = clientgoscheme.AddToScheme(Scheme)
_ = v1alpha1.AddToScheme(Scheme)
_ = clientgoscheme.AddToScheme(scheme)
_ = v1alpha1.AddToScheme(scheme)
}
func main() {
ctrlconfig.RegisterFlags(nil)
flag.Parse()
rootCmd := &cobra.Command{
Use: "k3k",
Short: "k3k controller",
Version: buildinfo.Version,
PreRunE: func(cmd *cobra.Command, args []string) error {
return validate()
},
PersistentPreRun: func(cmd *cobra.Command, args []string) {
cmds.InitializeConfig(cmd)
logger = log.New(debug)
},
RunE: run,
}
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Debug level logging")
rootCmd.PersistentFlags().StringVar(&kubeconfig, "kubeconfig", "", "kubeconfig path")
rootCmd.PersistentFlags().StringVar(&clusterCIDR, "cluster-cidr", "", "Cluster CIDR to be added to the networkpolicy")
rootCmd.PersistentFlags().StringVar(&sharedAgentImage, "shared-agent-image", "", "K3K Virtual Kubelet image")
rootCmd.PersistentFlags().StringVar(&sharedAgentImagePullPolicy, "shared-agent-pull-policy", "", "K3K Virtual Kubelet image pull policy must be one of Always, IfNotPresent or Never")
rootCmd.PersistentFlags().StringVar(&kubeletPortRange, "kubelet-port-range", "50000-51000", "Port Range for k3k kubelet in shared mode")
rootCmd.PersistentFlags().StringVar(&webhookPortRange, "webhook-port-range", "51001-52000", "Port Range for k3k kubelet webhook in shared mode")
rootCmd.PersistentFlags().StringVar(&k3SImage, "k3s-image", "rancher/k3k", "K3K server image")
rootCmd.PersistentFlags().StringVar(&k3SImagePullPolicy, "k3s-image-pull-policy", "", "K3K server image pull policy")
rootCmd.PersistentFlags().IntVar(&maxConcurrentReconciles, "max-concurrent-reconciles", 50, "maximum number of concurrent reconciles")
if err := rootCmd.Execute(); err != nil {
logger.Fatalw("failed to run k3k controller", zap.Error(err))
}
}
func run(cmd *cobra.Command, args []string) error {
ctx := context.Background()
kubeconfig := flag.Lookup("kubeconfig").Value.String()
logger.Info("Starting k3k - Version: " + buildinfo.Version)
restConfig, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
klog.Fatalf("Failed to create config from kubeconfig file: %v", err)
return fmt.Errorf("failed to create config from kubeconfig file: %v", err)
}
mgr, err := ctrl.NewManager(restConfig, manager.Options{
Scheme: Scheme,
Scheme: scheme,
})
if err != nil {
klog.Fatalf("Failed to create new controller runtime manager: %v", err)
return fmt.Errorf("failed to create new controller runtime manager: %v", err)
}
if err := cluster.Add(ctx, mgr); err != nil {
klog.Fatalf("Failed to add the new controller: %v", err)
ctrlruntimelog.SetLogger(zapr.NewLogger(logger.Desugar().WithOptions(zap.AddCallerSkip(1))))
logger.Info("adding cluster controller")
portAllocator, err := agent.NewPortAllocator(ctx, mgr.GetClient())
if err != nil {
return err
}
runnable := portAllocator.InitPortAllocatorConfig(ctx, mgr.GetClient(), kubeletPortRange, webhookPortRange)
if err := mgr.Add(runnable); err != nil {
return err
}
if err := cluster.Add(ctx, mgr, sharedAgentImage, sharedAgentImagePullPolicy, k3SImage, k3SImagePullPolicy, maxConcurrentReconciles, portAllocator, nil); err != nil {
return fmt.Errorf("failed to add the new cluster controller: %v", err)
}
logger.Info("adding etcd pod controller")
if err := cluster.AddPodController(ctx, mgr, maxConcurrentReconciles); err != nil {
return fmt.Errorf("failed to add the new cluster controller: %v", err)
}
logger.Info("adding clusterpolicy controller")
if err := policy.Add(mgr, clusterCIDR, maxConcurrentReconciles); err != nil {
return fmt.Errorf("failed to add the clusterpolicy controller: %v", err)
}
if err := mgr.Start(ctx); err != nil {
klog.Fatalf("Failed to start the manager: %v", err)
return fmt.Errorf("failed to start the manager: %v", err)
}
return nil
}
func validate() error {
if sharedAgentImagePullPolicy != "" {
if sharedAgentImagePullPolicy != string(v1.PullAlways) &&
sharedAgentImagePullPolicy != string(v1.PullIfNotPresent) &&
sharedAgentImagePullPolicy != string(v1.PullNever) {
return errors.New("invalid value for shared agent image policy")
}
}
return nil
}

View File

@@ -1,6 +0,0 @@
image: rancher/k3k:{{replace "+" "-" build.tag}}
manifests:
- image: rancher/k3k:{{replace "+" "-" build.tag}}-amd64
platform:
architecture: amd64
os: linux

Some files were not shown because too many files have changed in this diff Show More