Compare commits

..

12 Commits

Author SHA1 Message Date
Dario Tranchitella
e34fc1851f chore(helm): releasing v0.3.3 2023-08-08 12:07:30 +02:00
Dario Tranchitella
740fe9c938 chore(kustomize): releasing v0.3.3 2023-08-08 12:07:30 +02:00
Dario Tranchitella
65854721de fix(ingress): referencing ingress port from hostname 2023-08-08 10:55:33 +02:00
geoffrey1330
adde828e03 docs: added default label to TCP resources 2023-08-08 09:56:54 +02:00
Dario Tranchitella
ffc2c7c967 fix(gh): triggering e2e upon cmd changes 2023-08-03 18:04:07 +02:00
Dario Tranchitella
0f195286a7 docs(manager): cache resync period 2023-08-03 18:04:07 +02:00
Dario Tranchitella
f768f93fe9 feat: cache resync period 2023-08-03 18:04:07 +02:00
Dario Tranchitella
05cbff1fd8 docs: kubeconfig and certificates rotation 2023-08-03 18:03:54 +02:00
Dario Tranchitella
7e94ecdbab feat: kubeconfig and certificates rotation 2023-08-03 18:03:54 +02:00
Dario Tranchitella
648da19687 refactor: checking kubeconfig user certs validity 2023-08-03 18:03:54 +02:00
Dario Tranchitella
6c4b339c4b fix(typo): error message for kubeconfig 2023-08-03 18:03:54 +02:00
Dario Tranchitella
eee62032de refactor: ensuring owner reference and labels with controller label 2023-08-03 18:03:54 +02:00
38 changed files with 574 additions and 73 deletions

View File

@@ -13,6 +13,7 @@ on:
- 'main.go'
- 'Makefile'
- 'internal/**'
- 'cmd/**'
pull_request:
branches: [ "*" ]
paths:
@@ -25,6 +26,7 @@ on:
- 'main.go'
- 'Makefile'
- 'internal/**'
- 'cmd/**'
jobs:
kind:

View File

@@ -3,7 +3,7 @@
# To re-generate a bundle for another specific version without changing the standard setup, you can:
# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2)
# - use environment variables to overwrite this value (e.g export VERSION=0.0.2)
VERSION ?= 0.3.2
VERSION ?= 0.3.3
# CHANNELS define the bundle channels used in the bundle.
# Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable")

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: v0.3.2
appVersion: v0.3.3
description: Kamaji deploys and operates Kubernetes at scale with a fraction of the operational burden. Kamaji turns any Kubernetes cluster into an “admin cluster” to orchestrate other Kubernetes clusters called “tenant clusters”. Kamaji is special because the Control Plane components are running in a single pod instead of dedicated machines. This solution makes running multiple Control Planes cheaper and easier to deploy and operate.
home: https://github.com/clastix/kamaji
icon: https://github.com/clastix/kamaji/raw/master/assets/logo-colored.png
@@ -15,7 +15,7 @@ name: kamaji
sources:
- https://github.com/clastix/kamaji
type: application
version: 0.12.3
version: 0.12.4
annotations:
catalog.cattle.io/certified: partner
catalog.cattle.io/release-name: kamaji

View File

