mirror of
https://github.com/clastix/kamaji.git
synced 2026-02-22 22:03:51 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e34fc1851f | ||
|
|
740fe9c938 | ||
|
|
65854721de | ||
|
|
adde828e03 | ||
|
|
ffc2c7c967 | ||
|
|
0f195286a7 | ||
|
|
f768f93fe9 | ||
|
|
05cbff1fd8 | ||
|
|
7e94ecdbab | ||
|
|
648da19687 | ||
|
|
6c4b339c4b | ||
|
|
eee62032de | ||
|
|
8e8ee92fb2 | ||
|
|
f3be9e5442 | ||
|
|
fb296267f6 | ||
|
|
751ce3722b | ||
|
|
d99ffb0334 | ||
|
|
f831f385c4 | ||
|
|
f301c9bdc2 | ||
|
|
0909529e6b | ||
|
|
d0aacd03f6 | ||
|
|
f4c84946c0 | ||
|
|
2c72369b99 | ||
|
|
abcc662c96 | ||
|
|
792119d2d3 | ||
|
|
f0e675dea3 | ||
|
|
4413061640 | ||
|
|
8f57ff407e | ||
|
|
94f2d9074d | ||
|
|
fadcc219ec | ||
|
|
af5ac4acab | ||
|
|
069afd9b17 | ||
|
|
6741194034 | ||
|
|
7acba20056 | ||
|
|
0d2cf784f5 | ||
|
|
4db8230912 |
10
.github/workflows/docker-ci.yml
vendored
10
.github/workflows/docker-ci.yml
vendored
@@ -12,6 +12,8 @@ jobs:
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate build-args
|
||||
id: build-args
|
||||
@@ -85,7 +87,13 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
build-args:
|
||||
build-args: |
|
||||
GIT_LAST_TAG=${{ env.GIT_LAST_TAG }}
|
||||
GIT_HEAD_COMMIT=${{ env.GIT_HEAD_COMMIT }}
|
||||
GIT_TAG_COMMIT=${{ env.GIT_TAG_COMMIT }}
|
||||
GIT_MODIFIED=${{ env.GIT_MODIFIED }}
|
||||
GIT_REPO=${{ env.GIT_REPO }}
|
||||
BUILD_DATE=${{ env.BUILD_DATE }}
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.build-release.outputs.digest }}
|
||||
|
||||
2
.github/workflows/e2e.yaml
vendored
2
.github/workflows/e2e.yaml
vendored
@@ -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:
|
||||
|
||||
2
Makefile
2
Makefile
@@ -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.0
|
||||
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")
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
- [x] Seamless migration between datastores
|
||||
- [ ] Automatic assignment to a datastore
|
||||
- [ ] Autoscaling of Tenant Control Plane
|
||||
- [ ] Provisioning through Cluster APIs
|
||||
- [x] Provisioning through Cluster APIs
|
||||
- [ ] Terraform provider
|
||||
- [ ] Custom Prometheus metrics for monitoring and alerting
|
||||
|
||||
|
||||
BIN
assets/logo-colored.png
Normal file
BIN
assets/logo-colored.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
@@ -1,10 +1,8 @@
|
||||
apiVersion: v2
|
||||
appVersion: v0.3.0
|
||||
description: Kamaji is a tool aimed to build and operate a Managed Kubernetes Service
|
||||
with a fraction of the operational burden. With Kamaji, you can deploy and operate
|
||||
hundreds of Kubernetes clusters as a hyper-scaler.
|
||||
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/kamaji-logo.png
|
||||
icon: https://github.com/clastix/kamaji/raw/master/assets/logo-colored.png
|
||||
kubeVersion: ">=1.21.0-0"
|
||||
maintainers:
|
||||
- email: dario@tranchitella.eu
|
||||
@@ -17,8 +15,8 @@ name: kamaji
|
||||
sources:
|
||||
- https://github.com/clastix/kamaji
|
||||
type: application
|
||||
version: 0.12.0
|
||||
version: 0.12.4
|
||||
annotations:
|
||||
catalog.cattle.io/certified: partner
|
||||
catalog.cattle.io/release-name: kamaji
|
||||
catalog.cattle.io/display-name: Kamaji - Managed Kubernetes Service
|
||||
catalog.cattle.io/display-name: Kamaji
|
||||
@@ -1,8 +1,8 @@
|
||||
# kamaji
|
||||
|
||||
  
|
||||
  