@@ -1,6 +1,6 @@
# kamaji
![Version: 0.12.3](https://img.shields.io/badge/Version-0.12.3-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v0.3.2](https://img.shields.io/badge/AppVersion-v0.3.2-informational?style=flat-square)
![Version: 0.12.4](https://img.shields.io/badge/Version-0.12.4-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v0.3.3](https://img.shields.io/badge/AppVersion-v0.3.3-informational?style=flat-square)
Kamaji deploys and operates Kubernetes at scale with a fraction of the operational burden. Kamaji turns any Kubernetes cluster into an “admin cluster” to orchestrate other Kubernetes clusters called “tenant clusters”. Kamaji is special because the Control Plane components are running in a single pod instead of dedicated machines. This solution makes running multiple Control Planes cheaper and easier to deploy and operate.

View File

@@ -14,8 +14,10 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
@@ -31,6 +33,7 @@ import (
"github.com/clastix/kamaji/internal/webhook/routes"
)
//nolint:maintidx
func NewCmd(scheme *runtime.Scheme) *cobra.Command {
// CLI flags
var (
@@ -40,6 +43,7 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
tmpDirectory string
kineImage string
controllerReconcileTimeout time.Duration
cacheResyncPeriod time.Duration
datastore string
managerNamespace string
managerServiceAccountName string
@@ -98,6 +102,11 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
LeaderElection: leaderElect,
LeaderElectionNamespace: managerNamespace,
LeaderElectionID: "799b98bc.clastix.io",
NewCache: func(config *rest.Config, opts cache.Options) (cache.Cache, error) {
opts.Resync = &cacheResyncPeriod
return cache.New(config, opts)
},
})
if err != nil {
setupLog.Error(err, "unable to start manager")
@@ -105,7 +114,7 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
return err
}
tcpChannel := make(controllers.TenantControlPlaneChannel)
tcpChannel, certChannel := make(controllers.TenantControlPlaneChannel), make(controllers.CertificateChannel)
if err = (&controllers.DataStore{TenantControlPlaneTrigger: tcpChannel}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "DataStore")
@@ -122,6 +131,7 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
KineContainerImage: kineImage,
TmpBaseDirectory: tmpDirectory,
},
CertificateChan: certChannel,
TriggerChan: tcpChannel,
KamajiNamespace: managerNamespace,
KamajiServiceAccount: managerServiceAccountName,
@@ -136,6 +146,12 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
return err
}
if err = (&controllers.CertificateLifecycle{Channel: certChannel}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "CertificateLifecycle")
return err
}
if err = (&kamajiv1alpha1.DatastoreUsedSecret{}).SetupWithManager(ctx, mgr); err != nil {
setupLog.Error(err, "unable to create indexer", "indexer", "DatastoreUsedSecret")
@@ -239,6 +255,7 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
cmd.Flags().StringVar(&managerServiceAccountName, "serviceaccount-name", os.Getenv("SERVICE_ACCOUNT"), "The Kubernetes Namespace on which the Operator is running in, required for the TenantControlPlane migration jobs.")
cmd.Flags().StringVar(&webhookCAPath, "webhook-ca-path", "/tmp/k8s-webhook-server/serving-certs/ca.crt", "Path to the Manager webhook server CA, required for the TenantControlPlane migration jobs.")
cmd.Flags().DurationVar(&controllerReconcileTimeout, "controller-reconcile-timeout", 30*time.Second, "The reconciliation request timeout before the controller withdraw the external resource calls, such as dealing with the Datastore, or the Tenant Control Plane API endpoint.")
cmd.Flags().DurationVar(&cacheResyncPeriod, "cache-resync-period", 10*time.Hour, "The controller-runtime.Manager cache resync period.")
cobra.OnInitialize(func() {
viper.AutomaticEnv()

View File

@@ -5062,7 +5062,7 @@ spec:
valueFrom:
fieldRef:
fieldPath: spec.serviceAccountName
image: clastix/kamaji:v0.3.2
image: clastix/kamaji:v0.3.3
imagePullPolicy: Always
livenessProbe:
httpGet:

View File

@@ -13,4 +13,4 @@ kind: Kustomization
images:
- name: controller
newName: clastix/kamaji
newTag: v0.3.2
newTag: v0.3.3

View File

@@ -2,6 +2,8 @@ apiVersion: kamaji.clastix.io/v1alpha1
kind: TenantControlPlane
metadata:
name: k8s-126
labels:
tenant.clastix.io: k8s-126
spec:
controlPlane:
deployment:

View File

@@ -2,6 +2,8 @@ apiVersion: kamaji.clastix.io/v1alpha1
kind: TenantControlPlane
metadata:
name: additionalcontainers
labels:
tenant.clastix.io: additionalcontainers
spec:
dataStore: postgresql-bronze
controlPlane:

View File

@@ -2,6 +2,8 @@ apiVersion: kamaji.clastix.io/v1alpha1
kind: TenantControlPlane
metadata:
name: additional-volumes
labels:
tenant.clastix.io: additional-volumes
spec:
controlPlane:
deployment:

View File

@@ -2,6 +2,8 @@ apiVersion: kamaji.clastix.io/v1alpha1
kind: TenantControlPlane
metadata:
name: kine
labels:
tenant.clastix.io: kine
spec:
addons:
coreDNS: {}

View File

@@ -2,6 +2,8 @@ apiVersion: kamaji.clastix.io/v1alpha1
kind: TenantControlPlane
metadata:
name: konnectivity-addon
labels:
tenant.clastix.io: konnectivity-addon
spec:
deployment:
replicas: 2

View File

@@ -0,0 +1,10 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package controllers
import (
"sigs.k8s.io/controller-runtime/pkg/event"
)
type CertificateChannel chan event.GenericEvent

View File

@@ -0,0 +1,160 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package controllers
import (
"context"
"crypto/x509"
"fmt"
"time"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
clientcmdapiv1 "k8s.io/client-go/tools/clientcmd/api/v1"
controllerruntime "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/constants"
"github.com/clastix/kamaji/internal/crypto"
"github.com/clastix/kamaji/internal/utilities"
)
type CertificateLifecycle struct {
Channel CertificateChannel
client client.Client
}
func (s *CertificateLifecycle) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
logger := log.FromContext(ctx)
logger.Info("starting CertificateLifecycle handling")
secret := corev1.Secret{}
if err := s.client.Get(ctx, request.NamespacedName, &secret); err != nil {
if k8serrors.IsNotFound(err) {
logger.Info("resource may have been deleted, skipping")
return reconcile.Result{}, nil
}
}
checkType, ok := secret.GetLabels()[constants.ControllerLabelResource]
if !ok {
logger.Info("missing controller label, shouldn't happen")
return reconcile.Result{}, nil
}
var crt *x509.Certificate
var err error
switch checkType {
case "x509":
crt, err = s.extractCertificateFromBareSecret(secret)
case "kubeconfig":
crt, err = s.extractCertificateFromKubeconfig(secret)
default:
err = fmt.Errorf("unsupported strategy, %s", checkType)
}
if err != nil {
logger.Error(err, "skipping reconciliation")
return reconcile.Result{}, nil
}
deadline := time.Now().AddDate(0, 0, 1)
if deadline.After(crt.NotAfter) {
logger.Info("certificate near expiration, must be rotated")
s.Channel <- event.GenericEvent{Object: &kamajiv1alpha1.TenantControlPlane{
ObjectMeta: metav1.ObjectMeta{
Name: secret.GetOwnerReferences()[0].Name,
Namespace: secret.Namespace,
},
}}
logger.Info("certificate rotation triggered")
return reconcile.Result{}, nil
}
after := crt.NotAfter.Sub(deadline)
logger.Info("certificate is still valid, enqueuing back", "after", after.String())
return reconcile.Result{Requeue: true, RequeueAfter: after}, nil
}
func (s *CertificateLifecycle) extractCertificateFromBareSecret(secret corev1.Secret) (*x509.Certificate, error) {
var crt *x509.Certificate
var err error
for _, v := range secret.Data {
if crt, err = crypto.ParseCertificateBytes(v); err == nil {
break
}
}
if crt == nil {
return nil, fmt.Errorf("none of the provided keys is containing a valid x509 certificate")
}
return crt, nil
}
func (s *CertificateLifecycle) extractCertificateFromKubeconfig(secret corev1.Secret) (*x509.Certificate, error) {
var kc *clientcmdapiv1.Config
var err error
for k := range secret.Data {
if kc, err = utilities.DecodeKubeconfig(secret, k); err == nil {
break
}
}
if kc == nil {
return nil, fmt.Errorf("none of the provided keys is containing a valid kubeconfig")
}
crt, err := crypto.ParseCertificateBytes(kc.AuthInfos[0].AuthInfo.ClientCertificateData)
if err != nil {
return nil, errors.Wrap(err, "cannot parse kubeconfig certificate bytes")
}
return crt, nil
}
func (s *CertificateLifecycle) SetupWithManager(mgr controllerruntime.Manager) error {
s.client = mgr.GetClient()
supportedStrategies := sets.New[string]("x509", "kubeconfig")
return controllerruntime.NewControllerManagedBy(mgr).
For(&corev1.Secret{}, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
labels := object.GetLabels()
if labels == nil {
return false
}
value, ok := labels[constants.ControllerLabelResource]
if !ok {
return false
}
return supportedStrategies.Has(value)
}))).
Complete(s)
}

View File

@@ -50,6 +50,10 @@ type TenantControlPlaneReconciler struct {
KamajiService string
KamajiMigrateImage string
MaxConcurrentReconciles int
// CertificateChan is the channel used by the CertificateLifecycleController that is checking for
// certificates and kubeconfig user certs validity: a generic event for the given TCP will be triggered
// once the validity threshold for the given certificate is reached.
CertificateChan CertificateChannel
clock mutex.Clock
}
@@ -223,6 +227,14 @@ func (r *TenantControlPlaneReconciler) SetupWithManager(mgr ctrl.Manager) error
r.clock = clock.RealClock{}
return ctrl.NewControllerManagedBy(mgr).
Watches(&source.Channel{Source: r.CertificateChan}, handler.Funcs{GenericFunc: func(genericEvent event.GenericEvent, limitingInterface workqueue.RateLimitingInterface) {
limitingInterface.AddRateLimited(ctrl.Request{
NamespacedName: k8stypes.NamespacedName{
Namespace: genericEvent.Object.GetNamespace(),
Name: genericEvent.Object.GetName(),
},
})
}}).
Watches(&source.Channel{Source: r.TriggerChan}, handler.Funcs{GenericFunc: func(genericEvent event.GenericEvent, limitingInterface workqueue.RateLimitingInterface) {
limitingInterface.AddRateLimited(ctrl.Request{
NamespacedName: k8stypes.NamespacedName{

View File

@@ -99,6 +99,8 @@ kind: TenantControlPlane
metadata:
name: ${TENANT_NAME}
namespace: ${TENANT_NAMESPACE}
labels:
tenant.clastix.io: ${TENANT_NAME}
spec:
dataStore: default
controlPlane:

View File

@@ -0,0 +1,143 @@
# Certificates lifecycle
Kamaji is responsible for creating the required certificates, such as:
- the Kubernetes API Server certificate
- the Kubernetes API Server kubelet client certificate
- the Datastore certificate
- the front proxy client certificate
- the konnectivity certificate (if enabled)
Also, the following `kubeconfig` resources contain client certificates, which are created by Kamaji, such as:
- `admin`
- `controller-manager`
- `konnectivity` (if enabled)
- `scheduler`
All the certificates are created with the `kubeadm` defaults, thus their validity is set to 1 year.
## How to rotate certificates
If you need to manually rotate one of these certificates, the required operation is the deletion for the given Secret.
```
$: kubectl get secret
NAME TYPE DATA AGE
k8s-126-admin-kubeconfig Opaque 1 12m
k8s-126-api-server-certificate Opaque 2 12m
k8s-126-api-server-kubelet-client-certificate Opaque 2 3h45m
k8s-126-ca Opaque 4 3h45m
k8s-126-controller-manager-kubeconfig Opaque 1 3h45m
k8s-126-datastore-certificate Opaque 3 3h45m
k8s-126-datastore-config Opaque 4 3h45m
k8s-126-front-proxy-ca-certificate Opaque 2 3h45m
k8s-126-front-proxy-client-certificate Opaque 2 3h45m
k8s-126-konnectivity-certificate kubernetes.io/tls 2 3h45m
k8s-126-konnectivity-kubeconfig Opaque 1 3h45m
k8s-126-sa-certificate Opaque 2 3h45m
k8s-126-scheduler-kubeconfig Opaque 1 3h45m
```
Once this operation is performed, Kamaji will be notified of the missing certificate, and it will create it back.
```
$: kubectl delete secret -l kamaji.clastix.io/certificate_lifecycle_controller=x509
secret "k8s-126-api-server-certificate" deleted
secret "k8s-126-api-server-kubelet-client-certificate" deleted
secret "k8s-126-front-proxy-client-certificate" deleted
secret "k8s-126-konnectivity-certificate" deleted
$: kubectl delete secret -l kamaji.clastix.io/certificate_lifecycle_controller=x509
NAME TYPE DATA AGE
k8s-126-admin-kubeconfig Opaque 1 15m
k8s-126-api-server-certificate Opaque 2 12s
k8s-126-api-server-kubelet-client-certificate Opaque 2 12s
k8s-126-ca Opaque 4 3h48m
k8s-126-controller-manager-kubeconfig Opaque 1 3h48m
k8s-126-datastore-certificate Opaque 3 3h48m
k8s-126-datastore-config Opaque 4 3h48m
k8s-126-front-proxy-ca-certificate Opaque 2 3h48m
k8s-126-front-proxy-client-certificate Opaque 2 12s
k8s-126-konnectivity-certificate kubernetes.io/tls 2 11s
k8s-126-konnectivity-kubeconfig Opaque 1 3h48m
k8s-126-sa-certificate Opaque 2 3h48m
k8s-126-scheduler-kubeconfig Opaque 1 3h48m
```
You can notice the secrets have been automatically created back, as well as a TenantControlPlane rollout with the updated certificates.
```
$: kubectl get pods
NAME READY STATUS RESTARTS AGE
k8s-126-76768bdf89-82w8g 4/4 Running 0 58s
k8s-126-76768bdf89-fwltl 4/4 Running 0 58s
```
The same occurs with the `kubeconfig` ones.
```
$: kubectl delete secret -l kamaji.clastix.io/certificate_lifecycle_controller=kubeconfig
secret "k8s-126-admin-kubeconfig" deleted
secret "k8s-126-controller-manager-kubeconfig" deleted
secret "k8s-126-konnectivity-kubeconfig" deleted
secret "k8s-126-scheduler-kubeconfig" deleted
$: kubectl get pods
NAME READY STATUS RESTARTS AGE
k8s-126-576c775b5d-2gr9h 4/4 Running 0 50s
k8s-126-576c775b5d-jmvlm 4/4 Running 0 50s
```
## Automatic certificates rotation
The Kamaji operator will run a controller which processes all the Secrets to determine their expiration, both for the `kubeconfig`, as well as for the certificates.
The controller, named `CertificateLifecycle`, will extract the certificates from the _Secret_ objects notifying the `TenantControlPlaneReconciler` controller which will start a new certificate rotation.
The rotation will occur the day before their expiration.
> Nota Bene:
>
> Kamaji is responsible for creating the `etcd` client certificate, and the generation of a new one will occur.
> For other Datastore drivers, such as MySQL or PostgreSQL, the referenced Secret will always be deleted by the Controller to trigger the rotation:
> the PKI management, since it's offloaded externally, must provide the renewed certificates.
## Certificate Authority rotation
Kamaji is also taking care of your tenant clusters Certificate Authority.
This can be rotated manually by deleting the following secret.
```
$: kubectl delete secret k8s-126-ca
secret "k8s-126-ca" deleted
```
Once this occurs the TenantControlPlane will enter in the `CertificateAuthorityRotating` status.
```
$: kubectl get tcp -w
NAME VERSION STATUS CONTROL-PLANE ENDPOINT KUBECONFIG DATASTORE AGE
k8s-126 v1.26.0 Ready 172.18.255.200:6443 k8s-126-admin-kubeconfig default 3h58m
k8s-126 v1.26.0 CertificateAuthorityRotating 172.18.255.200:6443 k8s-126-admin-kubeconfig default 3h58m
k8s-126 v1.26.0 CertificateAuthorityRotating 172.18.255.200:6443 k8s-126-admin-kubeconfig default 3h58m
k8s-126 v1.26.0 CertificateAuthorityRotating 172.18.255.200:6443 k8s-126-admin-kubeconfig default 3h58m
k8s-126 v1.26.0 CertificateAuthorityRotating 172.18.255.200:6443 k8s-126-admin-kubeconfig default 3h58m
k8s-126 v1.26.0 CertificateAuthorityRotating 172.18.255.200:6443 k8s-126-admin-kubeconfig default 3h58m
k8s-126 v1.26.0 CertificateAuthorityRotating 172.18.255.200:6443 k8s-126-admin-kubeconfig default 3h58m
k8s-126 v1.26.0 CertificateAuthorityRotating 172.18.255.200:6443 k8s-126-admin-kubeconfig default 3h58m
k8s-126 v1.26.0 CertificateAuthorityRotating 172.18.255.200:6443 k8s-126-admin-kubeconfig default 3h58m
k8s-126 v1.26.0 CertificateAuthorityRotating 172.18.255.200:6443 k8s-126-admin-kubeconfig default 3h58m
k8s-126 v1.26.0 CertificateAuthorityRotating 172.18.255.200:6443 k8s-126-admin-kubeconfig default 3h58m
k8s-126 v1.26.0 CertificateAuthorityRotating 172.18.255.200:6443 k8s-126-admin-kubeconfig default 3h58m
k8s-126 v1.26.0 Ready 172.18.255.200:6443 k8s-126-admin-kubeconfig default 3h58m
k8s-126 v1.26.0 Ready 172.18.255.200:6443 k8s-126-admin-kubeconfig default 3h58m
k8s-126 v1.26.0 Ready 172.18.255.200:6443 k8s-126-admin-kubeconfig default 3h58m
k8s-126 v1.26.0 Ready 172.18.255.200:6443 k8s-126-admin-kubeconfig default 3h58m
k8s-126 v1.26.0 Ready 172.18.255.200:6443 k8s-126-admin-kubeconfig default 3h58m
```
This operation is intended to be performed manually since a new Certificate Authority requires the restart of all the components, as well as of the nodes:
in such case, you will need to distribute the new Certificate Authority and the new nodes certificates.
Given the sensibility of such operation, the `Secret` controller will not check the _CA_, which is offering validity of 10 years as `kubeadm` default values.

View File

@@ -144,6 +144,8 @@ kind: TenantControlPlane
metadata:
name: ${TENANT_NAME}
namespace: ${TENANT_NAMESPACE}
labels:
tenant.clastix.io: ${TENANT_NAME}
spec:
dataStore: default
controlPlane:

View File

@@ -14,6 +14,8 @@ apiVersion: kamaji.clastix.io/v1alpha1
kind: TenantControlPlane
metadata:
name: tenant-00
labels:
tenant.clastix.io: tenant-00
spec:
controlPlane:
deployment:

View File

@@ -191,6 +191,8 @@ kind: TenantControlPlane
metadata:
name: benchmark$I
namespace: $NS
labels:
tenant.clastix.io: benchmark$I
spec:
dataStore: $DS
controlPlane:

View File

@@ -4,22 +4,24 @@ Currently, **Kamaji** allows customization using CLI flags for the `manager` sub
Available flags are the following:
| Flag | Usage | Default |
| ---- | ------ | --- |
| `--metrics-bind-address` | The address the metric endpoint binds to. | `:8080` |
| `--health-probe-bind-address` | The address the probe endpoint binds to. | `:8081` |
| `--leader-elect` | Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager. | `true` |
| `--tmp-directory` | Directory which will be used to work with temporary files. | `/tmp/kamaji` |
| `--kine-image` | Container image along with tag to use for the Kine sidecar container (used only if etcd-storage-type is set to one of kine strategies). | `rancher/kine:v0.9.2-amd64` |
| `--datastore` | The default DataStore that should be used by Kamaji to setup the required storage. | `etcd` |
| `--migrate-image` | Specify the container image to launch when a TenantControlPlane is migrated to a new datastore. | `migrate-image` |
| `--max-concurrent-tcp-reconciles` | Specify the number of workers for the Tenant Control Plane controller (beware of CPU consumption). | `1` |
| `--pod-namespace` | The Kubernetes Namespace on which the Operator is running in, required for the TenantControlPlane migration jobs. | `os.Getenv("POD_NAMESPACE")` |
| `--webhook-service-name` | The Kamaji webhook server Service name which is used to get validation webhooks, required for the TenantControlPlane migration jobs. | `kamaji-webhook-service` |
| `--serviceaccount-name` | The Kubernetes ServiceAccount used by the Operator, required for the TenantControlPlane migration jobs. | `os.Getenv("SERVICE_ACCOUNT")` |
| `--webhook-ca-path` | Path to the Manager webhook server CA, required for the TenantControlPlane migration jobs. | `/tmp/k8s-webhook-server/serving-certs/ca.crt` |
| `--zap-devel` | Development Mode (encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode (encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error). | `true` |
| `--zap-encoder` | Zap log encoding, one of 'json' or 'console' | `console` |
| `--zap-log-level` | Zap Level to configure the verbosity of logging. Can be one of 'debug', 'info', 'error', or any integer value > 0 which corresponds to custom debug levels of increasing verbosity | `info` |
| `--zap-stacktrace-level` | Zap Level at and above which stacktraces are captured (one of 'info', 'error', 'panic'). | `info` |
| `--zap-time-encoding` | Zap time encoding (one of 'epoch', 'millis', 'nano', 'iso8601', 'rfc3339' or 'rfc3339nano') | `epoch` |
| Flag | Usage | Default |
|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------|
| `--metrics-bind-address` | The address the metric endpoint binds to. | `:8080` |
| `--health-probe-bind-address` | The address the probe endpoint binds to. | `:8081` |
| `--leader-elect` | Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager. | `true` |
| `--tmp-directory` | Directory which will be used to work with temporary files. | `/tmp/kamaji` |
| `--kine-image` | Container image along with tag to use for the Kine sidecar container (used only if etcd-storage-type is set to one of kine strategies). | `rancher/kine:v0.9.2-amd64` |
| `--datastore` | The default DataStore that should be used by Kamaji to setup the required storage. | `etcd` |
| `--migrate-image` | Specify the container image to launch when a TenantControlPlane is migrated to a new datastore. | `migrate-image` |
| `--max-concurrent-tcp-reconciles` | Specify the number of workers for the Tenant Control Plane controller (beware of CPU consumption). | `1` |
| `--pod-namespace` | The Kubernetes Namespace on which the Operator is running in, required for the TenantControlPlane migration jobs. | `os.Getenv("POD_NAMESPACE")` |
| `--webhook-service-name` | The Kamaji webhook server Service name which is used to get validation webhooks, required for the TenantControlPlane migration jobs. | `kamaji-webhook-service` |
| `--serviceaccount-name` | The Kubernetes ServiceAccount used by the Operator, required for the TenantControlPlane migration jobs. | `os.Getenv("SERVICE_ACCOUNT")` |
| `--webhook-ca-path` | Path to the Manager webhook server CA, required for the TenantControlPlane migration jobs. | `/tmp/k8s-webhook-server/serving-certs/ca.crt` |
| `--controller-reconcile-timeout` | The reconciliation request timeout before the controller withdraw the external resource calls, such as dealing with the Datastore, or the Tenant Control Plane API endpoint. | `30s` |
| `--cache-resync-period` | The controller-runtime.Manager cache resync period. | `10h` |
| `--zap-devel` | Development Mode (encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode (encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error). | `true` |
| `--zap-encoder` | Zap log encoding, one of 'json' or 'console' | `console` |
| `--zap-log-level` | Zap Level to configure the verbosity of logging. Can be one of 'debug', 'info', 'error', or any integer value > 0 which corresponds to custom debug levels of increasing verbosity | `info` |
| `--zap-stacktrace-level` | Zap Level at and above which stacktraces are captured (one of 'info', 'error', 'panic'). | `info` |
| `--zap-time-encoding` | Zap time encoding (one of 'epoch', 'millis', 'nano', 'iso8601', 'rfc3339' or 'rfc3339nano') | `epoch` |

View File

@@ -10,3 +10,4 @@ In Kamaji, there are different components that might require independent version
| v0.3.0 | v1.22+ | [v1.21.0 .. v1.27.0] |
| v0.3.1 | v1.22+ | [v1.21.0 .. v1.27.3] |
| v0.3.2 | v1.22+ | [v1.21.0 .. v1.27.3] |
| v0.3.2 | v1.22+ | [v1.21.0 .. v1.27.3] |

View File

@@ -65,6 +65,7 @@ nav:
- guides/upgrade.md
- guides/datastore-migration.md
- guides/backup-and-restore.md
- guides/certs-lifecycle.md
- 'Use Cases': use-cases.md
- 'Reference':
- reference/index.md

View File

@@ -9,4 +9,5 @@ const (
ControlPlaneLabelKey = "kamaji.clastix.io/name"
ControlPlaneLabelResource = "kamaji.clastix.io/component"
ControllerLabelResource = "kamaji.clastix.io/certificate_lifecycle_controller"
)

View File

@@ -194,9 +194,11 @@ func generateCertificateKeyPairBytes(template *x509.Certificate, caCert *x509.Ce
}
func checkCertificateValidity(cert x509.Certificate) bool {
now := time.Now()
// Avoiding waiting for the exact expiration date by creating a one-day gap
notAfter := cert.NotAfter.After(time.Now().AddDate(0, 0, 1))
notBefore := cert.NotBefore.Before(time.Now())
return now.Before(cert.NotAfter) && now.After(cert.NotBefore)
return notAfter && notBefore
}
func checkPublicKeys(a rsa.PublicKey, b rsa.PublicKey) bool {

View File

@@ -10,6 +10,9 @@ import (
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig"
"github.com/clastix/kamaji/internal/crypto"
"github.com/clastix/kamaji/internal/utilities"
)
func buildCertificateDirectoryWithCA(ca CertificatePrivateKeyPair, directory string) error {
@@ -46,6 +49,13 @@ func CreateKubeconfig(kubeconfigName string, ca CertificatePrivateKeyPair, confi
return os.ReadFile(path)
}
func IsKubeconfigValid(kubeconfigBytes []byte) bool {
return len(kubeconfigBytes) > 0
func IsKubeconfigValid(bytes []byte) bool {
kc, err := utilities.DecodeKubeconfigYAML(bytes)
if err != nil {
return false
}
ok, _ := crypto.IsValidCertificateKeyPairBytes(kc.AuthInfos[0].AuthInfo.ClientCertificateData, kc.AuthInfos[0].AuthInfo.ClientKeyData)
return ok
}

View File

@@ -18,6 +18,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/constants"
"github.com/clastix/kamaji/internal/crypto"
"github.com/clastix/kamaji/internal/kubeadm"
"github.com/clastix/kamaji/internal/utilities"
@@ -93,6 +94,19 @@ func (r *APIServerCertificate) mutate(ctx context.Context, tenantControlPlane *k
return err
}
r.resource.SetLabels(utilities.MergeMaps(
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
map[string]string{
constants.ControllerLabelResource: "x509",
},
))
if err := ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme()); err != nil {
logger.Error(err, "cannot set controller reference", "resource", r.GetName())
return err
}
if checksum := tenantControlPlane.Status.Certificates.APIServer.Checksum; len(checksum) > 0 && checksum == utilities.GetObjectChecksum(r.resource) || len(r.resource.UID) > 0 {
isCAValid, err := crypto.VerifyCertificate(r.resource.Data[kubeadmconstants.APIServerCertName], secretCA.Data[kubeadmconstants.CACertName], x509.ExtKeyUsageServerAuth)
if err != nil {
@@ -138,8 +152,6 @@ func (r *APIServerCertificate) mutate(ctx context.Context, tenantControlPlane *k
utilities.SetObjectChecksum(r.resource, r.resource.Data)
r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
return nil
}
}

View File

@@ -18,6 +18,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/constants"
"github.com/clastix/kamaji/internal/crypto"
"github.com/clastix/kamaji/internal/kubeadm"
"github.com/clastix/kamaji/internal/utilities"
@@ -93,6 +94,19 @@ func (r *APIServerKubeletClientCertificate) mutate(ctx context.Context, tenantCo
return err
}
r.resource.SetLabels(utilities.MergeMaps(
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
map[string]string{
constants.ControllerLabelResource: "x509",
},
))
if err := ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme()); err != nil {
logger.Error(err, "cannot set controller reference", "resource", r.GetName())
return err
}
if checksum := tenantControlPlane.Status.Certificates.APIServerKubeletClient.Checksum; len(checksum) > 0 && checksum == utilities.GetObjectChecksum(r.resource) || len(r.resource.UID) > 0 {
isCAValid, err := crypto.VerifyCertificate(r.resource.Data[kubeadmconstants.APIServerKubeletClientCertName], secretCA.Data[kubeadmconstants.CACertName], x509.ExtKeyUsageClientAuth)
if err != nil {
@@ -136,10 +150,8 @@ func (r *APIServerKubeletClientCertificate) mutate(ctx context.Context, tenantCo
kubeadmconstants.APIServerKubeletClientKeyName: certificateKeyPair.PrivateKey,
}
r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
utilities.SetObjectChecksum(r.resource, r.resource.Data)
return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
return nil
}
}

View File

@@ -16,6 +16,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/constants"
"github.com/clastix/kamaji/internal/crypto"
"github.com/clastix/kamaji/internal/utilities"
)
@@ -88,6 +89,19 @@ func (r *Certificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1al
r.resource.Data["ca.crt"] = ca
r.resource.SetLabels(utilities.MergeMaps(
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
map[string]string{
constants.ControllerLabelResource: "x509",
},
))
if err = ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme()); err != nil {
logger.Error(err, "cannot set controller reference", "resource", r.GetName())
return err
}
if utilities.GetObjectChecksum(r.resource) == utilities.CalculateMapChecksum(r.resource.Data) {
if r.DataStore.Spec.Driver == kamajiv1alpha1.EtcdDriver {
if isValid, _ := crypto.IsValidCertificateKeyPairBytes(r.resource.Data["server.crt"], r.resource.Data["server.key"]); isValid {
@@ -141,11 +155,6 @@ func (r *Certificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1al
utilities.SetObjectChecksum(r.resource, r.resource.Data)
r.resource.SetLabels(utilities.MergeMaps(
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
r.resource.GetLabels(),
))
return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
return nil
}
}

View File

@@ -18,6 +18,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/constants"
"github.com/clastix/kamaji/internal/crypto"
"github.com/clastix/kamaji/internal/kubeadm"
"github.com/clastix/kamaji/internal/utilities"
@@ -92,6 +93,20 @@ func (r *FrontProxyClientCertificate) mutate(ctx context.Context, tenantControlP
return err
}
r.resource.SetLabels(utilities.MergeMaps(
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
map[string]string{
constants.ControllerLabelResource: "x509",
},
))
if err := ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme()); err != nil {
logger.Error(err, "cannot set controller reference", "resource", r.GetName())
return err
}
if checksum := tenantControlPlane.Status.Certificates.FrontProxyClient.Checksum; len(checksum) > 0 && checksum == utilities.GetObjectChecksum(r.resource) || len(r.resource.UID) > 0 {
isCAValid, err := crypto.VerifyCertificate(r.resource.Data[kubeadmconstants.FrontProxyClientCertName], secretCA.Data[kubeadmconstants.FrontProxyCACertName], x509.ExtKeyUsageClientAuth)
if err != nil {
@@ -135,10 +150,8 @@ func (r *FrontProxyClientCertificate) mutate(ctx context.Context, tenantControlP
kubeadmconstants.FrontProxyClientKeyName: certificateKeyPair.PrivateKey,
}
r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
utilities.SetObjectChecksum(r.resource, r.resource.Data)
return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
return nil
}
}

View File

@@ -147,7 +147,7 @@ func (r *KubernetesIngressResource) mutate(tenantControlPlane *kamajiv1alpha1.Te
return fmt.Errorf("missing hostname to expose the Tenant Control Plane using an Ingress resource")
}
rule.Host = tenantControlPlane.Spec.ControlPlane.Ingress.Hostname
rule.Host, _ = utilities.GetControlPlaneAddressAndPortFromHostname(tenantControlPlane.Spec.ControlPlane.Ingress.Hostname, 0)
r.resource.Spec.Rules = []networkingv1.IngressRule{
rule,

View File

@@ -18,6 +18,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/constants"
"github.com/clastix/kamaji/internal/crypto"
"github.com/clastix/kamaji/internal/kubeadm"
"github.com/clastix/kamaji/internal/utilities"
@@ -89,6 +90,19 @@ func (r *CertificateResource) mutate(ctx context.Context, tenantControlPlane *ka
return func() error {
logger := log.FromContext(ctx, "resource", r.GetName())
r.resource.SetLabels(utilities.MergeMaps(
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
map[string]string{
constants.ControllerLabelResource: "x509",
},
))
if err := ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme()); err != nil {
logger.Error(err, "cannot set controller reference", "resource", r.GetName())
return err
}
if checksum := tenantControlPlane.Status.Addons.Konnectivity.Certificate.Checksum; len(checksum) > 0 && checksum == utilities.CalculateMapChecksum(r.resource.Data) {
isValid, err := crypto.IsValidCertificateKeyPairBytes(r.resource.Data[corev1.TLSCertKey], r.resource.Data[corev1.TLSPrivateKeyKey])
if err != nil {
@@ -126,10 +140,8 @@ func (r *CertificateResource) mutate(ctx context.Context, tenantControlPlane *ka
corev1.TLSPrivateKeyKey: privKey.Bytes(),
}
r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
utilities.SetObjectChecksum(r.resource, r.resource.Data)
return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
return nil
}
}

View File

@@ -19,6 +19,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/constants"
"github.com/clastix/kamaji/internal/utilities"
)
@@ -87,6 +88,19 @@ func (r *KubeconfigResource) mutate(ctx context.Context, tenantControlPlane *kam
return func() error {
logger := log.FromContext(ctx, "resource", r.GetName())
r.resource.SetLabels(utilities.MergeMaps(
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
map[string]string{
constants.ControllerLabelResource: "kubeconfig",
},
))
if err := ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme()); err != nil {
logger.Error(err, "cannot set controller reference for kubeconfig", "resource", r.GetName())
return err
}
if checksum := tenantControlPlane.Status.Addons.Konnectivity.Certificate.Checksum; len(checksum) > 0 && checksum == utilities.GetObjectChecksum(r.resource) {
return nil
}
@@ -157,8 +171,6 @@ func (r *KubeconfigResource) mutate(ctx context.Context, tenantControlPlane *kam
utilities.SetObjectChecksum(r.resource, r.resource.Data)
r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
return nil
}
}

View File

@@ -71,7 +71,7 @@ func (r *KubeadmConfigResource) UpdateTenantControlPlaneStatus(ctx context.Conte
func (r *KubeadmConfigResource) getControlPlaneEndpoint(ingress *kamajiv1alpha1.IngressSpec, address string, port int32) string {
if ingress != nil && len(ingress.Hostname) > 0 {
return ingress.Hostname
address, port = utilities.GetControlPlaneAddressAndPortFromHostname(ingress.Hostname, port)
}
return fmt.Sprintf("%s:%d", address, port)

View File

@@ -159,12 +159,26 @@ func (r *KubeconfigResource) mutate(ctx context.Context, tenantControlPlane *kam
return err
}
r.resource.SetLabels(utilities.MergeMaps(
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
map[string]string{
constants.ControllerLabelResource: "kubeconfig",
},
))
r.resource.SetAnnotations(map[string]string{constants.Checksum: checksum})
if err = ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme()); err != nil {
logger.Error(err, "cannot set controller reference", "resource", r.GetName())
return err
}
var shouldCreate bool
shouldCreate = shouldCreate || r.resource.Data == nil // Missing data key
shouldCreate = shouldCreate || len(r.resource.Data) == 0 // Missing data key
shouldCreate = shouldCreate || len(r.resource.Data[r.KubeConfigFileName]) == 0 // Missing kubeconfig file, must be generated
shouldCreate = shouldCreate || !kubeadm.IsKubeconfigValid(r.resource.Data[r.KubeConfigFileName]) // invalid kubeconfig
shouldCreate = shouldCreate || !kubeadm.IsKubeconfigValid(r.resource.Data[r.KubeConfigFileName]) // invalid kubeconfig, or expired client certificate
shouldCreate = shouldCreate || status.Checksum != checksum || len(r.resource.UID) == 0 // Wrong checksum
if shouldCreate {
@@ -175,7 +189,7 @@ func (r *KubeconfigResource) mutate(ctx context.Context, tenantControlPlane *kam
kubeconfig, kcErr := kubeadm.CreateKubeconfig(r.KubeConfigFileName, crtKeyPair, config)
if kcErr != nil {
logger.Error(kcErr, "cannot shouldCreate a valid kubeconfig")
logger.Error(kcErr, "cannot create a valid kubeconfig")
return kcErr
}
@@ -185,13 +199,7 @@ func (r *KubeconfigResource) mutate(ctx context.Context, tenantControlPlane *kam
}
}
r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
r.resource.SetAnnotations(map[string]string{
constants.Checksum: checksum,
})
return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
return nil
}
}

View File

@@ -0,0 +1,25 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package utilities
import (
"strconv"
"strings"
)
func GetControlPlaneAddressAndPortFromHostname(hostname string, defaultPort int32) (address string, port int32) {
parts := strings.Split(hostname, ":")
address, port = parts[0], defaultPort
if len(parts) == 2 {
intPort, _ := strconv.Atoi(parts[1])
if intPort > 0 {
port = int32(intPort)
}
}
return address, port
}

View File

@@ -0,0 +1,29 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package utilities
import (
"fmt"
corev1 "k8s.io/api/core/v1"
clientcmdapiv1 "k8s.io/client-go/tools/clientcmd/api/v1"
)
func DecodeKubeconfig(secret corev1.Secret, key string) (*clientcmdapiv1.Config, error) {
bytes, ok := secret.Data[key]
if !ok {
return nil, fmt.Errorf("%s is not into kubeconfig secret", key)
}
return DecodeKubeconfigYAML(bytes)
}
func DecodeKubeconfigYAML(bytes []byte) (*clientcmdapiv1.Config, error) {
kubeconfig := &clientcmdapiv1.Config{}
if err := DecodeFromYAML(string(bytes), kubeconfig); err != nil {
return nil, err
}
return kubeconfig, nil
}

View File

@@ -44,17 +44,7 @@ func GetTenantKubeconfig(ctx context.Context, client client.Client, tenantContro
return nil, err
}
bytes, ok := secretKubeconfig.Data[kubeadmconstants.AdminKubeConfigFileName]
if !ok {
return nil, fmt.Errorf("%s is not into kubeconfig secret", kubeadmconstants.AdminKubeConfigFileName)
}
kubeconfig := &clientcmdapiv1.Config{}
if err := DecodeFromYAML(string(bytes), kubeconfig); err != nil {
return nil, err
}
return kubeconfig, nil
return DecodeKubeconfig(*secretKubeconfig, kubeadmconstants.AdminKubeConfigFileName)
}
func GetRESTClientConfig(ctx context.Context, client client.Client, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (*restclient.Config, error) {