|
||||
|
||||
Kamaji is a tool aimed to build and operate a Managed Kubernetes Service with a fraction of the operational burden. With Kamaji, you can deploy and operate hundreds of Kubernetes clusters as a hyper-scaler.
|
||||
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.
|
||||
|
||||
## Maintainers
|
||||
|
||||
|
||||
@@ -1,30 +1,12 @@
|
||||
# Kamaji - Managed Kubernetes Service
|
||||
# Kamaji
|
||||
|
||||
Kamaji is a tool aimed to build and operate a Managed Kubernetes Service with a fraction of the operational burden.
|
||||
Kamaji deploys and operates Kubernetes at scale with a fraction of the operational burden.
|
||||
|
||||
Useful links:
|
||||
- [Kamaji Github repository](https://github.com/clastix/kamaji)
|
||||
- [Kamaji Documentation](https://github.com/clastix/kamaji/docs/)
|
||||
- [Kamaji Documentation](https://kamaji.clastix.io)
|
||||
|
||||
## Requirements
|
||||
|
||||
* Kubernetes v1.22+
|
||||
* Helm v3
|
||||
|
||||
# Installation
|
||||
|
||||
To install the Chart with the release name `kamaji`:
|
||||
|
||||
helm upgrade --install --namespace kamaji-system --create-namespace clastix/kamaji
|
||||
|
||||
Show the status:
|
||||
|
||||
helm status kamaji -n kamaji-system
|
||||
|
||||
Upgrade the Chart
|
||||
|
||||
helm upgrade kamaji -n kamaji-system clastix/kamaji
|
||||
|
||||
Uninstall the Chart
|
||||
|
||||
helm uninstall kamaji -n kamaji-system
|
||||
* Helm v3
|
||||
@@ -9,12 +9,15 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
goRuntime "runtime"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/klog/v2"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
|
||||
@@ -30,21 +33,24 @@ import (
|
||||
"github.com/clastix/kamaji/internal/webhook/routes"
|
||||
)
|
||||
|
||||
//nolint:maintidx
|
||||
func NewCmd(scheme *runtime.Scheme) *cobra.Command {
|
||||
// CLI flags
|
||||
var (
|
||||
metricsBindAddress string
|
||||
healthProbeBindAddress string
|
||||
leaderElect bool
|
||||
tmpDirectory string
|
||||
kineImage string
|
||||
datastore string
|
||||
managerNamespace string
|
||||
managerServiceAccountName string
|
||||
managerServiceName string
|
||||
webhookCABundle []byte
|
||||
migrateJobImage string
|
||||
maxConcurrentReconciles int
|
||||
metricsBindAddress string
|
||||
healthProbeBindAddress string
|
||||
leaderElect bool
|
||||
tmpDirectory string
|
||||
kineImage string
|
||||
controllerReconcileTimeout time.Duration
|
||||
cacheResyncPeriod time.Duration
|
||||
datastore string
|
||||
managerNamespace string
|
||||
managerServiceAccountName string
|
||||
managerServiceName string
|
||||
webhookCABundle []byte
|
||||
migrateJobImage string
|
||||
maxConcurrentReconciles int
|
||||
|
||||
webhookCAPath string
|
||||
)
|
||||
@@ -73,6 +79,10 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
if controllerReconcileTimeout.Seconds() == 0 {
|
||||
return fmt.Errorf("the controller reconcile timeout must be greater than zero")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -92,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")
|
||||
@@ -99,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")
|
||||
@@ -111,10 +126,12 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
|
||||
Client: mgr.GetClient(),
|
||||
APIReader: mgr.GetAPIReader(),
|
||||
Config: controllers.TenantControlPlaneReconcilerConfig{
|
||||
ReconcileTimeout: controllerReconcileTimeout,
|
||||
DefaultDataStoreName: datastore,
|
||||
KineContainerImage: kineImage,
|
||||
TmpBaseDirectory: tmpDirectory,
|
||||
},
|
||||
CertificateChan: certChannel,
|
||||
TriggerChan: tcpChannel,
|
||||
KamajiNamespace: managerNamespace,
|
||||
KamajiServiceAccount: managerServiceAccountName,
|
||||
@@ -129,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")
|
||||
|
||||
@@ -149,6 +172,7 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
|
||||
handlers.TenantControlPlaneDefaults{DefaultDatastore: datastore},
|
||||
},
|
||||
routes.TenantControlPlaneValidate{}: {
|
||||
handlers.TenantControlPlaneName{},
|
||||
handlers.TenantControlPlaneVersion{},
|
||||
handlers.TenantControlPlaneKubeletAddresses{},
|
||||
handlers.TenantControlPlaneDataStore{Client: mgr.GetClient()},
|
||||
@@ -230,6 +254,8 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
|
||||
cmd.Flags().StringVar(&managerServiceName, "webhook-service-name", "kamaji-webhook-service", "The Kamaji webhook server Service name which is used to get validation webhooks, required for the TenantControlPlane migration jobs.")
|
||||
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()
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
appsv1 "k8s.io/kubernetes/pkg/apis/apps/v1"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
)
|
||||
@@ -26,6 +27,7 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
|
||||
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
utilruntime.Must(kamajiv1alpha1.AddToScheme(scheme))
|
||||
utilruntime.Must(appsv1.RegisterDefaults(scheme))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5062,7 +5062,7 @@ spec:
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.serviceAccountName
|
||||
image: clastix/kamaji:v0.3.0
|
||||
image: clastix/kamaji:v0.3.3
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
|
||||
@@ -13,4 +13,4 @@ kind: Kustomization
|
||||
images:
|
||||
- name: controller
|
||||
newName: clastix/kamaji
|
||||
newTag: v0.3.0
|
||||
newTag: v0.3.3
|
||||
|
||||
@@ -2,6 +2,8 @@ apiVersion: kamaji.clastix.io/v1alpha1
|
||||
kind: TenantControlPlane
|
||||
metadata:
|
||||
name: k8s-126
|
||||
labels:
|
||||
tenant.clastix.io: k8s-126
|
||||
spec:
|
||||
controlPlane:
|
||||
deployment:
|
||||
|
||||
@@ -2,6 +2,8 @@ apiVersion: kamaji.clastix.io/v1alpha1
|
||||
kind: TenantControlPlane
|
||||
metadata:
|
||||
name: additionalcontainers
|
||||
labels:
|
||||
tenant.clastix.io: additionalcontainers
|
||||
spec:
|
||||
dataStore: postgresql-bronze
|
||||
controlPlane:
|
||||
|
||||
@@ -2,6 +2,8 @@ apiVersion: kamaji.clastix.io/v1alpha1
|
||||
kind: TenantControlPlane
|
||||
metadata:
|
||||
name: additional-volumes
|
||||
labels:
|
||||
tenant.clastix.io: additional-volumes
|
||||
spec:
|
||||
controlPlane:
|
||||
deployment:
|
||||
|
||||
@@ -2,6 +2,8 @@ apiVersion: kamaji.clastix.io/v1alpha1
|
||||
kind: TenantControlPlane
|
||||
metadata:
|
||||
name: kine
|
||||
labels:
|
||||
tenant.clastix.io: kine
|
||||
spec:
|
||||
addons:
|
||||
coreDNS: {}
|
||||
|
||||
@@ -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
|
||||
|
||||
10
controllers/cert_channel.go
Normal file
10
controllers/cert_channel.go
Normal 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
|
||||
160
controllers/certificate_lifecycle_controller.go
Normal file
160
controllers/certificate_lifecycle_controller.go
Normal 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)
|
||||
}
|
||||
@@ -50,12 +50,17 @@ 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
|
||||
}
|
||||
|
||||
// TenantControlPlaneReconcilerConfig gives the necessary configuration for TenantControlPlaneReconciler.
|
||||
type TenantControlPlaneReconcilerConfig struct {
|
||||
ReconcileTimeout time.Duration
|
||||
DefaultDataStoreName string
|
||||
KineContainerImage string
|
||||
TmpBaseDirectory string
|
||||
@@ -74,6 +79,10 @@ type TenantControlPlaneReconcilerConfig struct {
|
||||
func (r *TenantControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
log := log.FromContext(ctx)
|
||||
|
||||
var cancelFn context.CancelFunc
|
||||
ctx, cancelFn = context.WithTimeout(ctx, r.Config.ReconcileTimeout)
|
||||
defer cancelFn()
|
||||
|
||||
tenantControlPlane, err := r.getTenantControlPlane(ctx, req.NamespacedName)()
|
||||
if err != nil {
|
||||
if apimachineryerrors.IsNotFound(err) {
|
||||
@@ -218,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{
|
||||
|
||||
@@ -20,9 +20,13 @@ Kamaji offers a [Custom Resource Definition](https://kubernetes.io/docs/tasks/ex
|
||||
All the _“tenant clusters”_ built with Kamaji are fully compliant CNCF Kubernetes clusters and are compatible with the standard Kubernetes toolchains everybody knows and loves. See [CNCF compliance](reference/conformance.md).
|
||||
|
||||
## Tenant worker nodes
|
||||
And what about the tenant worker nodes? They are just _"worker nodes"_, i.e. regular virtual or bare metal machines, connecting to the APIs server of the Tenant Control Plane. Kamaji's goal is to manage the lifecycle of hundreds of these _“tenant clusters”_, not only one, so how to add another tenant cluster to Kamaji? As you could expect, you have just deploys a new Tenant Control Plane in one of the _“admin cluster”_ namespace, and then joins the tenant worker nodes to it.
|
||||
|
||||
We have in roadmap, the Cluster APIs support as well as a Terraform provider so that you can create _“tenant clusters”_ in a declarative way.
|
||||
And what about the tenant worker nodes?
|
||||
They are just _"worker nodes"_, i.e. regular virtual or bare metal machines, connecting to the APIs server of the Tenant Control Plane.
|
||||
Kamaji's goal is to manage the lifecycle of hundreds of these _“tenant clusters”_, not only one, so how to add another tenant cluster to Kamaji?
|
||||
As you could expect, you have just deploys a new Tenant Control Plane in one of the _“admin cluster”_ namespace, and then joins the tenant worker nodes to it.
|
||||
|
||||
A [Cluster API ControlPlane provider](https://github.com/clastix/cluster-api-control-plane-provider-kamaji) has been released, allowing to offer a Cluster API-native declarative lifecycle, by automating the worker nodes join.
|
||||
|
||||
## Datastores
|
||||
Putting the Tenant Control Plane in a pod is the easiest part. Also, we have to make sure each tenant cluster saves the state to be able to store and retrieve data. As we can deploy a Kubernetes cluster with an external `etcd` cluster, we explored this option for the Tenant Control Planes. On the admin cluster, you can deploy one or multi-tenant `etcd` to save the state of multiple tenant clusters. Kamaji offers a Custom Resource Definition called `DataStore` to provide a declarative approach of managing multiple datastores. By sharing the datastore between multiple tenants, the resiliency is still guaranteed and the pods' count remains under control, so it solves the main goal of resiliency and costs optimization. The trade-off here is that you have to operate external datastores, in addition to `etcd` of the _“admin cluster”_ and manage the access to be sure that each _“tenant cluster”_ uses only its data.
|
||||
|
||||
@@ -73,7 +73,7 @@ helm install \
|
||||
|
||||
## Install Kamaji Controller
|
||||
|
||||
Installing Kamaji via Helm charts is the preferred way. The Kamaji controller needs to access a Datastore in order to save data of the tenants' clusters. The Kamaji Helm Chart provides the installation of a basic unamanaged `etcd` as datastore, out of box.
|
||||
Installing Kamaji via Helm charts is the preferred way. The Kamaji controller needs to access a Datastore in order to save data of the tenants' clusters. The Kamaji Helm Chart provides the installation of a basic unmanaged `etcd` as datastore, out of box.
|
||||
|
||||
Install Kamaji with `helm` using an unmanaged `etcd` as default datastore:
|
||||
|
||||
@@ -99,6 +99,8 @@ kind: TenantControlPlane
|
||||
metadata:
|
||||
name: ${TENANT_NAME}
|
||||
namespace: ${TENANT_NAMESPACE}
|
||||
labels:
|
||||
tenant.clastix.io: ${TENANT_NAME}
|
||||
spec:
|
||||
dataStore: default
|
||||
controlPlane:
|
||||
@@ -240,10 +242,11 @@ And make sure it is `${TENANT_ADDR}:${TENANT_PORT}`.
|
||||
|
||||
### Prepare worker nodes to join
|
||||
|
||||
Currently Kamaji does not provide any helper for creation of tenant worker nodes. You should get a set of machines from your infrastructure provider, turn them into worker nodes, and then join to the tenant control plane with the `kubeadm`.
|
||||
Currently, Kamaji does not provide any helper for creation of tenant worker nodes.
|
||||
You should get a set of machines from your infrastructure provider, turn them into worker nodes, and then join to the tenant control plane with the `kubeadm`.
|
||||
|
||||
!!! note "Cluster APIs support"
|
||||
In the future, we'll provide creation of tenant clusters through Cluster APIs.
|
||||
Kamaji is sticking to the [Cluster Management API](https://github.com/kubernetes-sigs/cluster-api) project contracts by providing a `ControlPlane` provider.
|
||||
Please, refer to the [official repository](https://github.com/clastix/cluster-api-control-plane-provider-kamaji) to learn more about it.
|
||||
|
||||
You can use the provided helper script `/deploy/nodes-prerequisites.sh`, in order to install the dependencies on all the worker nodes:
|
||||
|
||||
|
||||
143
docs/content/guides/certs-lifecycle.md
Normal file
143
docs/content/guides/certs-lifecycle.md
Normal 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.
|
||||
@@ -114,7 +114,7 @@ helm install \
|
||||
|
||||
## Install Kamaji Controller
|
||||
|
||||
Installing Kamaji via Helm charts is the preferred way. The Kamaji controller needs to access a Datastore in order to save data of the tenants' clusters. The Kamaji Helm Chart provides the installation of a basic unamanaged `etcd` as datastore, out of box.
|
||||
Installing Kamaji via Helm charts is the preferred way. The Kamaji controller needs to access a Datastore in order to save data of the tenants' clusters. The Kamaji Helm Chart provides the installation of a basic unmanaged `etcd` as datastore, out of box.
|
||||
|
||||
Install Kamaji with `helm` using an unmanaged `etcd` as default datastore:
|
||||
|
||||
@@ -144,6 +144,8 @@ kind: TenantControlPlane
|
||||
metadata:
|
||||
name: ${TENANT_NAME}
|
||||
namespace: ${TENANT_NAMESPACE}
|
||||
labels:
|
||||
tenant.clastix.io: ${TENANT_NAME}
|
||||
spec:
|
||||
dataStore: default
|
||||
controlPlane:
|
||||
@@ -274,10 +276,11 @@ kubernetes 10.240.0.100:6443 57m
|
||||
|
||||
### Prepare worker nodes to join
|
||||
|
||||
Currently Kamaji does not provide any helper for creation of tenant worker nodes. You should get a set of machines from your infrastructure provider, turn them into worker nodes, and then join to the tenant control plane with the `kubeadm`.
|
||||
Currently, Kamaji does not provide any helper for creation of tenant worker nodes.
|
||||
You should get a set of machines from your infrastructure provider, turn them into worker nodes, and then join to the tenant control plane with the `kubeadm`.
|
||||
|
||||
!!! note "Cluster APIs support"
|
||||
In the future, we'll provide creation of tenant clusters through Cluster APIs.
|
||||
Kamaji is sticking to the [Cluster Management API](https://github.com/kubernetes-sigs/cluster-api) project contracts by providing a `ControlPlane` provider.
|
||||
An Azure-based cluster is not yet available: the available road-map is available on the [official repository](https://github.com/clastix/cluster-api-control-plane-provider-kamaji).
|
||||
|
||||
Create an Azure VM Stateful Set to host worker nodes
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ apiVersion: kamaji.clastix.io/v1alpha1
|
||||
kind: TenantControlPlane
|
||||
metadata:
|
||||
name: tenant-00
|
||||
labels:
|
||||
tenant.clastix.io: tenant-00
|
||||
spec:
|
||||
controlPlane:
|
||||
deployment:
|
||||
@@ -27,6 +29,9 @@ spec:
|
||||
```
|
||||
|
||||
## Upgrade of Tenant Worker Nodes
|
||||
As currently Kamaji is not providing any helpers for Tenant Worker Nodes, you should make sure to upgrade them manually, for example, with the help of `kubeadm`. Refer to the official [documentation](https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade/#upgrade-worker-nodes).
|
||||
|
||||
> We have in roadmap, the Cluster APIs support so that you can upgrade _“tenant clusters”_ in a fully declarative way.
|
||||
As currently Kamaji is not providing any helpers for Tenant Worker Nodes, you should make sure to upgrade them manually, for example, with the help of `kubeadm`.
|
||||
Refer to the official [documentation](https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade/#upgrade-worker-nodes).
|
||||
|
||||
Kamaji is offering a [Cluster API Control Plane provider](https://github.com/clastix/cluster-api-control-plane-provider-kamaji), thus integrating with the Kubernetes clusters declarative management approach.
|
||||
You can refer to the official [Cluster API documentation](https://cluster-api.sigs.k8s.io/).
|
||||
|
||||
@@ -191,6 +191,8 @@ kind: TenantControlPlane
|
||||
metadata:
|
||||
name: benchmark$I
|
||||
namespace: $NS
|
||||
labels:
|
||||
tenant.clastix.io: benchmark$I
|
||||
spec:
|
||||
dataStore: $DS
|
||||
controlPlane:
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
In Kamaji, there are different components that might require independent versioning and support level:
|
||||
|
||||
|Kamaji |Admin Cluster| Tenant Cluster |
|
||||
|-------|-------------|----------------------|
|
||||
| v0.0 | v1.22+ | [v1.21.0 .. v1.23.5] |
|
||||
| v0.1 | v1.22+ | [v1.21.0 .. v1.25.0] |
|
||||
| v0.2 | v1.22+ | [v1.21.0 .. v1.27.0] |
|
||||
|
||||
| Kamaji | Admin Cluster | Tenant Cluster |
|
||||
|--------|---------------|----------------------|
|
||||
| v0.0 | v1.22+ | [v1.21.0 .. v1.23.5] |
|
||||
| v0.1 | v1.22+ | [v1.21.0 .. v1.25.0] |
|
||||
| v0.2 | v1.22+ | [v1.21.0 .. v1.27.0] |
|
||||
| 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] |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,4 +9,5 @@ const (
|
||||
|
||||
ControlPlaneLabelKey = "kamaji.clastix.io/name"
|
||||
ControlPlaneLabelResource = "kamaji.clastix.io/component"
|
||||
ControllerLabelResource = "kamaji.clastix.io/certificate_lifecycle_controller"
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -35,7 +35,6 @@ func CreateKubeadmInitConfiguration(params Parameters) (*Configuration, error) {
|
||||
AdvertiseAddress: params.TenantControlPlaneAddress,
|
||||
BindPort: params.TenantControlPlanePort,
|
||||
}
|
||||
conf.NodeRegistration.Name = params.TenantControlPlaneName
|
||||
|
||||
caFile, certFile, keyFile := "", "", ""
|
||||
if strings.HasPrefix(params.ETCDs[0], "https") {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
package kubeadm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
@@ -19,6 +22,14 @@ import (
|
||||
"github.com/clastix/kamaji/internal/utilities"
|
||||
)
|
||||
|
||||
const (
|
||||
// kubeletConfigMapName defines base kubelet configuration ConfigMap name for kubeadm < 1.24.
|
||||
kubeletConfigMapName = "kubelet-config-%d.%d"
|
||||
)
|
||||
|
||||
// minVerUnversionedKubeletConfig defines minimum version from which kubeadm uses kubelet-config as a ConfigMap name.
|
||||
var minVerUnversionedKubeletConfig = semver.MustParse("1.24.0")
|
||||
|
||||
func UploadKubeadmConfig(client kubernetes.Interface, config *Configuration) ([]byte, error) {
|
||||
return nil, uploadconfig.UploadConfiguration(&config.InitConfiguration, client)
|
||||
}
|
||||
@@ -34,7 +45,10 @@ func UploadKubeletConfig(client kubernetes.Interface, config *Configuration) ([]
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configMapName := kubeadmconstants.KubeletBaseConfigurationConfigMap
|
||||
configMapName, err := generateKubeletConfigMapName(config.Parameters.TenantControlPlaneVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -50,7 +64,7 @@ func UploadKubeletConfig(client kubernetes.Interface, config *Configuration) ([]
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = createConfigMapRBACRules(client); err != nil {
|
||||
if err = createConfigMapRBACRules(client, configMapName); err != nil {
|
||||
return nil, errors.Wrap(err, "error creating kubelet configuration configmap RBAC rules")
|
||||
}
|
||||
|
||||
@@ -114,7 +128,7 @@ func getKubeletConfigmapContent(kubeletConfiguration KubeletConfiguration) ([]by
|
||||
return utilities.EncodeToYaml(&kc)
|
||||
}
|
||||
|
||||
func createConfigMapRBACRules(client kubernetes.Interface) error {
|
||||
func createConfigMapRBACRules(client kubernetes.Interface, configMapName string) error {
|
||||
configMapRBACName := kubeadmconstants.KubeletBaseConfigMapRole
|
||||
|
||||
if err := apiclient.CreateOrUpdateRole(client, &rbacv1.Role{
|
||||
@@ -127,7 +141,7 @@ func createConfigMapRBACRules(client kubernetes.Interface) error {
|
||||
Verbs: []string{"get"},
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"configmaps"},
|
||||
ResourceNames: []string{kubeadmconstants.KubeletBaseConfigurationConfigMap},
|
||||
ResourceNames: []string{configMapName},
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
@@ -156,3 +170,17 @@ func createConfigMapRBACRules(client kubernetes.Interface) error {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func generateKubeletConfigMapName(version string) (string, error) {
|
||||
parsedVersion, err := semver.ParseTolerant(version)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to parse kubernetes version %q", version)
|
||||
}
|
||||
|
||||
majorMinor := semver.Version{Major: parsedVersion.Major, Minor: parsedVersion.Minor}
|
||||
if majorMinor.GTE(minVerUnversionedKubeletConfig) {
|
||||
return kubeadmconstants.KubeletBaseConfigurationConfigMap, nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf(kubeletConfigMapName, parsedVersion.Major, parsedVersion.Minor), nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
@@ -96,6 +97,13 @@ func (r *CACertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1
|
||||
if err != nil {
|
||||
logger.Info(fmt.Sprintf("%s certificate-private_key pair is not valid: %s", kubeadmconstants.CACertAndKeyBaseName, err.Error()))
|
||||
}
|
||||
// Appending the Cluster API required keys if they're missing:
|
||||
// with this we're sure to avoid introducing breaking changes.
|
||||
if isValid && (!bytes.Equal(r.resource.Data[corev1.TLSCertKey], r.resource.Data[kubeadmconstants.CACertName]) || !bytes.Equal(r.resource.Data[kubeadmconstants.CAKeyName], r.resource.Data[corev1.TLSPrivateKeyKey])) {
|
||||
r.resource.Data[corev1.TLSCertKey] = r.resource.Data[kubeadmconstants.CACertName]
|
||||
r.resource.Data[corev1.TLSPrivateKeyKey] = r.resource.Data[kubeadmconstants.CAKeyName]
|
||||
}
|
||||
|
||||
if isValid {
|
||||
return nil
|
||||
}
|
||||
@@ -122,6 +130,11 @@ func (r *CACertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1
|
||||
r.resource.Data = map[string][]byte{
|
||||
kubeadmconstants.CACertName: ca.Certificate,
|
||||
kubeadmconstants.CAKeyName: ca.PrivateKey,
|
||||
// Required for Cluster API integration which is reading the basic TLS keys.
|
||||
// We cannot switch over basic corev1.Secret keys for backward compatibility,
|
||||
// it would require a new CA generation breaking all the clusters deployed.
|
||||
corev1.TLSCertKey: ca.Certificate,
|
||||
corev1.TLSPrivateKeyKey: ca.PrivateKey,
|
||||
}
|
||||
|
||||
r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package datastore
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@@ -94,8 +95,9 @@ func (r *Config) mutate(_ context.Context, tenantControlPlane *kamajiv1alpha1.Te
|
||||
if len(fromStatus) > 0 {
|
||||
return []byte(fromStatus)
|
||||
}
|
||||
|
||||
return []byte(fmt.Sprintf("%s_%s", tenantControlPlane.GetNamespace(), tenantControlPlane.GetName()))
|
||||
// The dash character (-) must be replaced with an underscore, PostgreSQL is complaining about it:
|
||||
// https://github.com/clastix/kamaji/issues/328
|
||||
return []byte(strings.ReplaceAll(fmt.Sprintf("%s_%s", tenantControlPlane.GetNamespace(), tenantControlPlane.GetName()), "-", "_"))
|
||||
}
|
||||
|
||||
r.resource.Data = map[string][]byte{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +142,9 @@ func (r *ServiceResource) mutate(_ context.Context, tenantControlPlane *kamajiv1
|
||||
r.resource.Spec.Ports[1].Protocol = corev1.ProtocolTCP
|
||||
r.resource.Spec.Ports[1].Port = tenantControlPlane.Spec.Addons.Konnectivity.KonnectivityServerSpec.Port
|
||||
r.resource.Spec.Ports[1].TargetPort = intstr.FromInt(int(tenantControlPlane.Spec.Addons.Konnectivity.KonnectivityServerSpec.Port))
|
||||
if tenantControlPlane.Spec.ControlPlane.Service.ServiceType == kamajiv1alpha1.ServiceTypeNodePort {
|
||||
r.resource.Spec.Ports[1].NodePort = tenantControlPlane.Spec.Addons.Konnectivity.KonnectivityServerSpec.Port
|
||||
}
|
||||
|
||||
return controllerutil.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -117,10 +117,10 @@ func (r *KubeconfigResource) CreateOrUpdate(ctx context.Context, tenantControlPl
|
||||
return utilities.CreateOrUpdateWithConflict(ctx, r.Client, r.resource, r.mutate(ctx, tenantControlPlane))
|
||||
}
|
||||
|
||||
func (r *KubeconfigResource) checksum(apiServerCertificatesSecret *corev1.Secret, kubeadmChecksum string) string {
|
||||
func (r *KubeconfigResource) checksum(caCertificatesSecret *corev1.Secret, kubeadmChecksum string) string {
|
||||
return utilities.CalculateMapChecksum(map[string][]byte{
|
||||
"ca-cert-checksum": apiServerCertificatesSecret.Data[kubeadmconstants.CACertName],
|
||||
"ca-key-checksum": apiServerCertificatesSecret.Data[kubeadmconstants.CAKeyName],
|
||||
"ca-cert-checksum": caCertificatesSecret.Data[kubeadmconstants.CACertName],
|
||||
"ca-key-checksum": caCertificatesSecret.Data[kubeadmconstants.CAKeyName],
|
||||
"kubeadmconfig": []byte(kubeadmChecksum),
|
||||
})
|
||||
}
|
||||
@@ -142,15 +142,15 @@ func (r *KubeconfigResource) mutate(ctx context.Context, tenantControlPlane *kam
|
||||
return err
|
||||
}
|
||||
|
||||
apiServerCertificatesSecretNamespacedName := k8stypes.NamespacedName{Namespace: tenantControlPlane.GetNamespace(), Name: tenantControlPlane.Status.Certificates.CA.SecretName}
|
||||
apiServerCertificatesSecret := &corev1.Secret{}
|
||||
if err := r.Client.Get(ctx, apiServerCertificatesSecretNamespacedName, apiServerCertificatesSecret); err != nil {
|
||||
caSecretNamespacedName := k8stypes.NamespacedName{Namespace: tenantControlPlane.GetNamespace(), Name: tenantControlPlane.Status.Certificates.CA.SecretName}
|
||||
caCertificatesSecret := &corev1.Secret{}
|
||||
if err = r.Client.Get(ctx, caSecretNamespacedName, caCertificatesSecret); err != nil {
|
||||
logger.Error(err, "cannot retrieve the CA")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
checksum := r.checksum(apiServerCertificatesSecret, config.Checksum())
|
||||
checksum := r.checksum(caCertificatesSecret, config.Checksum())
|
||||
|
||||
status, err := r.getKubeconfigStatus(tenantControlPlane)
|
||||
if err != nil {
|
||||
@@ -158,38 +158,48 @@ func (r *KubeconfigResource) mutate(ctx context.Context, tenantControlPlane *kam
|
||||
|
||||
return err
|
||||
}
|
||||
// A new kubeconfig must be generated when one of the following cases is occurring:
|
||||
// 1. the status checksum is different from the computed one
|
||||
// 2. the resource UID is empty, meaning it's a new resource (tl;dr; a first reconciliation)
|
||||
//
|
||||
// And finally, we're checking if the kubeconfig is valid: if not, generating a new one.
|
||||
if (status.Checksum != checksum || len(r.resource.UID) == 0) && !kubeadm.IsKubeconfigValid(r.resource.Data[r.KubeConfigFileName]) {
|
||||
kubeconfig, err := kubeadm.CreateKubeconfig(
|
||||
r.KubeConfigFileName,
|
||||
|
||||
kubeadm.CertificatePrivateKeyPair{
|
||||
Certificate: apiServerCertificatesSecret.Data[kubeadmconstants.CACertName],
|
||||
PrivateKey: apiServerCertificatesSecret.Data[kubeadmconstants.CAKeyName],
|
||||
},
|
||||
config,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error(err, "cannot create a valid kubeconfig")
|
||||
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})
|
||||
|
||||
return err
|
||||
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, or expired client certificate
|
||||
shouldCreate = shouldCreate || status.Checksum != checksum || len(r.resource.UID) == 0 // Wrong checksum
|
||||
|
||||
if shouldCreate {
|
||||
crtKeyPair := kubeadm.CertificatePrivateKeyPair{
|
||||
Certificate: caCertificatesSecret.Data[kubeadmconstants.CACertName],
|
||||
PrivateKey: caCertificatesSecret.Data[kubeadmconstants.CAKeyName],
|
||||
}
|
||||
|
||||
kubeconfig, kcErr := kubeadm.CreateKubeconfig(r.KubeConfigFileName, crtKeyPair, config)
|
||||
if kcErr != nil {
|
||||
logger.Error(kcErr, "cannot create a valid kubeconfig")
|
||||
|
||||
return kcErr
|
||||
}
|
||||
|
||||
r.resource.Data = map[string][]byte{
|
||||
r.KubeConfigFileName: kubeconfig,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
package upgrade
|
||||
|
||||
const (
|
||||
KubeadmVersion = "v1.27.0"
|
||||
KubeadmVersion = "v1.27.3"
|
||||
)
|
||||
|
||||
25
internal/utilities/ingress.go
Normal file
25
internal/utilities/ingress.go
Normal 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
|
||||
}
|
||||
29
internal/utilities/kubeconfig.go
Normal file
29
internal/utilities/kubeconfig.go
Normal 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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
40
internal/webhook/handlers/tcp_name.go
Normal file
40
internal/webhook/handlers/tcp_name.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gomodules.xyz/jsonpatch/v2"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
"github.com/clastix/kamaji/internal/webhook/utils"
|
||||
)
|
||||
|
||||
type TenantControlPlaneName struct{}
|
||||
|
||||
func (t TenantControlPlaneName) OnCreate(object runtime.Object) AdmissionResponse {
|
||||
return func(ctx context.Context, req admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
|
||||
tcp := object.(*kamajiv1alpha1.TenantControlPlane) //nolint:forcetypeassert
|
||||
|
||||
if errs := validation.IsDNS1035Label(tcp.Name); len(errs) > 0 {
|
||||
return nil, fmt.Errorf("the provided name is invalid, %s", strings.Join(errs, ","))
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (t TenantControlPlaneName) OnDelete(runtime.Object) AdmissionResponse {
|
||||
return utils.NilOp()
|
||||
}
|
||||
|
||||
func (t TenantControlPlaneName) OnUpdate(runtime.Object, runtime.Object) AdmissionResponse {
|
||||
return utils.NilOp()
|
||||
}
|
||||
@@ -4,9 +4,8 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
)
|
||||
|
||||
type TenantControlPlaneMigrate struct{}
|
||||
@@ -16,5 +15,5 @@ func (t TenantControlPlaneMigrate) GetPath() string {
|
||||
}
|
||||
|
||||
func (t TenantControlPlaneMigrate) GetObject() runtime.Object {
|
||||
return &kamajiv1alpha1.TenantControlPlane{}
|
||||
return &corev1.Namespace{}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user