feat: releasing kamaji

This commit is contained in:
Gonzalo Gabriel Jiménez Fuentes
2022-05-15 11:06:17 +02:00
committed by Dario Tranchitella
parent 7089412a96
commit 432c50b081
106 changed files with 12738 additions and 2 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
# Ignore build and test binaries.
bin/
testbin/

37
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: CI
on:
push:
branches: [ "*" ]
pull_request:
branches: [ "*" ]
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v2.3.0
with:
version: v1.45.2
only-new-issues: false
args: --timeout 5m --config .golangci.yml
diff:
name: diff
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-go@v2
with:
go-version: '1.17'
- run: make yaml-installation-file
- name: Checking if YAML installer file is not aligned
run: if [[ $(git diff | wc -l) -gt 0 ]]; then echo ">>> Untracked generated files have not been committed" && git --no-pager diff && exit 1; fi
- name: Checking if YAML installer generated untracked files
run: test -z "$(git ls-files --others --exclude-standard 2> /dev/null)"
- name: Checking if source code is not formatted
run: test -z "$(git diff 2> /dev/null)"

3
.gitignore vendored
View File

@@ -27,5 +27,6 @@ bin
**/*.kubeconfig
**/*.crt
**/*.key
**/*.pem
**/*.csr
.DS_Store

35
.golangci.yml Normal file
View File

@@ -0,0 +1,35 @@
linters-settings:
gci:
sections:
- standard
- default
- prefix(github.com/clastix/kamaji)
linters:
disable:
- wrapcheck
- gomnd
- scopelint
- golint
- interfacer
- maligned
- varnamelen
- testpackage
- tagliatelle
- paralleltest
- ireturn
- goerr113
- gochecknoglobals
- exhaustivestruct
- wsl
- exhaustive
- lll
- gosec
- gomoddirectives
- godox
- gochecknoinits
- funlen
- dupl
- maintidx
- cyclop
enable-all: true

29
Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
# Build the manager binary
FROM golang:1.17 as builder
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download
# Copy the go source
COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/
COPY internal/ internal/
# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go
# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
COPY ./kamaji.yaml .
USER 65532:65532
ENTRYPOINT ["/manager"]

215
Makefile Normal file
View File

@@ -0,0 +1,215 @@
# VERSION defines the project version for the bundle.
# Update this value when you upgrade the version of your project.
# 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.0.1
# 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")
# To re-generate a bundle for other specific channels without changing the standard setup, you can:
# - use the CHANNELS as arg of the bundle target (e.g make bundle CHANNELS=candidate,fast,stable)
# - use environment variables to overwrite this value (e.g export CHANNELS="candidate,fast,stable")
ifneq ($(origin CHANNELS), undefined)
BUNDLE_CHANNELS := --channels=$(CHANNELS)
endif
# DEFAULT_CHANNEL defines the default channel used in the bundle.
# Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable")
# To re-generate a bundle for any other default channel without changing the default setup, you can:
# - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable)
# - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable")
ifneq ($(origin DEFAULT_CHANNEL), undefined)
BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL)
endif
BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL)
# IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images.
# This variable is used to construct full image tags for bundle and catalog images.
#
# For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both
# clastix.io/operator-bundle:$VERSION and clastix.io/operator-catalog:$VERSION.
IMAGE_TAG_BASE ?= clastix.io/operator
# BUNDLE_IMG defines the image:tag used for the bundle.
# You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=<some-registry>/<project-name-bundle>:<tag>)
BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION)
# Image URL to use all building/pushing image targets
IMG ?= controller:latest
# Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
CRD_OPTIONS ?= "crd:trivialVersions=true,preserveUnknownFields=false"
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
ENVTEST_K8S_VERSION = 1.21
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif
# Setting SHELL to bash allows bash commands to be executed by recipes.
# This is a requirement for 'setup-envtest.sh' in the test target.
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
SHELL = /usr/bin/env bash -o pipefail
.SHELLFLAGS = -ec
all: build
##@ General
# The help target prints out all targets with their descriptions organized
# beneath their categories. The categories are represented by '##@' and the
# target descriptions by '##'. The awk commands is responsible for reading the
# entire set of makefiles included in this invocation, looking for lines of the
# file as xyz: ## something, and then pretty-format the target and help. Then,
# if there's a line with ##@ something, that gets pretty-printed as a category.
# More info on the usage of ANSI control characters for terminal formatting:
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
# More info on the awk command:
# http://linuxcommand.org/lc3_adv_awk.php
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Development
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
$(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
fmt: ## Run go fmt against code.
go fmt ./...
vet: ## Run go vet against code.
go vet ./...
test: manifests generate fmt vet envtest ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out
##@ Build
build: generate fmt vet ## Build manager binary.
go build -o bin/manager main.go
run: manifests generate fmt vet ## Run a controller from your host.
go run ./main.go
docker-build: test ## Build docker image with the manager.
docker build -t ${IMG} .
docker-push: ## Push docker image with the manager.
docker push ${IMG}
##@ Deployment
dev: generate manifests uninstall install rbac ## Full installation for development purposes
go fmt ./...
load: dev docker-build
kind load docker-image --name kamaji ${IMG}
rbac: manifests kustomize ## Install RBAC into the K8s cluster specified in ~/.kube/config.
$(KUSTOMIZE) build config/rbac | kubectl apply -f -
install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
$(KUSTOMIZE) build config/crd | kubectl apply -f -
uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config.
$(KUSTOMIZE) build config/crd | kubectl delete -f -
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default | kubectl apply -f -
undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config.
$(KUSTOMIZE) build config/default | kubectl delete -f -
yaml-installation-file: manifests kustomize ## Create yaml installation file
$(KUSTOMIZE) build config/default > config/install.yaml
CONTROLLER_GEN = $(shell pwd)/bin/controller-gen
controller-gen: ## Download controller-gen locally if necessary.
$(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.6.1)
KUSTOMIZE = $(shell pwd)/bin/kustomize
kustomize: ## Download kustomize locally if necessary.
$(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v3@v3.8.7)
ENVTEST = $(shell pwd)/bin/setup-envtest
envtest: ## Download envtest-setup locally if necessary.
$(call go-get-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@latest)
# go-get-tool will 'go get' any package $2 and install it to $1.
PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))
define go-get-tool
@[ -f $(1) ] || { \
set -e ;\
TMP_DIR=$$(mktemp -d) ;\
cd $$TMP_DIR ;\
go mod init tmp ;\
echo "Downloading $(2)" ;\
GOBIN=$(PROJECT_DIR)/bin go get $(2) ;\
rm -rf $$TMP_DIR ;\
}
endef
.PHONY: bundle
bundle: manifests kustomize ## Generate bundle manifests and metadata, then validate generated files.
operator-sdk generate kustomize manifests -q
cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG)
$(KUSTOMIZE) build config/manifests | operator-sdk generate bundle -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS)
operator-sdk bundle validate ./bundle
.PHONY: bundle-build
bundle-build: ## Build the bundle image.
docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) .
.PHONY: bundle-push
bundle-push: ## Push the bundle image.
$(MAKE) docker-push IMG=$(BUNDLE_IMG)
.PHONY: opm
OPM = ./bin/opm
opm: ## Download opm locally if necessary.
ifeq (,$(wildcard $(OPM)))
ifeq (,$(shell which opm 2>/dev/null))
@{ \
set -e ;\
mkdir -p $(dir $(OPM)) ;\
OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \
curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.15.1/$${OS}-$${ARCH}-opm ;\
chmod +x $(OPM) ;\
}
else
OPM = $(shell which opm)
endif
endif
# A comma-separated list of bundle images (e.g. make catalog-build BUNDLE_IMGS=example.com/operator-bundle:v0.1.0,example.com/operator-bundle:v0.2.0).
# These images MUST exist in a registry and be pull-able.
BUNDLE_IMGS ?= $(BUNDLE_IMG)
# The image tag given to the resulting catalog image (e.g. make catalog-build CATALOG_IMG=example.com/operator-catalog:v0.2.0).
CATALOG_IMG ?= $(IMAGE_TAG_BASE)-catalog:v$(VERSION)
# Set CATALOG_BASE_IMG to an existing catalog image tag to add $BUNDLE_IMGS to that image.
ifneq ($(origin CATALOG_BASE_IMG), undefined)
FROM_INDEX_OPT := --from-index $(CATALOG_BASE_IMG)
endif
# Build a catalog image by adding bundle images to an empty catalog using the operator package manager tool, 'opm'.
# This recipe invokes 'opm' in 'semver' bundle add mode. For more information on add modes, see:
# https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator
.PHONY: catalog-build
catalog-build: opm ## Build a catalog image.
$(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT)
# Push the catalog image.
.PHONY: catalog-push
catalog-push: ## Push a catalog image.
$(MAKE) docker-push IMG=$(CATALOG_IMG)

19
PROJECT Normal file
View File

@@ -0,0 +1,19 @@
domain: clastix.io
layout:
- go.kubebuilder.io/v3
plugins:
manifests.sdk.operatorframework.io/v2: {}
scorecard.sdk.operatorframework.io/v2: {}
projectName: operator
repo: github.com/clastix/kamaji
resources:
- api:
crdVersion: v1
namespaced: true
controller: true
domain: clastix.io
group: kamaji
kind: TenantControlPlane
path: github.com/clastix/kamaji/api/v1alpha1
version: v1alpha1
version: "3"

View File

@@ -119,4 +119,4 @@ A. Lighter Multi-Tenancy solutions, like Capsule shares the Kubernetes control p
Q. So I need a costly cloud infrastructure to try Kamaji?
A. No, it is possible to try Kamaji on your laptop with [KinD](./deploy/kind/README.md). We're porting it also on Canonical MicroK8s as an add-on, _tbd_.
A. No, it is possible to try Kamaji on your laptop with [KinD](./deploy/kind/README.md). We're porting it also on Canonical MicroK8s as an add-on, _tbd_.

View File

@@ -0,0 +1,23 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
// Package v1alpha1 contains API Schema definitions for the kamaji v1alpha1 API group
//+kubebuilder:object:generate=true
//+groupName=kamaji.clastix.io
package v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var (
// GroupVersion is group version used to register these objects.
GroupVersion = schema.GroupVersion{Group: "kamaji.clastix.io", Version: "v1alpha1"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)

View File

@@ -0,0 +1,43 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
"context"
"fmt"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
func (in *TenantControlPlane) GetAddress(ctx context.Context, client client.Client) (string, error) {
var loadBalancerStatus corev1.LoadBalancerStatus
svc := &corev1.Service{}
err := client.Get(ctx, types.NamespacedName{Namespace: in.GetNamespace(), Name: in.GetName()}, svc)
if err != nil {
return "", errors.Wrap(err, "cannot retrieve Service for the TenantControlPlane")
}
switch {
case len(in.Spec.NetworkProfile.Address) > 0:
// Returning the hard-coded value in the specification in case of non LoadBalanced resources
return in.Spec.NetworkProfile.Address, nil
case svc.Spec.Type == corev1.ServiceTypeLoadBalancer:
loadBalancerStatus = svc.Status.LoadBalancer
if len(loadBalancerStatus.Ingress) == 0 {
return "", fmt.Errorf("cannot retrieve the TenantControlPlane address, Service resource is not yet exposed as LoadBalancer")
}
for _, lb := range loadBalancerStatus.Ingress {
if ip := lb.IP; len(ip) > 0 {
return ip, nil
}
}
}
return "", fmt.Errorf("the actual resource doesn't have yet a valid IP address")
}

View File

@@ -0,0 +1,296 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
appv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/kamaji/internal/etcd"
)
// NetworkProfileSpec defines the desired state of NetworkProfile.
type NetworkProfileSpec struct {
// Address where API server of will be exposed.
// In case of LoadBalancer Service, this can be empty in order to use the exposed IP provided by the cloud controller manager.
Address string `json:"address,omitempty"`
// AllowAddressAsExternalIP will include tenantControlPlane.Spec.NetworkProfile.Address in the section of
// ExternalIPs of the Kubernetes Service (only ClusterIP or NodePort)
AllowAddressAsExternalIP bool `json:"allowAddressAsExternalIP,omitempty"`
// Port where API server of will be exposed
// +kubebuilder:default=6443
Port int32 `json:"port"`
// Domain of the tenant control plane
Domain string `json:"domain"`
// Kubernetes Service
ServiceCIDR string `json:"serviceCidr"`
// CIDR for Kubernetes Pods
PodCIDR string `json:"podCidr"`
DNSServiceIPs []string `json:"dnsServiceIPs"`
}
type KubeletSpec struct {
// CGroupFS defines the cgroup driver for Kubelet
// https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/configure-cgroup-driver/
CGroupFS CGroupDriver `json:"cgroupfs,omitempty"`
}
// KubernetesSpec defines the desired state of Kubernetes.
type KubernetesSpec struct {
// Kubernetes Version for the tenant control plane
Version string `json:"version"`
Kubelet KubeletSpec `json:"kubelet"`
// List of enabled Admission Controllers for the Tenant cluster.
// Full reference available here: https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers
// +kubebuilder:default=CertificateApproval;CertificateSigning;CertificateSubjectRestriction;DefaultIngressClass;DefaultStorageClass;DefaultTolerationSeconds;LimitRanger;MutatingAdmissionWebhook;NamespaceLifecycle;PersistentVolumeClaimResize;Priority;ResourceQuota;RuntimeClass;ServiceAccount;StorageObjectInUseProtection;TaintNodesByCondition;ValidatingAdmissionWebhook
AdmissionControllers AdmissionControllers `json:"admissionControllers,omitempty"`
}
// AdditionalMetadata defines which additional metadata, such as labels and annotations, must be attached to the created resource.
type AdditionalMetadata struct {
Labels map[string]string `json:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
}
// ControlPlane defines how the Tenant Control Plane Kubernetes resources must be created in the Admin Cluster,
// such as the number of Pod replicas, the Service resource, or the Ingress.
type ControlPlane struct {
// Defining the options for the deployed Tenant Control Plane as Deployment resource.
Deployment DeploymentSpec `json:"deployment,omitempty"`
// Defining the options for the Tenant Control Plane Service resource.
Service ServiceSpec `json:"service"`
// Defining the options for an Optional Ingress which will expose API Server of the Tenant Control Plane
Ingress IngressSpec `json:"ingress,omitempty"`
}
// IngressSpec defines the options for the ingress which will expose API Server of the Tenant Control Plane.
type IngressSpec struct {
AdditionalMetadata AdditionalMetadata `json:"additionalMetadata,omitempty"`
Enabled bool `json:"enabled"`
IngressClassName string `json:"ingressClassName,omitempty"`
// Hostname is an optional field which will be used as Ingress's Host. If it is not defined,
// Ingress's host will be "<tenant>.<namespace>.<domain>", where domain is specified under NetworkProfileSpec
Hostname string `json:"hostname,omitempty"`
}
type DeploymentSpec struct {
// +kubebuilder:default=2
Replicas int32 `json:"replicas,omitempty"`
AdditionalMetadata AdditionalMetadata `json:"additionalMetadata,omitempty"`
}
type ServiceSpec struct {
AdditionalMetadata AdditionalMetadata `json:"additionalMetadata,omitempty"`
// ServiceType allows specifying how to expose the Tenant Control Plane.
ServiceType ServiceType `json:"serviceType"`
}
// TenantControlPlaneSpec defines the desired state of TenantControlPlane.
type TenantControlPlaneSpec struct {
ControlPlane ControlPlane `json:"controlPlane"`
// Kubernetes specification for tenant control plane
Kubernetes KubernetesSpec `json:"kubernetes"`
// NetworkProfile specifies how the network is
NetworkProfile NetworkProfileSpec `json:"networkProfile,omitempty"`
}
// ETCDAPIServerCertificate defines the observed state of ETCD Certificate for API server.
type APIServerCertificatesStatus struct {
SecretName string `json:"secretName,omitempty"`
LastUpdate metav1.Time `json:"lastUpdate,omitempty"`
}
// ETCDAPIServerCertificate defines the observed state of ETCD Certificate for API server.
type ETCDCertificateStatus struct {
SecretName string `json:"secretName,omitempty"`
LastUpdate metav1.Time `json:"lastUpdate,omitempty"`
}
// ETCDAPIServerCertificate defines the observed state of ETCD Certificate for API server.
type ETCDCertificatesStatus struct {
APIServer ETCDCertificateStatus `json:"apiServer,omitempty"`
CA ETCDCertificateStatus `json:"ca,omitempty"`
}
// CertificatePrivateKeyPair defines the status.
type CertificatePrivateKeyPairStatus struct {
SecretName string `json:"secretName,omitempty"`
LastUpdate metav1.Time `json:"lastUpdate,omitempty"`
}
// CertificatePrivateKeyPair defines the status.
type PublicKeyPrivateKeyPairStatus struct {
SecretName string `json:"secretName,omitempty"`
LastUpdate metav1.Time `json:"lastUpdate,omitempty"`
}
// ETCDCertificates defines the observed state of ETCD Certificates.
type CertificatesStatus struct {
CA CertificatePrivateKeyPairStatus `json:"ca,omitempty"`
APIServer CertificatePrivateKeyPairStatus `json:"apiServer,omitempty"`
APIServerKubeletClient CertificatePrivateKeyPairStatus `json:"apiServerKubeletClient,omitempty"`
FrontProxyCA CertificatePrivateKeyPairStatus `json:"frontProxyCA,omitempty"`
FrontProxyClient CertificatePrivateKeyPairStatus `json:"frontProxyClient,omitempty"`
SA PublicKeyPrivateKeyPairStatus `json:"sa,omitempty"`
ETCD *ETCDCertificatesStatus `json:"etcd,omitempty"`
}
// ETCDStatus defines the observed state of ETCDStatus.
type ETCDStatus struct {
Role etcd.Role `json:"role,omitempty"`
User etcd.User `json:"user,omitempty"`
}
// StorageStatus defines the observed state of StorageStatus.
type StorageStatus struct {
ETCD *ETCDStatus `json:"etcd,omitempty"`
}
// TenantControlPlaneKubeconfigsStatus contains information about a the generated kubeconfig.
type KubeconfigStatus struct {
SecretName string `json:"secretName,omitempty"`
LastUpdate metav1.Time `json:"lastUpdate,omitempty"`
}
// TenantControlPlaneKubeconfigsStatus stores information about all the generated kubeconfigs.
type KubeconfigsStatus struct {
Admin KubeconfigStatus `json:"admin,omitempty"`
ControllerManager KubeconfigStatus `json:"controlerManager,omitempty"`
Scheduler KubeconfigStatus `json:"scheduler,omitempty"`
}
// KubeadmConfigStatus contains the status of the configuration required by kubeadm.
type KubeadmConfigStatus struct {
ConfigmapName string `json:"configmapName,omitempty"`
LastUpdate metav1.Time `json:"lastUpdate,omitempty"`
ResourceVersion string `json:"resourceVersion"`
}
// KubeadmPhasesStatus contains the status of of a kubeadm phase action.
type KubeadmPhaseStatus struct {
KubeadmConfigResourceVersion string `json:"kubeadmConfigResourceVersion,omitempty"`
LastUpdate metav1.Time `json:"lastUpdate,omitempty"`
}
// KubeadmPhasesStatus contains the status of the different kubeadm phases action.
type KubeadmPhasesStatus struct {
UploadConfigKubeadm KubeadmPhaseStatus `json:"uploadConfigKubeadm"`
UploadConfigKubelet KubeadmPhaseStatus `json:"uploadConfigKubelet"`
AddonCoreDNS KubeadmPhaseStatus `json:"addonCoreDNS"`
AddonKubeProxy KubeadmPhaseStatus `json:"addonKubeProxy"`
BootstrapToken KubeadmPhaseStatus `json:"bootstrapToken"`
}
// TenantControlPlaneStatus defines the observed state of TenantControlPlane.
type TenantControlPlaneStatus struct {
// Storage Status contains information about Kubernetes storage system
Storage StorageStatus `json:"storage,omitempty"`
// Certificates contains information about the different certificates
// that are necessary to run a kubernetes control plane
Certificates CertificatesStatus `json:"certificates,omitempty"`
// KubeConfig contains information about the kubenconfigs that control plane pieces need
KubeConfig KubeconfigsStatus `json:"kubeconfig,omitempty"`
// Kubernetes contains information about the reconciliation of the required Kubernetes resources deployed in the admin cluster
Kubernetes KubernetesStatus `json:"kubernetesResources,omitempty"`
// KubeadmConfig contains the status of the configuration required by kubeadm
KubeadmConfig KubeadmConfigStatus `json:"kubeadmconfig,omitempty"`
// KubeadmPhase contains the status of the kubeadm phases action
KubeadmPhase KubeadmPhasesStatus `json:"kubeadmPhase,omitempty"`
// ControlPlaneEndpoint contains the status of the kubernetes control plane
ControlPlaneEndpoint string `json:"controlPlaneEndpoint,omitempty"`
}
// KubernetesStatus defines the status of the resources deployed in the management cluster,
// such as Deployment and Service.
type KubernetesStatus struct {
// KubernetesVersion contains the information regarding the running Kubernetes version, and its upgrade status.
Version KubernetesVersion `json:"version,omitempty"`
Deployment KubernetesDeploymentStatus `json:"deployment,omitempty"`
Service KubernetesServiceStatus `json:"service,omitempty"`
Ingress KubernetesIngressStatus `json:"ingress,omitempty"`
}
// +kubebuilder:validation:Enum=Provisioning;Upgrading;Ready;NotReady
type KubernetesVersionStatus string
var (
VersionProvisioning KubernetesVersionStatus = "Provisioning"
VersionUpgrading KubernetesVersionStatus = "Upgrading"
VersionReady KubernetesVersionStatus = "Ready"
VersionNotReady KubernetesVersionStatus = "NotReady"
)
type KubernetesVersion struct {
// Version is the running Kubernetes version of the Tenant Control Plane.
Version string `json:"version,omitempty"`
// +kubebuilder:default=Provisioning
// Status returns the current status of the Kubernetes version, such as its provisioning state, or completed upgrade.
Status *KubernetesVersionStatus `json:"status"`
}
// KubernetesDeploymentStatus defines the status for the Tenant Control Plane Deployment in the management cluster.
type KubernetesDeploymentStatus struct {
appv1.DeploymentStatus `json:",inline"`
// The name of the Deployment for the given cluster.
Name string `json:"name"`
// The namespace which the Deployment for the given cluster is deployed.
Namespace string `json:"namespace"`
}
// KubernetesServiceStatus defines the status for the Tenant Control Plane Service in the management cluster.
type KubernetesServiceStatus struct {
corev1.ServiceStatus `json:",inline"`
// The name of the Service for the given cluster.
Name string `json:"name"`
// The namespace which the Service for the given cluster is deployed.
Namespace string `json:"namespace"`
// The port where the service is running
Port int32 `json:"port"`
}
// KubernetesIngressStatus defines the status for the Tenant Control Plane Ingress in the management cluster.
type KubernetesIngressStatus struct {
networkingv1.IngressStatus `json:",inline"`
// The name of the Ingress for the given cluster.
Name string `json:"name"`
// The namespace which the Ingress for the given cluster is deployed.
Namespace string `json:"namespace"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName=tcp
// +kubebuilder:printcolumn:name="Version",type="string",JSONPath=".spec.kubernetes.version",description="Kubernetes version"
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.kubernetesResources.version.status",description="Kubernetes version"
// +kubebuilder:printcolumn:name="Control-Plane-Endpoint",type="string",JSONPath=".status.controlPlaneEndpoint",description="Tenant Control Plane Endpoint (API server)"
// +kubebuilder:printcolumn:name="Kubeconfig",type="string",JSONPath=".status.kubeconfig.admin.secretName",description="Secret which contains admin kubeconfig"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
// TenantControlPlane is the Schema for the tenantcontrolplanes API.
type TenantControlPlane struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TenantControlPlaneSpec `json:"spec,omitempty"`
Status TenantControlPlaneStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// TenantControlPlaneList contains a list of TenantControlPlane.
type TenantControlPlaneList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []TenantControlPlane `json:"items"`
}
func init() {
SchemeBuilder.Register(&TenantControlPlane{}, &TenantControlPlaneList{})
}

37
api/v1alpha1/types.go Normal file
View File

@@ -0,0 +1,37 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import corev1 "k8s.io/api/core/v1"
// +kubebuilder:validation:Enum=AlwaysAdmit;AlwaysDeny;AlwaysPullImages;CertificateApproval;CertificateSigning;CertificateSubjectRestriction;DefaultIngressClass;DefaultStorageClass;DefaultTolerationSeconds;DenyEscalatingExec;DenyExecOnPrivileged;DenyServiceExternalIPs;EventRateLimit;ExtendedResourceToleration;ImagePolicyWebhook;LimitPodHardAntiAffinityTopology;LimitRanger;MutatingAdmissionWebhook;NamespaceAutoProvision;NamespaceExists;NamespaceLifecycle;NodeRestriction;OwnerReferencesPermissionEnforcement;PersistentVolumeClaimResize;PersistentVolumeLabel;PodNodeSelector;PodSecurity;PodSecurityPolicy;PodTolerationRestriction;Priority;ResourceQuota;RuntimeClass;SecurityContextDeny;ServiceAccount;StorageObjectInUseProtection;TaintNodesByCondition;ValidatingAdmissionWebhook
type AdmissionController string
type AdmissionControllers []AdmissionController
func (a AdmissionControllers) ToSlice() []string {
out := make([]string, len(a))
for i, v := range a {
out[i] = string(v)
}
return out
}
// +kubebuilder:validation:Enum=systemd;cgroupfs
type CGroupDriver string
func (c CGroupDriver) String() string {
return (string)(c)
}
const (
ServiceTypeLoadBalancer = (ServiceType)(corev1.ServiceTypeLoadBalancer)
ServiceTypeClusterIP = (ServiceType)(corev1.ServiceTypeClusterIP)
ServiceTypeNodePort = (ServiceType)(corev1.ServiceTypeNodePort)
)
// +kubebuilder:validation:Enum=ClusterIP;NodePort;LoadBalancer
type ServiceType corev1.ServiceType

View File

@@ -0,0 +1,598 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *APIServerCertificatesStatus) DeepCopyInto(out *APIServerCertificatesStatus) {
*out = *in
in.LastUpdate.DeepCopyInto(&out.LastUpdate)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServerCertificatesStatus.
func (in *APIServerCertificatesStatus) DeepCopy() *APIServerCertificatesStatus {
if in == nil {
return nil
}
out := new(APIServerCertificatesStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdditionalMetadata) DeepCopyInto(out *AdditionalMetadata) {
*out = *in
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Annotations != nil {
in, out := &in.Annotations, &out.Annotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalMetadata.
func (in *AdditionalMetadata) DeepCopy() *AdditionalMetadata {
if in == nil {
return nil
}
out := new(AdditionalMetadata)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in AdmissionControllers) DeepCopyInto(out *AdmissionControllers) {
{
in := &in
*out = make(AdmissionControllers, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdmissionControllers.
func (in AdmissionControllers) DeepCopy() AdmissionControllers {
if in == nil {
return nil
}
out := new(AdmissionControllers)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CertificatePrivateKeyPairStatus) DeepCopyInto(out *CertificatePrivateKeyPairStatus) {
*out = *in
in.LastUpdate.DeepCopyInto(&out.LastUpdate)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificatePrivateKeyPairStatus.
func (in *CertificatePrivateKeyPairStatus) DeepCopy() *CertificatePrivateKeyPairStatus {
if in == nil {
return nil
}
out := new(CertificatePrivateKeyPairStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CertificatesStatus) DeepCopyInto(out *CertificatesStatus) {
*out = *in
in.CA.DeepCopyInto(&out.CA)
in.APIServer.DeepCopyInto(&out.APIServer)
in.APIServerKubeletClient.DeepCopyInto(&out.APIServerKubeletClient)
in.FrontProxyCA.DeepCopyInto(&out.FrontProxyCA)
in.FrontProxyClient.DeepCopyInto(&out.FrontProxyClient)
in.SA.DeepCopyInto(&out.SA)
if in.ETCD != nil {
in, out := &in.ETCD, &out.ETCD
*out = new(ETCDCertificatesStatus)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificatesStatus.
func (in *CertificatesStatus) DeepCopy() *CertificatesStatus {
if in == nil {
return nil
}
out := new(CertificatesStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ControlPlane) DeepCopyInto(out *ControlPlane) {
*out = *in
in.Deployment.DeepCopyInto(&out.Deployment)
in.Service.DeepCopyInto(&out.Service)
in.Ingress.DeepCopyInto(&out.Ingress)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlane.
func (in *ControlPlane) DeepCopy() *ControlPlane {
if in == nil {
return nil
}
out := new(ControlPlane)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeploymentSpec) DeepCopyInto(out *DeploymentSpec) {
*out = *in
in.AdditionalMetadata.DeepCopyInto(&out.AdditionalMetadata)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentSpec.
func (in *DeploymentSpec) DeepCopy() *DeploymentSpec {
if in == nil {
return nil
}
out := new(DeploymentSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ETCDCertificateStatus) DeepCopyInto(out *ETCDCertificateStatus) {
*out = *in
in.LastUpdate.DeepCopyInto(&out.LastUpdate)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ETCDCertificateStatus.
func (in *ETCDCertificateStatus) DeepCopy() *ETCDCertificateStatus {
if in == nil {
return nil
}
out := new(ETCDCertificateStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ETCDCertificatesStatus) DeepCopyInto(out *ETCDCertificatesStatus) {
*out = *in
in.APIServer.DeepCopyInto(&out.APIServer)
in.CA.DeepCopyInto(&out.CA)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ETCDCertificatesStatus.
func (in *ETCDCertificatesStatus) DeepCopy() *ETCDCertificatesStatus {
if in == nil {
return nil
}
out := new(ETCDCertificatesStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ETCDStatus) DeepCopyInto(out *ETCDStatus) {
*out = *in
in.Role.DeepCopyInto(&out.Role)
in.User.DeepCopyInto(&out.User)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ETCDStatus.
func (in *ETCDStatus) DeepCopy() *ETCDStatus {
if in == nil {
return nil
}
out := new(ETCDStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IngressSpec) DeepCopyInto(out *IngressSpec) {
*out = *in
in.AdditionalMetadata.DeepCopyInto(&out.AdditionalMetadata)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressSpec.
func (in *IngressSpec) DeepCopy() *IngressSpec {
if in == nil {
return nil
}
out := new(IngressSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubeadmConfigStatus) DeepCopyInto(out *KubeadmConfigStatus) {
*out = *in
in.LastUpdate.DeepCopyInto(&out.LastUpdate)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeadmConfigStatus.
func (in *KubeadmConfigStatus) DeepCopy() *KubeadmConfigStatus {
if in == nil {
return nil
}
out := new(KubeadmConfigStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubeadmPhaseStatus) DeepCopyInto(out *KubeadmPhaseStatus) {
*out = *in
in.LastUpdate.DeepCopyInto(&out.LastUpdate)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeadmPhaseStatus.
func (in *KubeadmPhaseStatus) DeepCopy() *KubeadmPhaseStatus {
if in == nil {
return nil
}
out := new(KubeadmPhaseStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubeadmPhasesStatus) DeepCopyInto(out *KubeadmPhasesStatus) {
*out = *in
in.UploadConfigKubeadm.DeepCopyInto(&out.UploadConfigKubeadm)
in.UploadConfigKubelet.DeepCopyInto(&out.UploadConfigKubelet)
in.AddonCoreDNS.DeepCopyInto(&out.AddonCoreDNS)
in.AddonKubeProxy.DeepCopyInto(&out.AddonKubeProxy)
in.BootstrapToken.DeepCopyInto(&out.BootstrapToken)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeadmPhasesStatus.
func (in *KubeadmPhasesStatus) DeepCopy() *KubeadmPhasesStatus {
if in == nil {
return nil
}
out := new(KubeadmPhasesStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubeconfigStatus) DeepCopyInto(out *KubeconfigStatus) {
*out = *in
in.LastUpdate.DeepCopyInto(&out.LastUpdate)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeconfigStatus.
func (in *KubeconfigStatus) DeepCopy() *KubeconfigStatus {
if in == nil {
return nil
}
out := new(KubeconfigStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubeconfigsStatus) DeepCopyInto(out *KubeconfigsStatus) {
*out = *in
in.Admin.DeepCopyInto(&out.Admin)
in.ControllerManager.DeepCopyInto(&out.ControllerManager)
in.Scheduler.DeepCopyInto(&out.Scheduler)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeconfigsStatus.
func (in *KubeconfigsStatus) DeepCopy() *KubeconfigsStatus {
if in == nil {
return nil
}
out := new(KubeconfigsStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubeletSpec) DeepCopyInto(out *KubeletSpec) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeletSpec.
func (in *KubeletSpec) DeepCopy() *KubeletSpec {
if in == nil {
return nil
}
out := new(KubeletSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubernetesDeploymentStatus) DeepCopyInto(out *KubernetesDeploymentStatus) {
*out = *in
in.DeploymentStatus.DeepCopyInto(&out.DeploymentStatus)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesDeploymentStatus.
func (in *KubernetesDeploymentStatus) DeepCopy() *KubernetesDeploymentStatus {
if in == nil {
return nil
}
out := new(KubernetesDeploymentStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubernetesIngressStatus) DeepCopyInto(out *KubernetesIngressStatus) {
*out = *in
in.IngressStatus.DeepCopyInto(&out.IngressStatus)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesIngressStatus.
func (in *KubernetesIngressStatus) DeepCopy() *KubernetesIngressStatus {
if in == nil {
return nil
}
out := new(KubernetesIngressStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubernetesServiceStatus) DeepCopyInto(out *KubernetesServiceStatus) {
*out = *in
in.ServiceStatus.DeepCopyInto(&out.ServiceStatus)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesServiceStatus.
func (in *KubernetesServiceStatus) DeepCopy() *KubernetesServiceStatus {
if in == nil {
return nil
}
out := new(KubernetesServiceStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubernetesSpec) DeepCopyInto(out *KubernetesSpec) {
*out = *in
out.Kubelet = in.Kubelet
if in.AdmissionControllers != nil {
in, out := &in.AdmissionControllers, &out.AdmissionControllers
*out = make(AdmissionControllers, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesSpec.
func (in *KubernetesSpec) DeepCopy() *KubernetesSpec {
if in == nil {
return nil
}
out := new(KubernetesSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubernetesStatus) DeepCopyInto(out *KubernetesStatus) {
*out = *in
in.Version.DeepCopyInto(&out.Version)
in.Deployment.DeepCopyInto(&out.Deployment)
in.Service.DeepCopyInto(&out.Service)
in.Ingress.DeepCopyInto(&out.Ingress)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesStatus.
func (in *KubernetesStatus) DeepCopy() *KubernetesStatus {
if in == nil {
return nil
}
out := new(KubernetesStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubernetesVersion) DeepCopyInto(out *KubernetesVersion) {
*out = *in
if in.Status != nil {
in, out := &in.Status, &out.Status
*out = new(KubernetesVersionStatus)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesVersion.
func (in *KubernetesVersion) DeepCopy() *KubernetesVersion {
if in == nil {
return nil
}
out := new(KubernetesVersion)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkProfileSpec) DeepCopyInto(out *NetworkProfileSpec) {
*out = *in
if in.DNSServiceIPs != nil {
in, out := &in.DNSServiceIPs, &out.DNSServiceIPs
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkProfileSpec.
func (in *NetworkProfileSpec) DeepCopy() *NetworkProfileSpec {
if in == nil {
return nil
}
out := new(NetworkProfileSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PublicKeyPrivateKeyPairStatus) DeepCopyInto(out *PublicKeyPrivateKeyPairStatus) {
*out = *in
in.LastUpdate.DeepCopyInto(&out.LastUpdate)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicKeyPrivateKeyPairStatus.
func (in *PublicKeyPrivateKeyPairStatus) DeepCopy() *PublicKeyPrivateKeyPairStatus {
if in == nil {
return nil
}
out := new(PublicKeyPrivateKeyPairStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServiceSpec) DeepCopyInto(out *ServiceSpec) {
*out = *in
in.AdditionalMetadata.DeepCopyInto(&out.AdditionalMetadata)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceSpec.
func (in *ServiceSpec) DeepCopy() *ServiceSpec {
if in == nil {
return nil
}
out := new(ServiceSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StorageStatus) DeepCopyInto(out *StorageStatus) {
*out = *in
if in.ETCD != nil {
in, out := &in.ETCD, &out.ETCD
*out = new(ETCDStatus)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StorageStatus.
func (in *StorageStatus) DeepCopy() *StorageStatus {
if in == nil {
return nil
}
out := new(StorageStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantControlPlane) DeepCopyInto(out *TenantControlPlane) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantControlPlane.
func (in *TenantControlPlane) DeepCopy() *TenantControlPlane {
if in == nil {
return nil
}
out := new(TenantControlPlane)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TenantControlPlane) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantControlPlaneList) DeepCopyInto(out *TenantControlPlaneList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]TenantControlPlane, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantControlPlaneList.
func (in *TenantControlPlaneList) DeepCopy() *TenantControlPlaneList {
if in == nil {
return nil
}
out := new(TenantControlPlaneList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TenantControlPlaneList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantControlPlaneSpec) DeepCopyInto(out *TenantControlPlaneSpec) {
*out = *in
in.ControlPlane.DeepCopyInto(&out.ControlPlane)
in.Kubernetes.DeepCopyInto(&out.Kubernetes)
in.NetworkProfile.DeepCopyInto(&out.NetworkProfile)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantControlPlaneSpec.
func (in *TenantControlPlaneSpec) DeepCopy() *TenantControlPlaneSpec {
if in == nil {
return nil
}
out := new(TenantControlPlaneSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantControlPlaneStatus) DeepCopyInto(out *TenantControlPlaneStatus) {
*out = *in
in.Storage.DeepCopyInto(&out.Storage)
in.Certificates.DeepCopyInto(&out.Certificates)
in.KubeConfig.DeepCopyInto(&out.KubeConfig)
in.Kubernetes.DeepCopyInto(&out.Kubernetes)
in.KubeadmConfig.DeepCopyInto(&out.KubeadmConfig)
in.KubeadmPhase.DeepCopyInto(&out.KubeadmPhase)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantControlPlaneStatus.
func (in *TenantControlPlaneStatus) DeepCopy() *TenantControlPlaneStatus {
if in == nil {
return nil
}
out := new(TenantControlPlaneStatus)
in.DeepCopyInto(out)
return out
}

View File

@@ -0,0 +1,889 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.6.1
creationTimestamp: null
name: tenantcontrolplanes.kamaji.clastix.io
spec:
group: kamaji.clastix.io
names:
kind: TenantControlPlane
listKind: TenantControlPlaneList
plural: tenantcontrolplanes
shortNames:
- tcp
singular: tenantcontrolplane
scope: Namespaced
versions:
- additionalPrinterColumns:
- description: Kubernetes version
jsonPath: .spec.kubernetes.version
name: Version
type: string
- description: Kubernetes version
jsonPath: .status.kubernetesResources.version.status
name: Status
type: string
- description: Tenant Control Plane Endpoint (API server)
jsonPath: .status.controlPlaneEndpoint
name: Control-Plane-Endpoint
type: string
- description: Secret which contains admin kubeconfig
jsonPath: .status.kubeconfig.admin.secretName
name: Kubeconfig
type: string
- description: Age
jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: TenantControlPlane is the Schema for the tenantcontrolplanes
API.
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: TenantControlPlaneSpec defines the desired state of TenantControlPlane.
properties:
controlPlane:
description: ControlPlane defines how the Tenant Control Plane Kubernetes
resources must be created in the Admin Cluster, such as the number
of Pod replicas, the Service resource, or the Ingress.
properties:
deployment:
description: Defining the options for the deployed Tenant Control
Plane as Deployment resource.
properties:
additionalMetadata:
description: AdditionalMetadata defines which additional metadata,
such as labels and annotations, must be attached to the
created resource.
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
replicas:
default: 2
format: int32
type: integer
type: object
ingress:
description: Defining the options for an Optional Ingress which
will expose API Server of the Tenant Control Plane
properties:
additionalMetadata:
description: AdditionalMetadata defines which additional metadata,
such as labels and annotations, must be attached to the
created resource.
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
enabled:
type: boolean
hostname:
description: Hostname is an optional field which will be used
as Ingress's Host. If it is not defined, Ingress's host
will be "<tenant>.<namespace>.<domain>", where domain is
specified under NetworkProfileSpec
type: string
ingressClassName:
type: string
required:
- enabled
type: object
service:
description: Defining the options for the Tenant Control Plane
Service resource.
properties:
additionalMetadata:
description: AdditionalMetadata defines which additional metadata,
such as labels and annotations, must be attached to the
created resource.
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
serviceType:
description: ServiceType allows specifying how to expose the
Tenant Control Plane.
enum:
- ClusterIP
- NodePort
- LoadBalancer
type: string
required:
- serviceType
type: object
required:
- service
type: object
kubernetes:
description: Kubernetes specification for tenant control plane
properties:
admissionControllers:
default:
- CertificateApproval
- CertificateSigning
- CertificateSubjectRestriction
- DefaultIngressClass
- DefaultStorageClass
- DefaultTolerationSeconds
- LimitRanger
- MutatingAdmissionWebhook
- NamespaceLifecycle
- PersistentVolumeClaimResize
- Priority
- ResourceQuota
- RuntimeClass
- ServiceAccount
- StorageObjectInUseProtection
- TaintNodesByCondition
- ValidatingAdmissionWebhook
description: 'List of enabled Admission Controllers for the Tenant
cluster. Full reference available here: https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers'
items:
enum:
- AlwaysAdmit
- AlwaysDeny
- AlwaysPullImages
- CertificateApproval
- CertificateSigning
- CertificateSubjectRestriction
- DefaultIngressClass
- DefaultStorageClass
- DefaultTolerationSeconds
- DenyEscalatingExec
- DenyExecOnPrivileged
- DenyServiceExternalIPs
- EventRateLimit
- ExtendedResourceToleration
- ImagePolicyWebhook
- LimitPodHardAntiAffinityTopology
- LimitRanger
- MutatingAdmissionWebhook
- NamespaceAutoProvision
- NamespaceExists
- NamespaceLifecycle
- NodeRestriction
- OwnerReferencesPermissionEnforcement
- PersistentVolumeClaimResize
- PersistentVolumeLabel
- PodNodeSelector
- PodSecurity
- PodSecurityPolicy
- PodTolerationRestriction
- Priority
- ResourceQuota
- RuntimeClass
- SecurityContextDeny
- ServiceAccount
- StorageObjectInUseProtection
- TaintNodesByCondition
- ValidatingAdmissionWebhook
type: string
type: array
kubelet:
properties:
cgroupfs:
description: CGroupFS defines the cgroup driver for Kubelet
https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/configure-cgroup-driver/
enum:
- systemd
- cgroupfs
type: string
type: object
version:
description: Kubernetes Version for the tenant control plane
type: string
required:
- kubelet
- version
type: object
networkProfile:
description: NetworkProfile specifies how the network is
properties:
address:
description: Address where API server of will be exposed. In case
of LoadBalancer Service, this can be empty in order to use the
exposed IP provided by the cloud controller manager.
type: string
allowAddressAsExternalIP:
description: AllowAddressAsExternalIP will include tenantControlPlane.Spec.NetworkProfile.Address
in the section of ExternalIPs of the Kubernetes Service (only
ClusterIP or NodePort)
type: boolean
dnsServiceIPs:
items:
type: string
type: array
domain:
description: Domain of the tenant control plane
type: string
podCidr:
description: CIDR for Kubernetes Pods
type: string
port:
default: 6443
description: Port where API server of will be exposed
format: int32
type: integer
serviceCidr:
description: Kubernetes Service
type: string
required:
- dnsServiceIPs
- domain
- podCidr
- port
- serviceCidr
type: object
required:
- controlPlane
- kubernetes
type: object
status:
description: TenantControlPlaneStatus defines the observed state of TenantControlPlane.
properties:
certificates:
description: Certificates contains information about the different
certificates that are necessary to run a kubernetes control plane
properties:
apiServer:
description: CertificatePrivateKeyPair defines the status.
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
apiServerKubeletClient:
description: CertificatePrivateKeyPair defines the status.
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
ca:
description: CertificatePrivateKeyPair defines the status.
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
etcd:
description: ETCDAPIServerCertificate defines the observed state
of ETCD Certificate for API server.
properties:
apiServer:
description: ETCDAPIServerCertificate defines the observed
state of ETCD Certificate for API server.
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
ca:
description: ETCDAPIServerCertificate defines the observed
state of ETCD Certificate for API server.
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
type: object
frontProxyCA:
description: CertificatePrivateKeyPair defines the status.
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
frontProxyClient:
description: CertificatePrivateKeyPair defines the status.
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
sa:
description: CertificatePrivateKeyPair defines the status.
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
type: object
controlPlaneEndpoint:
description: ControlPlaneEndpoint contains the status of the kubernetes
control plane
type: string
kubeadmPhase:
description: KubeadmPhase contains the status of the kubeadm phases
action
properties:
addonCoreDNS:
description: KubeadmPhasesStatus contains the status of of a kubeadm
phase action.
properties:
kubeadmConfigResourceVersion:
type: string
lastUpdate:
format: date-time
type: string
type: object
addonKubeProxy:
description: KubeadmPhasesStatus contains the status of of a kubeadm
phase action.
properties:
kubeadmConfigResourceVersion:
type: string
lastUpdate:
format: date-time
type: string
type: object
bootstrapToken:
description: KubeadmPhasesStatus contains the status of of a kubeadm
phase action.
properties:
kubeadmConfigResourceVersion:
type: string
lastUpdate:
format: date-time
type: string
type: object
uploadConfigKubeadm:
description: KubeadmPhasesStatus contains the status of of a kubeadm
phase action.
properties:
kubeadmConfigResourceVersion:
type: string
lastUpdate:
format: date-time
type: string
type: object
uploadConfigKubelet:
description: KubeadmPhasesStatus contains the status of of a kubeadm
phase action.
properties:
kubeadmConfigResourceVersion:
type: string
lastUpdate:
format: date-time
type: string
type: object
required:
- addonCoreDNS
- addonKubeProxy
- bootstrapToken
- uploadConfigKubeadm
- uploadConfigKubelet
type: object
kubeadmconfig:
description: KubeadmConfig contains the status of the configuration
required by kubeadm
properties:
configmapName:
type: string
lastUpdate:
format: date-time
type: string
resourceVersion:
type: string
required:
- resourceVersion
type: object
kubeconfig:
description: KubeConfig contains information about the kubenconfigs
that control plane pieces need
properties:
admin:
description: TenantControlPlaneKubeconfigsStatus contains information
about a the generated kubeconfig.
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
controlerManager:
description: TenantControlPlaneKubeconfigsStatus contains information
about a the generated kubeconfig.
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
scheduler:
description: TenantControlPlaneKubeconfigsStatus contains information
about a the generated kubeconfig.
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
type: object
kubernetesResources:
description: Kubernetes contains information about the reconciliation
of the required Kubernetes resources deployed in the admin cluster
properties:
deployment:
description: KubernetesDeploymentStatus defines the status for
the Tenant Control Plane Deployment in the management cluster.
properties:
availableReplicas:
description: Total number of available pods (ready for at
least minReadySeconds) targeted by this deployment.
format: int32
type: integer
collisionCount:
description: Count of hash collisions for the Deployment.
The Deployment controller uses this field as a collision
avoidance mechanism when it needs to create the name for
the newest ReplicaSet.
format: int32
type: integer
conditions:
description: Represents the latest available observations
of a deployment's current state.
items:
description: DeploymentCondition describes the state of
a deployment at a certain point.
properties:
lastTransitionTime:
description: Last time the condition transitioned from
one status to another.
format: date-time
type: string
lastUpdateTime:
description: The last time this condition was updated.
format: date-time
type: string
message:
description: A human readable message indicating details
about the transition.
type: string
reason:
description: The reason for the condition's last transition.
type: string
status:
description: Status of the condition, one of True, False,
Unknown.
type: string
type:
description: Type of deployment condition.
type: string
required:
- status
- type
type: object
type: array
name:
description: The name of the Deployment for the given cluster.
type: string
namespace:
description: The namespace which the Deployment for the given
cluster is deployed.
type: string
observedGeneration:
description: The generation observed by the deployment controller.
format: int64
type: integer
readyReplicas:
description: readyReplicas is the number of pods targeted
by this Deployment with a Ready Condition.
format: int32
type: integer
replicas:
description: Total number of non-terminated pods targeted
by this deployment (their labels match the selector).
format: int32
type: integer
unavailableReplicas:
description: Total number of unavailable pods targeted by
this deployment. This is the total number of pods that are
still required for the deployment to have 100% available
capacity. They may either be pods that are running but not
yet available or pods that still have not been created.
format: int32
type: integer
updatedReplicas:
description: Total number of non-terminated pods targeted
by this deployment that have the desired template spec.
format: int32
type: integer
required:
- name
- namespace
type: object
ingress:
description: KubernetesIngressStatus defines the status for the
Tenant Control Plane Ingress in the management cluster.
properties:
loadBalancer:
description: LoadBalancer contains the current status of the
load-balancer.
properties:
ingress:
description: Ingress is a list containing ingress points
for the load-balancer. Traffic intended for the service
should be sent to these ingress points.
items:
description: 'LoadBalancerIngress represents the status
of a load-balancer ingress point: traffic intended
for the service should be sent to an ingress point.'
properties:
hostname:
description: Hostname is set for load-balancer ingress
points that are DNS based (typically AWS load-balancers)
type: string
ip:
description: IP is set for load-balancer ingress
points that are IP based (typically GCE or OpenStack
load-balancers)
type: string
ports:
description: Ports is a list of records of service
ports If used, every port defined in the service
should have an entry in it
items:
properties:
error:
description: 'Error is to record the problem
with the service port The format of the
error shall comply with the following rules:
- built-in error values shall be specified
in this file and those shall use CamelCase
names - cloud provider specific error values
must have names that comply with the format
foo.example.com/CamelCase. --- The regex
it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)'
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
port:
description: Port is the port number of the
service port of which status is recorded
here
format: int32
type: integer
protocol:
default: TCP
description: 'Protocol is the protocol of
the service port of which status is recorded
here The supported values are: "TCP", "UDP",
"SCTP"'
type: string
required:
- port
- protocol
type: object
type: array
x-kubernetes-list-type: atomic
type: object
type: array
type: object
name:
description: The name of the Ingress for the given cluster.
type: string
namespace:
description: The namespace which the Ingress for the given
cluster is deployed.
type: string
required:
- name
- namespace
type: object
service:
description: KubernetesServiceStatus defines the status for the
Tenant Control Plane Service in the management cluster.
properties:
conditions:
description: Current service state
items:
description: "Condition contains details for one aspect
of the current state of this API Resource. --- This struct
is intended for direct use as an array at the field path
.status.conditions. For example, type FooStatus struct{
\ // Represents the observations of a foo's current
state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type
\ // +patchStrategy=merge // +listType=map //
+listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\"
patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`
\n // other fields }"
properties:
lastTransitionTime:
description: lastTransitionTime is the last time the
condition transitioned from one status to another.
This should be when the underlying condition changed. If
that is not known, then using the time when the API
field changed is acceptable.
format: date-time
type: string
message:
description: message is a human readable message indicating
details about the transition. This may be an empty
string.
maxLength: 32768
type: string
observedGeneration:
description: observedGeneration represents the .metadata.generation
that the condition was set based upon. For instance,
if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration
is 9, the condition is out of date with respect to
the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: reason contains a programmatic identifier
indicating the reason for the condition's last transition.
Producers of specific condition types may define expected
values and meanings for this field, and whether the
values are considered a guaranteed API. The value
should be a CamelCase string. This field may not be
empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False,
Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
--- Many .condition.type values are consistent across
resources like Available, but because arbitrary conditions
can be useful (see .node.status.conditions), the ability
to deconflict is important. The regex it matches is
(dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
loadBalancer:
description: LoadBalancer contains the current status of the
load-balancer, if one is present.
properties:
ingress:
description: Ingress is a list containing ingress points
for the load-balancer. Traffic intended for the service
should be sent to these ingress points.
items:
description: 'LoadBalancerIngress represents the status
of a load-balancer ingress point: traffic intended
for the service should be sent to an ingress point.'
properties:
hostname:
description: Hostname is set for load-balancer ingress
points that are DNS based (typically AWS load-balancers)
type: string
ip:
description: IP is set for load-balancer ingress
points that are IP based (typically GCE or OpenStack
load-balancers)
type: string
ports:
description: Ports is a list of records of service
ports If used, every port defined in the service
should have an entry in it
items:
properties:
error:
description: 'Error is to record the problem
with the service port The format of the
error shall comply with the following rules:
- built-in error values shall be specified
in this file and those shall use CamelCase
names - cloud provider specific error values
must have names that comply with the format
foo.example.com/CamelCase. --- The regex
it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)'
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
port:
description: Port is the port number of the
service port of which status is recorded
here
format: int32
type: integer
protocol:
default: TCP
description: 'Protocol is the protocol of
the service port of which status is recorded
here The supported values are: "TCP", "UDP",
"SCTP"'
type: string
required:
- port
- protocol
type: object
type: array
x-kubernetes-list-type: atomic
type: object
type: array
type: object
name:
description: The name of the Service for the given cluster.
type: string
namespace:
description: The namespace which the Service for the given
cluster is deployed.
type: string
port:
description: The port where the service is running
format: int32
type: integer
required:
- name
- namespace
- port
type: object
version:
description: KubernetesVersion contains the information regarding
the running Kubernetes version, and its upgrade status.
properties:
status:
default: Provisioning
description: Status returns the current status of the Kubernetes
version, such as its provisioning state, or completed upgrade.
enum:
- Provisioning
- Upgrading
- Ready
- NotReady
type: string
version:
description: Version is the running Kubernetes version of
the Tenant Control Plane.
type: string
required:
- status
type: object
type: object
storage:
description: Storage Status contains information about Kubernetes
storage system
properties:
etcd:
description: ETCDStatus defines the observed state of ETCDStatus.
properties:
role:
properties:
exists:
type: boolean
name:
type: string
permissions:
items:
properties:
key:
type: string
rangeEnd:
type: string
type:
type: integer
type: object
type: array
required:
- exists
- name
type: object
user:
properties:
exists:
type: boolean
name:
type: string
roles:
items:
type: string
type: array
required:
- exists
- name
type: object
type: object
type: object
type: object
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@@ -0,0 +1,21 @@
# This kustomization.yaml is not intended to be run by itself,
# since it depends on service name and namespace that are out of this kustomize package.
# It should be run by config/default
resources:
- bases/kamaji.clastix.io_tenantcontrolplanes.yaml
#+kubebuilder:scaffold:crdkustomizeresource
patchesStrategicMerge:
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
# patches here are for enabling the conversion webhook for each CRD
#- patches/webhook_in_clusters.yaml
#+kubebuilder:scaffold:crdkustomizewebhookpatch
# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
# patches here are for enabling the CA injection for each CRD
#- patches/cainjection_in_clusters.yaml
#+kubebuilder:scaffold:crdkustomizecainjectionpatch
# the following config is for teaching kustomize how to do kustomization for CRDs.
configurations:
- kustomizeconfig.yaml

View File

@@ -0,0 +1,19 @@
# This file is for teaching kustomize how to substitute name and namespace reference in CRD
nameReference:
- kind: Service
version: v1
fieldSpecs:
- kind: CustomResourceDefinition
version: v1
group: apiextensions.k8s.io
path: spec/conversion/webhook/clientConfig/service/name
namespace:
- kind: CustomResourceDefinition
version: v1
group: apiextensions.k8s.io
path: spec/conversion/webhook/clientConfig/service/namespace
create: false
varReference:
- path: metadata/annotations

View File

@@ -0,0 +1,7 @@
# The following patch adds a directive for certmanager to inject CA into the CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
name: tenantcontrolplanes.kamaji.clastix.io

View File

@@ -0,0 +1,16 @@
# The following patch enables a conversion webhook for the CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: tenantcontrolplanes.kamaji.clastix.io
spec:
conversion:
strategy: Webhook
webhook:
clientConfig:
service:
namespace: system
name: webhook-service
path: /convert
conversionReviewVersions:
- v1

View File

@@ -0,0 +1,74 @@
# Adds namespace to all resources.
namespace: kamaji-system
# Value of this field is prepended to the
# names of all resources, e.g. a deployment named
# "wordpress" becomes "alices-wordpress".
# Note that it should also match with the prefix (text before '-') of the namespace
# field above.
namePrefix: kamaji-
# Labels to add to all resources and selectors.
#commonLabels:
# someName: someValue
bases:
- ../crd
- ../rbac
- ../manager
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
#- ../webhook
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
#- ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
#- ../prometheus
patchesStrategicMerge:
# Protect the /metrics endpoint by putting it behind auth.
# If you want your controller-manager to expose the /metrics
# endpoint w/o any authn/z, please comment the following line.
- manager_auth_proxy_patch.yaml
# Mount the controller config file for loading manager configurations
# through a ComponentConfig type
#- manager_config_patch.yaml
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
#- manager_webhook_patch.yaml
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
# 'CERTMANAGER' needs to be enabled to use ca injection
#- webhookcainjection_patch.yaml
# the following config is for teaching kustomize how to do var substitution
vars:
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
# objref:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert # this name should match the one in certificate.yaml
# fieldref:
# fieldpath: metadata.namespace
#- name: CERTIFICATE_NAME
# objref:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert # this name should match the one in certificate.yaml
#- name: SERVICE_NAMESPACE # namespace of the service
# objref:
# kind: Service
# version: v1
# name: webhook-service
# fieldref:
# fieldpath: metadata.namespace
#- name: SERVICE_NAME
# objref:
# kind: Service
# version: v1
# name: webhook-service

View File

@@ -0,0 +1,27 @@
# This patch inject a sidecar container which is a HTTP proxy for the
# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews.
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
spec:
containers:
- name: kube-rbac-proxy
image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0
args:
- "--secure-listen-address=0.0.0.0:8443"
- "--upstream=http://127.0.0.1:8080/"
- "--logtostderr=true"
- "--v=10"
ports:
- containerPort: 8443
protocol: TCP
name: https
- name: manager
args:
- "--health-probe-bind-address=:8081"
- "--metrics-bind-address=127.0.0.1:8080"
- "--leader-elect"

View File

@@ -0,0 +1,20 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
spec:
containers:
- name: manager
args:
- "--config=controller_manager_config.yaml"
volumeMounts:
- name: manager-config
mountPath: /controller_manager_config.yaml
subPath: controller_manager_config.yaml
volumes:
- name: manager-config
configMap:
name: manager-config

1051
config/install.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
apiVersion: controller-runtime.sigs.k8s.io/v1alpha1
kind: ControllerManagerConfig
health:
healthProbeBindAddress: :8081
metrics:
bindAddress: 127.0.0.1:8080
webhook:
port: 9443
leaderElection:
leaderElect: true
resourceName: 799b98bc.clastix.io

View File

@@ -0,0 +1,16 @@
resources:
- manager.yaml
generatorOptions:
disableNameSuffixHash: true
configMapGenerator:
- files:
- controller_manager_config.yaml
name: manager-config
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: controller
newName: quay.io/clastix/kamaji
newTag: latest

View File

@@ -0,0 +1,57 @@
apiVersion: v1
kind: Namespace
metadata:
labels:
control-plane: controller-manager
name: system
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
labels:
control-plane: controller-manager
spec:
selector:
matchLabels:
control-plane: controller-manager
replicas: 1
template:
metadata:
labels:
control-plane: controller-manager
spec:
securityContext:
runAsNonRoot: true
containers:
- command:
- /manager
args:
- --leader-elect
image: controller:latest
imagePullPolicy: IfNotPresent
name: manager
securityContext:
allowPrivilegeEscalation: false
livenessProbe:
httpGet:
path: /healthz
port: 8081
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /readyz
port: 8081
initialDelaySeconds: 5
periodSeconds: 10
resources:
limits:
cpu: 200m
memory: 100Mi
requests:
cpu: 100m
memory: 20Mi
serviceAccountName: controller-manager
terminationGracePeriodSeconds: 10

View File

@@ -0,0 +1,27 @@
# These resources constitute the fully configured set of manifests
# used to generate the 'manifests/' directory in a bundle.
resources:
- bases/operator.clusterserviceversion.yaml
- ../default
- ../samples
- ../scorecard
# [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix.
# Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager.
# These patches remove the unnecessary "cert" volume and its manager container volumeMount.
#patchesJson6902:
#- target:
# group: apps
# version: v1
# kind: Deployment
# name: controller-manager
# namespace: system
# patch: |-
# # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs.
# # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment.
# - op: remove
# path: /spec/template/spec/containers/1/volumeMounts/0
# # Remove the "cert" volume, since OLM will create and mount a set of certs.
# # Update the indices in this path if adding or removing volumes in the manager's Deployment.
# - op: remove
# path: /spec/template/spec/volumes/0

View File

@@ -0,0 +1,2 @@
resources:
- monitor.yaml

View File

@@ -0,0 +1,20 @@
# Prometheus Monitor Service (Metrics)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
control-plane: controller-manager
name: controller-manager-metrics-monitor
namespace: system
spec:
endpoints:
- path: /metrics
port: https
scheme: https
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
tlsConfig:
insecureSkipVerify: true
selector:
matchLabels:
control-plane: controller-manager

View File

@@ -0,0 +1,9 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: metrics-reader
rules:
- nonResourceURLs:
- "/metrics"
verbs:
- get

View File

@@ -0,0 +1,17 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: proxy-role
rules:
- apiGroups:
- authentication.k8s.io
resources:
- tokenreviews
verbs:
- create
- apiGroups:
- authorization.k8s.io
resources:
- subjectaccessreviews
verbs:
- create

View File

@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: proxy-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: proxy-role
subjects:
- kind: ServiceAccount
name: controller-manager
namespace: system

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
labels:
control-plane: controller-manager
name: controller-manager-metrics-service
namespace: system
spec:
ports:
- name: https
port: 8443
protocol: TCP
targetPort: https
selector:
control-plane: controller-manager

View File

@@ -0,0 +1,18 @@
resources:
# All RBAC will be applied under this service account in
# the deployment namespace. You may comment out this resource
# if your manager will use a service account that exists at
# runtime. Be sure to update RoleBinding and ClusterRoleBinding
# subjects if changing service account names.
- service_account.yaml
- role.yaml
- role_binding.yaml
- leader_election_role.yaml
- leader_election_role_binding.yaml
# Comment the following 4 lines if you want to disable
# the auth proxy (https://github.com/brancz/kube-rbac-proxy)
# which protects your /metrics endpoint.
- auth_proxy_service.yaml
- auth_proxy_role.yaml
- auth_proxy_role_binding.yaml
- auth_proxy_client_clusterrole.yaml

View File

@@ -0,0 +1,37 @@
# permissions to do leader election.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: leader-election-role
rules:
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch

View File

@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: leader-election-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: leader-election-role
subjects:
- kind: ServiceAccount
name: controller-manager
namespace: system

94
config/rbac/role.yaml Normal file
View File

@@ -0,0 +1,94 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
creationTimestamp: null
name: manager-role
rules:
- apiGroups:
- apps
resources:
- deployments
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ""
resources:
- configmaps
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ""
resources:
- secrets
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ""
resources:
- services
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- kamaji.clastix.io
resources:
- tenantcontrolplanes
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- kamaji.clastix.io
resources:
- tenantcontrolplanes/finalizers
verbs:
- update
- apiGroups:
- kamaji.clastix.io
resources:
- tenantcontrolplanes/status
verbs:
- get
- patch
- update
- apiGroups:
- networking.k8s.io
resources:
- ingresses
verbs:
- create
- delete
- get
- list
- patch
- update
- watch

View File

@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: manager-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: manager-role
subjects:
- kind: ServiceAccount
name: controller-manager
namespace: system

View File

@@ -0,0 +1,5 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: controller-manager
namespace: system

View File

@@ -0,0 +1,24 @@
# permissions for end users to edit clusters.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: tenantcontrolplane-editor-role
rules:
- apiGroups:
- kamaji.clastix.io
resources:
- tenantcontrolplanes
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- kamaji.clastix.io
resources:
- tenantcontrolplanes/status
verbs:
- get

View File

@@ -0,0 +1,20 @@
# permissions for end users to view clusters.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: tenantcontrolplane-viewer-role
rules:
- apiGroups:
- kamaji.clastix.io
resources:
- tenantcontrolplanes
verbs:
- get
- list
- watch
- apiGroups:
- kamaji.clastix.io
resources:
- tenantcontrolplanes/status
verbs:
- get

View File

@@ -0,0 +1,48 @@
apiVersion: kamaji.clastix.io/v1alpha1
kind: TenantControlPlane
metadata:
name: test
spec:
controlPlane:
deployment:
replicas: 2
additionalMetadata:
annotations:
environment.clastix.io: test
tier.clastix.io: "0"
labels:
tenant.clastix.io: test
kind.clastix.io: deployment
service:
additionalMetadata:
annotations:
environment.clastix.io: test
tier.clastix.io: "0"
labels:
tenant.clastix.io: test
kind.clastix.io: service
serviceType: ClusterIP
ingress:
enabled: true
hostname: kamaji.local
ingressClassName: nginx
additionalMetadata:
annotations:
kubernetes.io/ingress.allow-http: "false"
nginx.ingress.kubernetes.io/secure-backends: "true"
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
kubernetes:
version: "v1.23.1"
kubelet:
cgroupfs: systemd
admissionControllers:
- ResourceQuota
- LimitRanger
networkProfile:
address: "127.0.0.1"
port: 6443
domain: "clastix.labs"
serviceCidr: "10.96.0.0/16"
podCidr: "10.244.0.0/16"
dnsServiceIPs:
- "10.96.0.10"

View File

@@ -0,0 +1,4 @@
## Append samples you want in your CSV to this file as resources ##
resources:
- kamaji_v1alpha1_tenantcontrolplane.yaml
#+kubebuilder:scaffold:manifestskustomizesamples

View File

@@ -0,0 +1,7 @@
apiVersion: scorecard.operatorframework.io/v1alpha3
kind: Configuration
metadata:
name: config
stages:
- parallel: true
tests: []

View File

@@ -0,0 +1,16 @@
resources:
- bases/config.yaml
patchesJson6902:
- path: patches/basic.config.yaml
target:
group: scorecard.operatorframework.io
version: v1alpha3
kind: Configuration
name: config
- path: patches/olm.config.yaml
target:
group: scorecard.operatorframework.io
version: v1alpha3
kind: Configuration
name: config
#+kubebuilder:scaffold:patchesJson6902

View File

@@ -0,0 +1,10 @@
- op: add
path: /stages/0/tests/-
value:
entrypoint:
- scorecard-test
- basic-check-spec
image: quay.io/operator-framework/scorecard-test:v1.13.1
labels:
suite: basic
test: basic-check-spec-test

View File

@@ -0,0 +1,50 @@
- op: add
path: /stages/0/tests/-
value:
entrypoint:
- scorecard-test
- olm-bundle-validation
image: quay.io/operator-framework/scorecard-test:v1.13.1
labels:
suite: olm
test: olm-bundle-validation-test
- op: add
path: /stages/0/tests/-
value:
entrypoint:
- scorecard-test
- olm-crds-have-validation
image: quay.io/operator-framework/scorecard-test:v1.13.1
labels:
suite: olm
test: olm-crds-have-validation-test
- op: add
path: /stages/0/tests/-
value:
entrypoint:
- scorecard-test
- olm-crds-have-resources
image: quay.io/operator-framework/scorecard-test:v1.13.1
labels:
suite: olm
test: olm-crds-have-resources-test
- op: add
path: /stages/0/tests/-
value:
entrypoint:
- scorecard-test
- olm-spec-descriptors
image: quay.io/operator-framework/scorecard-test:v1.13.1
labels:
suite: olm
test: olm-spec-descriptors-test
- op: add
path: /stages/0/tests/-
value:
entrypoint:
- scorecard-test
- olm-status-descriptors
image: quay.io/operator-framework/scorecard-test:v1.13.1
labels:
suite: olm
test: olm-status-descriptors-test

65
controllers/suite_test.go Normal file
View File

@@ -0,0 +1,65 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package controllers
import (
"path/filepath"
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
)
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
var cfg *rest.Config // nolint
var k8sClient client.Client
var testEnv *envtest.Environment
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t,
"Controller Suite",
[]Reporter{printer.NewlineReporter{}})
}
var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
ErrorIfCRDPathMissing: true,
}
cfg, err := testEnv.Start()
Expect(err).NotTo(HaveOccurred())
Expect(cfg).NotTo(BeNil())
err = kamajiv1alpha1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
//+kubebuilder:scaffold:scheme
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient).NotTo(BeNil())
}, 60)
var _ = AfterSuite(func() {
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).NotTo(HaveOccurred())
})

View File

@@ -0,0 +1,350 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package controllers
import (
"context"
"fmt"
"strings"
"github.com/google/uuid"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
k8stypes "k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/resources"
)
const (
separator = ","
finalizer = "finalizer.kamaji.clastix.io"
)
// TenantControlPlaneReconciler reconciles a TenantControlPlane object.
type TenantControlPlaneReconciler struct {
client.Client
Scheme *runtime.Scheme
Config TenantControlPlaneReconcilerConfig
}
// TenantControlPlaneReconcilerConfig gives the necessary configuration for TenantControlPlaneReconciler.
type TenantControlPlaneReconcilerConfig struct {
ETCDCASecretName string
ETCDCASecretNamespace string
ETCDClientSecretName string
ETCDClientSecretNamespace string
ETCDEndpoints string
ETCDCompactionInterval string
TmpBaseDirectory string
}
//+kubebuilder:rbac:groups=kamaji.clastix.io,resources=tenantcontrolplanes,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=kamaji.clastix.io,resources=tenantcontrolplanes/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=kamaji.clastix.io,resources=tenantcontrolplanes/finalizers,verbs=update
// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete
func (r *TenantControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
tenantControlPlane := &kamajiv1alpha1.TenantControlPlane{}
isTenantControlPlane, err := r.getTenantControlPlane(ctx, req.NamespacedName, tenantControlPlane)
if err != nil {
return ctrl.Result{}, err
}
if !isTenantControlPlane {
return ctrl.Result{}, nil
}
markedToBeDeleted := tenantControlPlane.GetDeletionTimestamp() != nil
hasFinalizer := hasFinalizer(*tenantControlPlane)
if markedToBeDeleted && !hasFinalizer {
return ctrl.Result{}, nil
}
if markedToBeDeleted {
registeredDeleteableResources := []resources.DeleteableResource{
&resources.ETCDSetupResource{
Name: "etcd-setup",
Client: r.Client,
Scheme: r.Scheme,
Log: log,
ETCDClientCertsSecret: getNamespacedName(r.Config.ETCDClientSecretNamespace, r.Config.ETCDClientSecretName),
ETCDCACertsSecret: getNamespacedName(r.Config.ETCDCASecretNamespace, r.Config.ETCDCASecretName),
Endpoints: getArrayFromString(r.Config.ETCDEndpoints),
},
}
for _, resource := range registeredDeleteableResources {
if err := resource.Delete(ctx, tenantControlPlane); err != nil {
return ctrl.Result{}, err
}
}
if hasFinalizer {
controllerutil.RemoveFinalizer(tenantControlPlane, finalizer)
if err := r.Update(ctx, tenantControlPlane); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
if !hasFinalizer {
controllerutil.AddFinalizer(tenantControlPlane, finalizer)
if err := r.Update(ctx, tenantControlPlane); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
registeredResources := []resources.Resource{
&resources.KubernetesServiceResource{
Client: r.Client,
},
&resources.KubeadmConfigResource{
Name: "kubeadmconfig",
Port: tenantControlPlane.Spec.NetworkProfile.Port,
KubernetesVersion: tenantControlPlane.Spec.Kubernetes.Version,
PodCIDR: tenantControlPlane.Spec.NetworkProfile.PodCIDR,
ServiceCIDR: tenantControlPlane.Spec.NetworkProfile.ServiceCIDR,
Domain: tenantControlPlane.Spec.NetworkProfile.Domain,
ETCDs: getArrayFromString(r.Config.ETCDEndpoints),
ETCDCompactionInterval: r.Config.ETCDCompactionInterval,
Client: r.Client,
Scheme: r.Scheme,
Log: log,
TmpDirectory: getTmpDirectory(r.Config.TmpBaseDirectory, *tenantControlPlane),
},
&resources.CACertificate{
Name: "ca",
Client: r.Client,
Log: log,
TmpDirectory: getTmpDirectory(r.Config.TmpBaseDirectory, *tenantControlPlane),
},
&resources.FrontProxyCACertificate{
Name: "front-proxy-ca-certificate",
Client: r.Client,
Log: log,
TmpDirectory: getTmpDirectory(r.Config.TmpBaseDirectory, *tenantControlPlane),
},
&resources.SACertificate{
Name: "sa-certificate",
Client: r.Client,
Log: log,
TmpDirectory: getTmpDirectory(r.Config.TmpBaseDirectory, *tenantControlPlane),
},
&resources.APIServerCertificate{
Name: "api-server-certificate",
Client: r.Client,
Log: log,
TmpDirectory: getTmpDirectory(r.Config.TmpBaseDirectory, *tenantControlPlane),
},
&resources.APIServerKubeletClientCertificate{
Name: "api-server-kubelet-client-certificate",
Client: r.Client,
Log: log,
TmpDirectory: getTmpDirectory(r.Config.TmpBaseDirectory, *tenantControlPlane),
},
&resources.FrontProxyClientCertificate{
Name: "front-proxy-client-certificate",
Client: r.Client,
Log: log,
TmpDirectory: getTmpDirectory(r.Config.TmpBaseDirectory, *tenantControlPlane),
},
&resources.KubeconfigResource{
Name: "admin-kubeconfig",
Client: r.Client,
Scheme: r.Scheme,
Log: log,
KubeConfigFileName: resources.AdminKubeConfigFileName,
TmpDirectory: getTmpDirectory(r.Config.TmpBaseDirectory, *tenantControlPlane),
},
&resources.KubeconfigResource{
Name: "controller-manager-kubeconfig",
Client: r.Client,
Scheme: r.Scheme,
Log: log,
KubeConfigFileName: resources.ControllerManagerKubeConfigFileName,
TmpDirectory: getTmpDirectory(r.Config.TmpBaseDirectory, *tenantControlPlane),
},
&resources.KubeconfigResource{
Name: "scheduler-kubeconfig",
Client: r.Client,
Scheme: r.Scheme,
Log: log,
KubeConfigFileName: resources.SchedulerKubeConfigFileName,
TmpDirectory: getTmpDirectory(r.Config.TmpBaseDirectory, *tenantControlPlane),
},
&resources.ETCDCACertificatesResource{
Name: "etcd-ca-certificates",
Client: r.Client,
Log: log,
ETCDCASecretName: r.Config.ETCDCASecretName,
ETCDCASecretNamespace: r.Config.ETCDCASecretNamespace,
},
&resources.ETCDCertificatesResource{
Name: "etcd-certificates",
Client: r.Client,
Log: log,
},
&resources.ETCDSetupResource{
Name: "etcd-setup",
Client: r.Client,
Scheme: r.Scheme,
Log: log,
ETCDClientCertsSecret: getNamespacedName(r.Config.ETCDClientSecretNamespace, r.Config.ETCDClientSecretName),
ETCDCACertsSecret: getNamespacedName(r.Config.ETCDCASecretNamespace, r.Config.ETCDCASecretName),
Endpoints: getArrayFromString(r.Config.ETCDEndpoints),
},
&resources.KubernetesUpgrade{
Name: "upgrade",
Client: r.Client,
},
&resources.KubernetesDeploymentResource{
Client: r.Client,
ETCDEndpoints: getArrayFromString(r.Config.ETCDEndpoints),
ETCDCompactionInterval: r.Config.ETCDCompactionInterval,
},
&resources.KubernetesIngressResource{
Client: r.Client,
},
&resources.KubeadmPhaseResource{
Name: "upload-config-kubeadm",
Client: r.Client,
Log: log,
KubeadmPhase: resources.PhaseUploadConfigKubeadm,
},
&resources.KubeadmPhaseResource{
Name: "upload-config-kubelet",
Client: r.Client,
Log: log,
KubeadmPhase: resources.PhaseUploadConfigKubelet,
},
&resources.KubeadmPhaseResource{
Name: "addon-coredns",
Client: r.Client,
Log: log,
KubeadmPhase: resources.PhaseAddonCoreDNS,
},
&resources.KubeadmPhaseResource{
Name: "addon-kubeproxy",
Client: r.Client,
Log: log,
KubeadmPhase: resources.PhaseAddonKubeProxy,
},
&resources.KubeadmPhaseResource{
Name: "bootstrap-token",
Client: r.Client,
Log: log,
KubeadmPhase: resources.PhaseBootstrapToken,
},
}
for _, resource := range registeredResources {
result, err := resources.Handle(ctx, resource, tenantControlPlane)
if err != nil {
return ctrl.Result{}, err
}
if result == controllerutil.OperationResultNone {
continue
}
if err := r.updateStatus(ctx, req.NamespacedName, resource); err != nil {
return ctrl.Result{}, err
}
log.Info(fmt.Sprintf("%s has been configured", resource.GetName()))
return ctrl.Result{}, nil
}
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *TenantControlPlaneReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&kamajiv1alpha1.TenantControlPlane{}).
Owns(&corev1.Secret{}).
Owns(&corev1.ConfigMap{}).
Owns(&appsv1.Deployment{}).
Owns(&corev1.Service{}).
Owns(&networkingv1.Ingress{}).
Complete(r)
}
func (r *TenantControlPlaneReconciler) getTenantControlPlane(ctx context.Context, namespacedName k8stypes.NamespacedName, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (bool, error) {
if err := r.Client.Get(ctx, namespacedName, tenantControlPlane); err != nil {
if !k8serrors.IsNotFound(err) {
return false, err
}
return false, nil
}
return true, nil
}
func (r *TenantControlPlaneReconciler) updateStatus(ctx context.Context, namespacedName k8stypes.NamespacedName, resource resources.Resource) error {
tenantControlPlane := &kamajiv1alpha1.TenantControlPlane{}
isTenantControlPlane, err := r.getTenantControlPlane(ctx, namespacedName, tenantControlPlane)
if err != nil {
return err
}
if !isTenantControlPlane {
return fmt.Errorf("error updating tenantControlPlane %s: not found", namespacedName.Name)
}
if err := resource.UpdateTenantControlPlaneStatus(ctx, tenantControlPlane); err != nil {
return err
}
if err := r.Status().Update(ctx, tenantControlPlane); err != nil {
return fmt.Errorf("error updating tenantControlPlane status: %w", err)
}
return nil
}
func getArrayFromString(s string) []string {
var a []string
a = append(a, strings.Split(s, separator)...)
return a
}
func getNamespacedName(namespace string, name string) k8stypes.NamespacedName {
return k8stypes.NamespacedName{Namespace: namespace, Name: name}
}
func getTmpDirectory(base string, tenantControlPlane kamajiv1alpha1.TenantControlPlane) string {
return fmt.Sprintf("%s/%s/%s", base, tenantControlPlane.GetName(), uuid.New())
}
func hasFinalizer(tenantControlPlane kamajiv1alpha1.TenantControlPlane) bool {
for _, f := range tenantControlPlane.GetFinalizers() {
if f == finalizer {
return true
}
}
return false
}

142
go.mod Normal file
View File

@@ -0,0 +1,142 @@
module github.com/clastix/kamaji
go 1.17
require (
github.com/go-logr/logr v1.2.0
github.com/google/uuid v1.1.2
github.com/onsi/ginkgo v1.16.5
github.com/onsi/gomega v1.17.0
github.com/pkg/errors v0.9.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.10.1
go.etcd.io/etcd/api/v3 v3.5.1
go.etcd.io/etcd/client/v3 v3.5.1
google.golang.org/grpc v1.43.0
k8s.io/api v0.23.5
k8s.io/apimachinery v0.23.5
k8s.io/client-go v0.23.5
k8s.io/cluster-bootstrap v0.0.0
k8s.io/component-base v0.23.5
k8s.io/kube-proxy v0.0.0
k8s.io/kubelet v0.0.0
k8s.io/kubernetes v1.23.5
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
sigs.k8s.io/controller-runtime v0.11.0
)
require (
cloud.google.com/go v0.99.0 // indirect
cloud.google.com/go/storage v1.18.2 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.18 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/Microsoft/go-winio v0.4.17 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
github.com/cncf/xds/go v0.0.0-20211216145620-d92e9ce0af51 // indirect
github.com/coredns/caddy v1.1.0 // indirect
github.com/coredns/corefile-migration v1.0.14 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/envoyproxy/go-control-plane v0.10.1 // indirect
github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-logr/zapr v1.2.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/spf13/afero v1.7.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/cobra v1.4.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.1 // indirect
go.opencensus.io v0.23.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.19.1 // indirect
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa // indirect
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
google.golang.org/api v0.63.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/apiextensions-apiserver v0.23.5 // indirect
k8s.io/apiserver v0.23.5 // indirect
k8s.io/klog/v2 v2.60.1 // indirect
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
k8s.io/system-validators v1.6.0 // indirect
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
replace (
k8s.io/api => k8s.io/api v0.23.5
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.23.5
k8s.io/apimachinery => k8s.io/apimachinery v0.23.5
k8s.io/apiserver => k8s.io/apiserver v0.23.5
k8s.io/cli-runtime => k8s.io/cli-runtime v0.23.5
k8s.io/client-go => k8s.io/client-go v0.23.5
k8s.io/cloud-provider => k8s.io/cloud-provider v0.23.5
k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.23.5
k8s.io/code-generator => k8s.io/code-generator v0.23.5
k8s.io/component-base => k8s.io/component-base v0.23.5
k8s.io/component-helpers => k8s.io/component-helpers v0.23.5
k8s.io/controller-manager => k8s.io/controller-manager v0.23.5
k8s.io/cri-api => k8s.io/cri-api v0.23.5
k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.23.5
k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.23.5
k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.23.5
k8s.io/kube-proxy => k8s.io/kube-proxy v0.23.5
k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.23.5
k8s.io/kubectl => k8s.io/kubectl v0.23.5
k8s.io/kubelet => k8s.io/kubelet v0.23.5
k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.23.5
k8s.io/metrics => k8s.io/metrics v0.23.5
k8s.io/mount-utils => k8s.io/mount-utils v0.23.5
k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.23.5
k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.23.5
)

1710
go.sum Normal file

File diff suppressed because it is too large Load Diff

2
hack/boilerplate.go.txt Normal file
View File

@@ -0,0 +1,2 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

15
helm/README.md Normal file
View File

@@ -0,0 +1,15 @@
# Deploy with Helm
## Pre-requisites
1. Deploy a [multi-tenant Etcd cluster](https://github.com/clastix/kamaji-internal/blob/master/deploy/getting-started-with-kamaji.md#setup-internal-multi-tenant-etcd)
```
make -C ../deploy/etcd
```
## Install
```
helm upgrade --install --namespace kamaji-system --create-namespace kamaji ./kamaji
```

23
helm/kamaji/.helmignore Normal file
View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

35
helm/kamaji/Chart.yaml Normal file
View File

@@ -0,0 +1,35 @@
apiVersion: v2
name: kamaji
description: A Kubernetes distribution aimed to build and operate a Managed Kubernetes service with a fraction of operational burde.
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: 0.1.0
home: https://github.com/clastix/kamaji-internal/tree/master/helm/kamaji
sources: ["https://github.com/clastix/kamaji-internal"]
kubeVersion: ">=1.18"
maintainers:
- email: iam@mendrugory.com
name: Gonzalo Gabriel Jiménez Fuentes
- email: dario@tranchitella.eu
name: Dario Tranchitella
- email: me@maxgio.it
name: Massimiliano Giovagnoli

9
helm/kamaji/Makefile Normal file
View File

@@ -0,0 +1,9 @@
docs: HELMDOCS_VERSION := v1.8.1
docs: docker
@docker run --rm -v "$$(pwd):/helm-docs" -u $$(id -u) jnorwood/helm-docs:$(HELMDOCS_VERSION)
docker:
@hash docker 2>/dev/null || {\
echo "You need docker" &&\
exit 1;\
}

100
helm/kamaji/README.md Normal file
View File

@@ -0,0 +1,100 @@
# kamaji
![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.1.0](https://img.shields.io/badge/AppVersion-0.1.0-informational?style=flat-square)
A Kubernetes distribution aimed to build and operate a Managed Kubernetes service with a fraction of operational burde.
**Homepage:** <https://github.com/clastix/kamaji-internal/tree/master/helm/kamaji>
## Installing the Chart
To install the chart with the release name `kamaji`:
### Pre-requisites
1. Deploy a [multi-tenant Etcd cluster](https://github.com/clastix/kamaji-internal/blob/master/deploy/getting-started-with-kamaji.md#setup-internal-multi-tenant-etcd)
2. Create the `Secret` containing the Etcd CA cert keypair:
```
kubectl -n kamaji-system create secret generic etcd-certs \
--from-file=/path/to/etcd/ca.crt \
--from-file=/path/to/etcd/ca.key
```
3. Create a `Secret` containing the Etcd root user client cert keypair:
```
kubectl -n kamaji-system create secret tls root-client-certs \
--cert=/path/to/etcd/root.pem \
--key=/path/to/etcd/root-key.pem
```
### Install Kamaji
```console
helm upgrade --install --namespace kamaji-system --create-namespace kamaji .
```
## Maintainers
| Name | Email | Url |
| ---- | ------ | --- |
| Gonzalo Gabriel Jiménez Fuentes | <iam@mendrugory.com> | |
| Dario Tranchitella | <dario@tranchitella.eu> | |
| Massimiliano Giovagnoli | <me@maxgio.it> | |
## Source Code
* <https://github.com/clastix/kamaji-internal>
## Requirements
Kubernetes: `>=1.18`
## Values
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| affinity | object | `{}` | Kubernetes affinity rules to apply to Kamaji controller pods |
| configPath | string | `"./kamaji.yaml"` | Configuration file path alternative. (default "./kamaji.yaml") |
| etcd.caSecret.name | string | `"etcd-certs"` | Name of the secret which contains CA's certificate and private key. (default: "etcd-certs") |
| etcd.caSecret.namespace | string | `"kamaji-system"` | Namespace of the secret which contains CA's certificate and private key. (default: "kamaji") |
| etcd.clientSecret.name | string | `"root-client-certs"` | Name of the secret which contains ETCD client certificates. (default: "root-client-certs") |
| etcd.clientSecret.namespace | string | `"kamaji-system"` | Name of the namespace where the secret which contains ETCD client certificates is. (default: "kamaji") |
| etcd.compactionInterval | int | `0` | ETCD Compaction interval (e.g. "5m0s"). (default: "0" (disabled)) |
| etcd.endpoints | string | `"etcd-0.etcd.kamaji-system.svc.cluster.local:2379,etcd-1.etcd.kamaji-system.svc.cluster.local:2379,etcd-2.etcd.kamaji-system.svc.cluster.local:2379"` | (string) Comma-separated list of the endpoints of the etcd cluster's members. |
| extraArgs | list | `[]` | A list of extra arguments to add to the kamaji controller default ones |
| fullnameOverride | string | `""` | |
| healthProbeBindAddress | string | `":8081"` | The address the probe endpoint binds to. (default ":8081") |
| image.pullPolicy | string | `"IfNotPresent"` | |
| image.repository | string | `"quay.io/clastix/kamaji"` | The container image of the Kamaji controller. |
| image.tag | string | `"latest"` | |
| imagePullSecrets | list | `[]` | |
| ingress.annotations | object | `{}` | |
| ingress.className | string | `""` | Name of the ingress class to route through this controller. |
| ingress.enabled | bool | `false` | Whether to expose the Kamaji controller through an Ingress. |
| ingress.hosts[0].host | string | `"chart-example.local"` | |
| ingress.hosts[0].paths[0].path | string | `"/"` | |
| ingress.hosts[0].paths[0].pathType | string | `"ImplementationSpecific"` | |
| ingress.tls | list | `[]` | |
| livenessProbe | object | `{"httpGet":{"path":"/healthz","port":"healthcheck"},"initialDelaySeconds":15,"periodSeconds":20}` | The livenessProbe for the controller container |
| loggingDevel.enable | bool | `false` | (string) Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error) (default false) |
| metricsBindAddress | string | `":8080"` | (string) The address the metric endpoint binds to. (default ":8080") |
| nameOverride | string | `""` | |
| nodeSelector | object | `{}` | Kubernetes node selector rules to schedule Kamaji controller |
| podAnnotations | object | `{}` | The annotations to apply to the Kamaji controller pods. |
| podSecurityContext | object | `{"runAsNonRoot":true}` | The securityContext to apply to the Kamaji controller pods. |
| readinessProbe | object | `{"httpGet":{"path":"/readyz","port":"healthcheck"},"initialDelaySeconds":5,"periodSeconds":10}` | The readinessProbe for the controller container |
| replicaCount | int | `1` | The number of the pod replicas for the Kamaji controller. |
| resources.limits.cpu | string | `"200m"` | |
| resources.limits.memory | string | `"100Mi"` | |
| resources.requests.cpu | string | `"100m"` | |
| resources.requests.memory | string | `"20Mi"` | |
| securityContext | object | `{"allowPrivilegeEscalation":false}` | The securityContext to apply to the Kamaji controller container only. It does not apply to the Kamaji RBAC proxy container. |
| service.port | int | `8443` | |
| service.type | string | `"ClusterIP"` | |
| serviceAccount.annotations | object | `{}` | |
| serviceAccount.create | bool | `true` | |
| serviceAccount.name | string | `"kamaji-controller-manager"` | |
| temporaryDirectoryPath | string | `"/tmp/kamaji"` | Directory which will be used to work with temporary files. (default "/tmp/kamaji") |
| tolerations | list | `[]` | Kubernetes node taints that the Kamaji controller pods would tolerate |

View File

@@ -0,0 +1,45 @@
{{ template "chart.header" . }}
{{ template "chart.deprecationWarning" . }}
{{ template "chart.badgesSection" . }}
{{ template "chart.description" . }}
{{ template "chart.homepageLine" . }}
## Installing the Chart
To install the chart with the release name `kamaji`:
### Pre-requisites
1. Deploy a [multi-tenant Etcd cluster](https://github.com/clastix/kamaji-internal/blob/master/deploy/getting-started-with-kamaji.md#setup-internal-multi-tenant-etcd)
2. Create the `Secret` containing the Etcd CA cert keypair:
```
kubectl -n kamaji-system create secret generic etcd-certs \
--from-file=/path/to/etcd/ca.crt \
--from-file=/path/to/etcd/ca.key
```
3. Create a `Secret` containing the Etcd root user client cert keypair:
```
kubectl -n kamaji-system create secret tls root-client-certs \
--cert=/path/to/etcd/root.pem \
--key=/path/to/etcd/root-key.pem
```
### Install Kamaji
```console
helm upgrade --install --namespace kamaji-system --create-namespace kamaji .
```
{{ template "chart.maintainersSection" . }}
{{ template "chart.sourcesSection" . }}
{{ template "chart.requirementsSection" . }}
{{ template "chart.valuesSection" . }}

View File

@@ -0,0 +1,716 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.6.1
creationTimestamp: null
name: tenantcontrolplanes.kamaji.clastix.io
spec:
group: kamaji.clastix.io
names:
kind: TenantControlPlane
listKind: TenantControlPlaneList
plural: tenantcontrolplanes
shortNames:
- tcp
singular: tenantcontrolplane
scope: Namespaced
versions:
- additionalPrinterColumns:
- description: Kubernetes version
jsonPath: .spec.kubernetes.version
name: Version
type: string
- description: Tenant Control Plane Endpoint (API server)
jsonPath: .status.controlPlaneEndpoint
name: Control-Plane-Endpoint
type: string
- description: Secret which contains admin kubeconfig
jsonPath: .status.kubeconfig.admin.secretName
name: Kubeconfig
type: string
- description: Age
jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: TenantControlPlane is the Schema for the tenantcontrolplanes API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: TenantControlPlaneSpec defines the desired state of TenantControlPlane
properties:
controlPlane:
description: ControlPlane defines how the Tenant Control Plane Kubernetes resources must be created in the Admin Cluster, such as the number of Pod replicas, the Service resource, or the Ingress.
properties:
deployment:
description: Defining the options for the deployed Tenant Control Plane as Deployment resource.
properties:
additionalMetadata:
description: AdditionalMetadata defines which additional metadata, such as labels and annotations, must be attached to the created resource.
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
replicas:
default: 2
format: int32
type: integer
type: object
ingress:
description: Defining the options for an Optional Ingress which will expose API Server of the Tenant Control Plane
properties:
additionalMetadata:
description: AdditionalMetadata defines which additional metadata, such as labels and annotations, must be attached to the created resource.
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
enabled:
type: boolean
hostname:
description: Hostname is an optional field which will be used as Ingress's Host. If it is not defined, Ingress's host will be "<tenant>.<namespace>.<domain>", where domain is specified under NetworkProfileSpec
type: string
ingressClassName:
type: string
required:
- enabled
type: object
service:
description: Defining the options for the Tenant Control Plane Service resource.
properties:
additionalMetadata:
description: AdditionalMetadata defines which additional metadata, such as labels and annotations, must be attached to the created resource.
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
serviceType:
description: ServiceType allows specifying how to expose the Tenant Control Plane.
enum:
- ClusterIP
- NodePort
- LoadBalancer
type: string
required:
- serviceType
type: object
required:
- service
type: object
kubernetes:
description: Kubernetes specification for tenant control plane
properties:
admissionControllers:
default:
- CertificateApproval
- CertificateSigning
- CertificateSubjectRestriction
- DefaultIngressClass
- DefaultStorageClass
- DefaultTolerationSeconds
- LimitRanger
- MutatingAdmissionWebhook
- NamespaceLifecycle
- PersistentVolumeClaimResize
- Priority
- ResourceQuota
- RuntimeClass
- ServiceAccount
- StorageObjectInUseProtection
- TaintNodesByCondition
- ValidatingAdmissionWebhook
description: 'List of enabled Admission Controllers for the Tenant cluster. Full reference available here: https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers'
items:
enum:
- AlwaysAdmit
- AlwaysDeny
- AlwaysPullImages
- CertificateApproval
- CertificateSigning
- CertificateSubjectRestriction
- DefaultIngressClass
- DefaultStorageClass
- DefaultTolerationSeconds
- DenyEscalatingExec
- DenyExecOnPrivileged
- DenyServiceExternalIPs
- EventRateLimit
- ExtendedResourceToleration
- ImagePolicyWebhook
- LimitPodHardAntiAffinityTopology
- LimitRanger
- MutatingAdmissionWebhook
- NamespaceAutoProvision
- NamespaceExists
- NamespaceLifecycle
- NodeRestriction
- OwnerReferencesPermissionEnforcement
- PersistentVolumeClaimResize
- PersistentVolumeLabel
- PodNodeSelector
- PodSecurity
- PodSecurityPolicy
- PodTolerationRestriction
- Priority
- ResourceQuota
- RuntimeClass
- SecurityContextDeny
- ServiceAccount
- StorageObjectInUseProtection
- TaintNodesByCondition
- ValidatingAdmissionWebhook
type: string
type: array
kubelet:
properties:
cgroupfs:
description: CGroupFS defines the cgroup driver for Kubelet https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/configure-cgroup-driver/
enum:
- systemd
- cgroupfs
type: string
type: object
version:
description: Kubernetes Version for the tenant control plane
type: string
required:
- kubelet
- version
type: object
networkProfile:
description: NetworkProfile specifies how the network is
properties:
address:
description: Address where API server of will be exposed
type: string
dnsServiceIPs:
items:
type: string
type: array
domain:
description: Domain of the tenant control plane
type: string
podCidr:
description: CIDR for Kubernetes Pods
type: string
port:
default: 6443
description: Port where API server of will be exposed
format: int32
type: integer
serviceCidr:
description: Kubernetes Service
type: string
required:
- address
- dnsServiceIPs
- domain
- podCidr
- port
- serviceCidr
type: object
required:
- controlPlane
- kubernetes
type: object
status:
description: TenantControlPlaneStatus defines the observed state of TenantControlPlane
properties:
certificates:
description: Certificates contains information about the different certificates that are necessary to run a kubernetes control plane
properties:
apiServer:
description: CertificatePrivateKeyPair defines the status
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
apiServerKubeletClient:
description: CertificatePrivateKeyPair defines the status
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
ca:
description: CertificatePrivateKeyPair defines the status
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
etcd:
description: ETCDAPIServerCertificate defines the observed state of ETCD Certificate for API server
properties:
apiServer:
description: ETCDAPIServerCertificate defines the observed state of ETCD Certificate for API server
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
ca:
description: ETCDAPIServerCertificate defines the observed state of ETCD Certificate for API server
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
type: object
frontProxyCA:
description: CertificatePrivateKeyPair defines the status
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
frontProxyClient:
description: CertificatePrivateKeyPair defines the status
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
sa:
description: CertificatePrivateKeyPair defines the status
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
type: object
controlPlaneEndpoint:
description: ControlPlaneEndpoint contains the status of the kubernetes control plane
type: string
kubeadmPhase:
description: KubeadmPhase contains the status of the kubeadm phases action
properties:
addonCoreDNS:
description: KubeadmPhasesStatus contains the status of of a kubeadm phase action
properties:
kubeadmConfigResourceVersion:
type: string
lastUpdate:
format: date-time
type: string
type: object
addonKubeProxy:
description: KubeadmPhasesStatus contains the status of of a kubeadm phase action
properties:
kubeadmConfigResourceVersion:
type: string
lastUpdate:
format: date-time
type: string
type: object
bootstrapToken:
description: KubeadmPhasesStatus contains the status of of a kubeadm phase action
properties:
kubeadmConfigResourceVersion:
type: string
lastUpdate:
format: date-time
type: string
type: object
uploadConfigKubeadm:
description: KubeadmPhasesStatus contains the status of of a kubeadm phase action
properties:
kubeadmConfigResourceVersion:
type: string
lastUpdate:
format: date-time
type: string
type: object
uploadConfigKubelet:
description: KubeadmPhasesStatus contains the status of of a kubeadm phase action
properties:
kubeadmConfigResourceVersion:
type: string
lastUpdate:
format: date-time
type: string
type: object
required:
- addonCoreDNS
- addonKubeProxy
- bootstrapToken
- uploadConfigKubeadm
- uploadConfigKubelet
type: object
kubeadmconfig:
description: KubeadmConfig contains the status of the configuration required by kubeadm
properties:
configmapName:
type: string
lastUpdate:
format: date-time
type: string
resourceVersion:
type: string
required:
- resourceVersion
type: object
kubeconfig:
description: KubeConfig contains information about the kubenconfigs that control plane pieces need
properties:
admin:
description: TenantControlPlaneKubeconfigsStatus contains information about a the generated kubeconfig
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
controlerManager:
description: TenantControlPlaneKubeconfigsStatus contains information about a the generated kubeconfig
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
scheduler:
description: TenantControlPlaneKubeconfigsStatus contains information about a the generated kubeconfig
properties:
lastUpdate:
format: date-time
type: string
secretName:
type: string
type: object
type: object
kubernetesResources:
description: KubernetesResources contains information about the reconciliation of the required Kubernetes resources deployed in the admin cluster
properties:
deployment:
description: KubernetesDeploymentStatus defines the status for the Tenant Control Plane Deployment in the management cluster.
properties:
availableReplicas:
description: Total number of available pods (ready for at least minReadySeconds) targeted by this deployment.
format: int32
type: integer
collisionCount:
description: Count of hash collisions for the Deployment. The Deployment controller uses this field as a collision avoidance mechanism when it needs to create the name for the newest ReplicaSet.
format: int32
type: integer
conditions:
description: Represents the latest available observations of a deployment's current state.
items:
description: DeploymentCondition describes the state of a deployment at a certain point.
properties:
lastTransitionTime:
description: Last time the condition transitioned from one status to another.
format: date-time
type: string
lastUpdateTime:
description: The last time this condition was updated.
format: date-time
type: string
message:
description: A human readable message indicating details about the transition.
type: string
reason:
description: The reason for the condition's last transition.
type: string
status:
description: Status of the condition, one of True, False, Unknown.
type: string
type:
description: Type of deployment condition.
type: string
required:
- status
- type
type: object
type: array
name:
description: The name of the Deployment for the given cluster.
type: string
namespace:
description: The namespace which the Deployment for the given cluster is deployed.
type: string
observedGeneration:
description: The generation observed by the deployment controller.
format: int64
type: integer
readyReplicas:
description: readyReplicas is the number of pods targeted by this Deployment with a Ready Condition.
format: int32
type: integer
replicas:
description: Total number of non-terminated pods targeted by this deployment (their labels match the selector).
format: int32
type: integer
unavailableReplicas:
description: Total number of unavailable pods targeted by this deployment. This is the total number of pods that are still required for the deployment to have 100% available capacity. They may either be pods that are running but not yet available or pods that still have not been created.
format: int32
type: integer
updatedReplicas:
description: Total number of non-terminated pods targeted by this deployment that have the desired template spec.
format: int32
type: integer
required:
- name
- namespace
type: object
ingress:
description: KubernetesIngressStatus defines the status for the Tenant Control Plane Ingress in the management cluster.
properties:
loadBalancer:
description: LoadBalancer contains the current status of the load-balancer.
properties:
ingress:
description: Ingress is a list containing ingress points for the load-balancer. Traffic intended for the service should be sent to these ingress points.
items:
description: 'LoadBalancerIngress represents the status of a load-balancer ingress point: traffic intended for the service should be sent to an ingress point.'
properties:
hostname:
description: Hostname is set for load-balancer ingress points that are DNS based (typically AWS load-balancers)
type: string
ip:
description: IP is set for load-balancer ingress points that are IP based (typically GCE or OpenStack load-balancers)
type: string
ports:
description: Ports is a list of records of service ports If used, every port defined in the service should have an entry in it
items:
properties:
error:
description: 'Error is to record the problem with the service port The format of the error shall comply with the following rules: - built-in error values shall be specified in this file and those shall use CamelCase names - cloud provider specific error values must have names that comply with the format foo.example.com/CamelCase. --- The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)'
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
port:
description: Port is the port number of the service port of which status is recorded here
format: int32
type: integer
protocol:
default: TCP
description: 'Protocol is the protocol of the service port of which status is recorded here The supported values are: "TCP", "UDP", "SCTP"'
type: string
required:
- port
- protocol
type: object
type: array
x-kubernetes-list-type: atomic
type: object
type: array
type: object
name:
description: The name of the Ingress for the given cluster.
type: string
namespace:
description: The namespace which the Ingress for the given cluster is deployed.
type: string
required:
- name
- namespace
type: object
service:
description: KubernetesServiceStatus defines the status for the Tenant Control Plane Service in the management cluster.
properties:
conditions:
description: Current service state
items:
description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties:
lastTransitionTime:
description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: message is a human readable message indicating details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
loadBalancer:
description: LoadBalancer contains the current status of the load-balancer, if one is present.
properties:
ingress:
description: Ingress is a list containing ingress points for the load-balancer. Traffic intended for the service should be sent to these ingress points.
items:
description: 'LoadBalancerIngress represents the status of a load-balancer ingress point: traffic intended for the service should be sent to an ingress point.'
properties:
hostname:
description: Hostname is set for load-balancer ingress points that are DNS based (typically AWS load-balancers)
type: string
ip:
description: IP is set for load-balancer ingress points that are IP based (typically GCE or OpenStack load-balancers)
type: string
ports:
description: Ports is a list of records of service ports If used, every port defined in the service should have an entry in it
items:
properties:
error:
description: 'Error is to record the problem with the service port The format of the error shall comply with the following rules: - built-in error values shall be specified in this file and those shall use CamelCase names - cloud provider specific error values must have names that comply with the format foo.example.com/CamelCase. --- The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)'
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
port:
description: Port is the port number of the service port of which status is recorded here
format: int32
type: integer
protocol:
default: TCP
description: 'Protocol is the protocol of the service port of which status is recorded here The supported values are: "TCP", "UDP", "SCTP"'
type: string
required:
- port
- protocol
type: object
type: array
x-kubernetes-list-type: atomic
type: object
type: array
type: object
name:
description: The name of the Service for the given cluster.
type: string
namespace:
description: The namespace which the Service for the given cluster is deployed.
type: string
port:
description: The port where the service is running
format: int32
type: integer
required:
- name
- namespace
- port
type: object
type: object
storage:
description: Storage Status contains information about Kubernetes storage system
properties:
etcd:
description: ETCDStatus defines the observed state of ETCDStatus
properties:
role:
properties:
exists:
type: boolean
name:
type: string
permissions:
items:
properties:
key:
type: string
rangeEnd:
type: string
type:
type: integer
type: object
type: array
required:
- exists
- name
type: object
user:
properties:
exists:
type: boolean
name:
type: string
roles:
items:
type: string
type: array
required:
- exists
- name
type: object
type: object
type: object
type: object
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@@ -0,0 +1,22 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "kamaji.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "kamaji.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "kamaji.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "kamaji.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View File

@@ -0,0 +1,63 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "kamaji.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "kamaji.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "kamaji.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "kamaji.labels" -}}
helm.sh/chart: {{ include "kamaji.chart" . }}
{{ include "kamaji.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "kamaji.selectorLabels" -}}
app.kubernetes.io/name: {{ include "kamaji.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: controller-manager
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "kamaji.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "kamaji.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,92 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "kamaji.fullname" . }}
labels:
{{- include "kamaji.labels" . | nindent 4 }}
namespace: {{ .Release.Namespace }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "kamaji.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "kamaji.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
serviceAccountName: {{ include "kamaji.serviceAccountName" . }}
containers:
- args:
- --secure-listen-address=0.0.0.0:8443
- --upstream=http://127.0.0.1:8080/
- --logtostderr=true
- --v=10
image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0
name: kube-rbac-proxy
ports:
- containerPort: 8443
name: https
protocol: TCP
- args:
- --config-file={{ .Values.configPath }}
- --etcd-ca-secret-name={{ .Values.etcd.caSecret.name }}
- --etcd-ca-secret-namespace={{ .Values.etcd.caSecret.namespace }}
- --etcd-client-secret-name={{ .Values.etcd.clientSecret.name }}
- --etcd-client-secret-namespace={{ .Values.etcd.clientSecret.namespace }}
- --etcd-compaction-interval={{ .Values.etcd.compactionInterval }}
- --etcd-endpoints={{ .Values.etcd.endpoints }}
- --health-probe-bind-address={{ .Values.healthProbeBindAddress }}
- --leader-elect
- --metrics-bind-address={{ .Values.metricsBindAddress }}
- --tmp-directory={{ .Values.temporaryDirectoryPath }}
{{- if .Values.loggingDevel.enable }}
- --zap-devel
{{- end }}
{{- with .Values.extraArgs }}
{{- toYaml . | nindent 8 }}
{{- end }}
command:
- /manager
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- with .Values.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 10 }}
{{- end }}
name: manager
ports:
- containerPort: 8081
name: healthcheck
protocol: TCP
{{- with .Values.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 10 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
terminationGracePeriodSeconds: 10
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,61 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "kamaji.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "kamaji.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- else }}
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,210 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "kamaji.serviceAccountName" . }}
labels:
{{- include "kamaji.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
namespace: {{ .Release.Namespace }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: kamaji-leader-election-role
namespace: {{ .Release.Namespace }}
rules:
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
creationTimestamp: null
name: kamaji-manager-role
rules:
- apiGroups:
- apps
resources:
- deployments
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ""
resources:
- configmaps
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ""
resources:
- secrets
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ""
resources:
- services
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- kamaji.clastix.io
resources:
- tenantcontrolplanes
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- kamaji.clastix.io
resources:
- tenantcontrolplanes/finalizers
verbs:
- update
- apiGroups:
- kamaji.clastix.io
resources:
- tenantcontrolplanes/status
verbs:
- get
- patch
- update
- apiGroups:
- networking.k8s.io
resources:
- ingresses
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kamaji-metrics-reader
rules:
- nonResourceURLs:
- /metrics
verbs:
- get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kamaji-proxy-role
rules:
- apiGroups:
- authentication.k8s.io
resources:
- tokenreviews
verbs:
- create
- apiGroups:
- authorization.k8s.io
resources:
- subjectaccessreviews
verbs:
- create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: kamaji-leader-election-rolebinding
namespace: {{ .Release.Namespace }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: kamaji-leader-election-role
subjects:
- kind: ServiceAccount
name: {{ include "kamaji.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kamaji-manager-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kamaji-manager-role
subjects:
- kind: ServiceAccount
name: {{ include "kamaji.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kamaji-proxy-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kamaji-proxy-role
subjects:
- kind: ServiceAccount
name: {{ include "kamaji.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}

View File

@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "kamaji.fullname" . }}
labels:
{{- include "kamaji.labels" . | nindent 4 }}
namespace: {{ .Release.Namespace }}
spec:
type: {{ .Values.service.type }}
ports:
- name: https
port: {{ .Values.service.port }}
protocol: TCP
targetPort: https
selector:
{{- include "kamaji.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,2 @@
etcd:
endpoints: "etcd-0.etcd.kamaji-system.svc.cluster.local:2379,etcd-1.etcd.kamaji-system.svc.cluster.local:2379,etcd-2.etcd.kamaji-system.svc.cluster.local:2379"

136
helm/kamaji/values.yaml Normal file
View File

@@ -0,0 +1,136 @@
# Default values for kamaji.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# -- The number of the pod replicas for the Kamaji controller.
replicaCount: 1
image:
# -- The container image of the Kamaji controller.
repository: quay.io/clastix/kamaji
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: latest
# -- A list of extra arguments to add to the kamaji controller default ones
extraArgs: []
# -- Configuration file path alternative. (default "./kamaji.yaml")
configPath: "./kamaji.yaml"
etcd:
caSecret:
# -- Name of the secret which contains CA's certificate and private key. (default: "etcd-certs")
name: etcd-certs
# -- Namespace of the secret which contains CA's certificate and private key. (default: "kamaji")
namespace: kamaji-system
clientSecret:
# -- Name of the secret which contains ETCD client certificates. (default: "root-client-certs")
name: root-client-certs
# -- Name of the namespace where the secret which contains ETCD client certificates is. (default: "kamaji")
namespace: kamaji-system
# -- ETCD Compaction interval (e.g. "5m0s"). (default: "0" (disabled))
compactionInterval: 0
# -- (string) Comma-separated list of the endpoints of the etcd cluster's members.
endpoints: "etcd-0.etcd.kamaji-system.svc.cluster.local:2379,etcd-1.etcd.kamaji-system.svc.cluster.local:2379,etcd-2.etcd.kamaji-system.svc.cluster.local:2379"
# -- The address the probe endpoint binds to. (default ":8081")
healthProbeBindAddress: ":8081"
# -- The livenessProbe for the controller container
livenessProbe:
httpGet:
path: /healthz
port: healthcheck
initialDelaySeconds: 15
periodSeconds: 20
# -- The readinessProbe for the controller container
readinessProbe:
httpGet:
path: /readyz
port: healthcheck
initialDelaySeconds: 5
periodSeconds: 10
# -- (string) The address the metric endpoint binds to. (default ":8080")
metricsBindAddress: ":8080"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: kamaji-controller-manager
# -- The annotations to apply to the Kamaji controller pods.
podAnnotations: {}
# -- The securityContext to apply to the Kamaji controller pods.
podSecurityContext:
runAsNonRoot: true
# -- The securityContext to apply to the Kamaji controller container only. It does not apply to the Kamaji RBAC proxy container.
securityContext:
allowPrivilegeEscalation: false
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: ClusterIP
port: 8443
ingress:
# -- Whether to expose the Kamaji controller through an Ingress.
enabled: false
# -- Name of the ingress class to route through this controller.
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources:
limits:
cpu: 200m
memory: 100Mi
requests:
cpu: 100m
memory: 20Mi
# -- Kubernetes node selector rules to schedule Kamaji controller
nodeSelector: {}
# -- Kubernetes node taints that the Kamaji controller pods would tolerate
tolerations: []
# -- Kubernetes affinity rules to apply to Kamaji controller pods
affinity: {}
# -- Directory which will be used to work with temporary files. (default "/tmp/kamaji")
temporaryDirectoryPath: "/tmp/kamaji"
loggingDevel:
# -- (string) Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error) (default false)
enable: false

123
internal/config/config.go Normal file
View File

@@ -0,0 +1,123 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package config
import (
"flag"
"fmt"
"log"
"github.com/pkg/errors"
"github.com/spf13/pflag"
"github.com/spf13/viper"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
)
var (
config *viper.Viper
cfgFile string
)
const (
envPrefix = "KAMAJI"
defaultETCDCASecretName = "etcd-certs"
defaultETCDCASecretNamespace = "kamaji-system"
defaultETCDEndpoints = "etcd-server:2379"
defaultETCDCompactionInterval = "0"
defaultETCDClientSecretName = "root-client-certs"
defaultETCDClientSecretNamespace = "kamaji-system"
defaultTmpDirectory = "/tmp/kamaji"
)
func InitConfig() (*viper.Viper, error) {
config = viper.New()
// Setup flags with standard "flag" module
flag.StringVar(&cfgFile, "config-file", "kamaji.yaml", "Configuration file alternative.")
flag.String("metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
flag.String("health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.Bool("leader-elect", false, "Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
flag.String("etcd-ca-secret-name", defaultETCDCASecretName, "Name of the secret which contains CA's certificate and private key.")
flag.String("etcd-ca-secret-namespace", defaultETCDCASecretNamespace, "Namespace of the secret which contains CA's certificate and private key.")
flag.String("etcd-client-secret-name", defaultETCDClientSecretName, "Name of the secret which contains ETCD client certificates")
flag.String("etcd-client-secret-namespace", defaultETCDClientSecretNamespace, "Name of the namespace where the secret which contains ETCD client certificates is")
flag.String("etcd-endpoints", defaultETCDEndpoints, "Comma-separated list with ETCD endpoints (i.e. etcd-0.etcd.kamaji-system.svc.cluster.local,etcd-1.etcd.kamaji-system.svc.cluster.local,etcd-2.etcd.kamaji-system.svc.cluster.local)")
flag.String("etcd-compaction-interval", defaultETCDCompactionInterval, "ETCD Compaction interval (i.e. \"5m0s\"). (default: \"0\" (disabled))")
flag.String("tmp-directory", defaultTmpDirectory, "Directory which will be used to work with temporary files.")
// Setup zap configuration
opts := zap.Options{
Development: true,
}
opts.BindFlags(flag.CommandLine)
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
// Add flag set to pflag in order to parse with viper
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.Parse()
if err := config.BindPFlags(pflag.CommandLine); err != nil {
log.Printf("error binding flags: %s", err)
return nil, err
}
// Setup environment variables
if err := config.BindEnv("metrics-bind-address", fmt.Sprintf("%s_METRICS_BIND_ADDRESS", envPrefix)); err != nil {
return nil, err
}
if err := config.BindEnv("health-probe-bind-address", fmt.Sprintf("%s_HEALTH_PROBE_BIND_ADDRESS", envPrefix)); err != nil {
return nil, err
}
if err := config.BindEnv("leader-elect", fmt.Sprintf("%s_LEADER_ELECTION", envPrefix)); err != nil {
return nil, err
}
if err := config.BindEnv("etcd-ca-secret-name", fmt.Sprintf("%s_ETCD_CA_SECRET_NAME", envPrefix)); err != nil {
return nil, err
}
if err := config.BindEnv("etcd-ca-secret-namespace", fmt.Sprintf("%s_ETCD_CA_SECRET_NAMESPACE", envPrefix)); err != nil {
return nil, err
}
if err := config.BindEnv("etcd-client-secret-name", fmt.Sprintf("%s_ETCD_CLIENT_SECRET_NAME", envPrefix)); err != nil {
return nil, err
}
if err := config.BindEnv("etcd-client-secret-namespace", fmt.Sprintf("%s_ETCD_CLIENT_SECRET_NAMESPACE", envPrefix)); err != nil {
return nil, err
}
if err := config.BindEnv("etcd-endpoints", fmt.Sprintf("%s_ETCD_ENDPOINTS", envPrefix)); err != nil {
return nil, err
}
if err := config.BindEnv("etcd-compaction-interval", fmt.Sprintf("%s_ETCD_COMPACTION_INTERVAL", envPrefix)); err != nil {
return nil, err
}
if err := config.BindEnv("tmp-directory", fmt.Sprintf("%s_TMP_DIRECTORY", envPrefix)); err != nil {
return nil, err
}
// Setup config file
if cfgFile != "" {
// Using flag-passed config file
config.SetConfigFile(cfgFile)
} else {
// Using default config file
config.AddConfigPath(".")
config.SetConfigName("kamaji")
}
config.SetConfigType("yaml")
if err := config.ReadInConfig(); err != nil {
if errors.Is(err, &viper.ConfigParseError{}) {
log.Printf("error parsing config file: %v", err)
return nil, err
}
log.Println("No config file used")
return nil, err
}
log.Printf("Using config file: %v", viper.ConfigFileUsed())
return config, nil
}

View File

@@ -0,0 +1,9 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package constants
const (
ProjectNameLabelKey = "kamaji.clastix.io/project"
ProjectNameLabelValue = "kamaji"
)

125
internal/crypto/crypto.go Normal file
View File

@@ -0,0 +1,125 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package crypto
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"time"
)
func GetCertificate(cert []byte) (*x509.Certificate, error) {
pemContent, _ := pem.Decode(cert)
if pemContent == nil {
return nil, fmt.Errorf("no right PEM block")
}
return x509.ParseCertificate(pemContent.Bytes)
}
func GetPrivateKey(privKey []byte) (*rsa.PrivateKey, error) {
pemContent, _ := pem.Decode(privKey)
if pemContent == nil {
return nil, fmt.Errorf("no right PEM block")
}
return x509.ParsePKCS1PrivateKey(pemContent.Bytes)
}
func GetPublickKey(pubKey []byte) (*rsa.PublicKey, error) {
pemContent, _ := pem.Decode(pubKey)
if pemContent == nil {
return nil, fmt.Errorf("no right PEM block")
}
pub, err := x509.ParsePKIXPublicKey(pemContent.Bytes)
if err != nil {
return nil, err
}
return pub.(*rsa.PublicKey), nil // nolint:forcetypeassert
}
func GenerateCertificateKeyPairBytes(template *x509.Certificate, bitSize int, caCert *x509.Certificate, caKey *rsa.PrivateKey) (*bytes.Buffer, *bytes.Buffer, error) {
certPrivKey, err := rsa.GenerateKey(rand.Reader, bitSize)
if err != nil {
return nil, nil, err
}
certBytes, err := x509.CreateCertificate(rand.Reader, template, caCert, &certPrivKey.PublicKey, caKey)
if err != nil {
return nil, nil, err
}
certPEM := &bytes.Buffer{}
if err := pem.Encode(certPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: certBytes,
}); err != nil {
return nil, nil, err
}
certPrivKeyPEM := &bytes.Buffer{}
if err := pem.Encode(certPrivKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
}); err != nil {
return nil, nil, err
}
return certPEM, certPrivKeyPEM, nil
}
func IsValidKeyPairBytes(pubKeyBytes []byte, privKeyBytes []byte) (bool, error) {
privKey, err := GetPrivateKey(privKeyBytes)
if err != nil {
return false, err
}
pubKey, err := GetPublickKey(pubKeyBytes)
if err != nil {
return false, err
}
return checkPublicKeys(privKey.PublicKey, *pubKey), nil
}
func IsValidCertificateKeyPairBytes(certBytes []byte, privKeyBytes []byte) (bool, error) {
cert, err := GetCertificate(certBytes)
if err != nil {
return false, err
}
privKey, err := GetPrivateKey(privKeyBytes)
if err != nil {
return false, err
}
return isValidCertificateKeyPairBytes(*cert, *privKey), nil
}
func isValidCertificateKeyPairBytes(cert x509.Certificate, privKey rsa.PrivateKey) bool {
return checkCertificateValidity(cert) && checkCertificateKeyPair(cert, privKey)
}
func checkCertificateValidity(cert x509.Certificate) bool {
now := time.Now()
return now.Before(cert.NotAfter) && now.After(cert.NotBefore)
}
func checkCertificateKeyPair(cert x509.Certificate, privKey rsa.PrivateKey) bool {
return checkPublicKeys(*cert.PublicKey.(*rsa.PublicKey), privKey.PublicKey) // nolint:forcetypeassert
}
func checkPublicKeys(a rsa.PublicKey, b rsa.PublicKey) bool {
isN := a.N.Cmp(b.N) == 0
isE := a.E == b.E
return isN && isE
}

177
internal/etcd/api.go Normal file
View File

@@ -0,0 +1,177 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package etcd
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"time"
"go.etcd.io/etcd/api/v3/authpb"
"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
etcdclient "go.etcd.io/etcd/client/v3"
"google.golang.org/grpc/codes"
)
const (
etcdTimeout = 10 // seconds
// rangeEnd is the key following the last key of the range.
// If rangeEnd is \0, the range is all keys greater than or equal to the key argument
// source: https://etcd.io/docs/v3.5/learning/api/
rangeEnd = "\\0"
)
func NewClient(config Config) (*etcdclient.Client, error) {
cert, err := tls.X509KeyPair(config.ETCDCertificate, config.ETCDPrivateKey)
if err != nil {
return nil, err
}
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(config.ETCDCA)
cfg := etcdclient.Config{
Endpoints: config.Endpoints,
TLS: &tls.Config{ // nolint:gosec
Certificates: []tls.Certificate{cert},
RootCAs: pool,
},
}
return etcdclient.New(cfg)
}
func GetUser(ctx context.Context, client *etcdclient.Client, user *User) error {
ctxWithTimeout, cancel := context.WithTimeout(ctx, etcdTimeout*time.Second)
defer cancel()
response, err := client.UserGet(ctxWithTimeout, user.Name)
if err != nil {
var etcdError rpctypes.EtcdError
if errors.As(err, &etcdError) && etcdError.Code() == codes.FailedPrecondition {
return nil
}
return err
}
user.Roles = response.Roles
user.Exists = true
return nil
}
func AddUser(ctx context.Context, client *etcdclient.Client, username string) error {
ctxWithTimeout, cancel := getContextWithTimeout(ctx)
defer cancel()
opts := etcdclient.UserAddOptions{
NoPassword: true,
}
_, err := client.Auth.UserAddWithOptions(ctxWithTimeout, username, "", &opts)
return err
}
func RemoveUser(ctx context.Context, client *etcdclient.Client, username string) error {
ctxWithTimeout, cancel := getContextWithTimeout(ctx)
defer cancel()
_, err := client.Auth.UserDelete(ctxWithTimeout, username)
return err
}
func GetRole(ctx context.Context, client *etcdclient.Client, role *Role) error {
ctxWithTimeout, cancel := getContextWithTimeout(ctx)
defer cancel()
response, err := client.RoleGet(ctxWithTimeout, role.Name)
if err != nil {
var etcdError rpctypes.EtcdError
if errors.As(err, &etcdError) && etcdError.Code() == codes.FailedPrecondition {
return nil
}
return err
}
role.Exists = true
for _, perm := range response.Perm {
permission := Permission{
Type: int(perm.PermType),
Key: string(perm.Key),
RangeEnd: string(perm.RangeEnd),
}
role.Permissions = append(role.Permissions, permission)
}
return nil
}
func AddRole(ctx context.Context, client *etcdclient.Client, roleName string) error {
ctxWithTimeout, cancel := getContextWithTimeout(ctx)
defer cancel()
_, err := client.Auth.RoleAdd(ctxWithTimeout, roleName)
return err
}
func RemoveRole(ctx context.Context, client *etcdclient.Client, roleName string) error {
ctxWithTimeout, cancel := getContextWithTimeout(ctx)
defer cancel()
_, err := client.Auth.RoleDelete(ctxWithTimeout, roleName)
return err
}
func GrantUserRole(ctx context.Context, client *etcdclient.Client, user User, role Role) error {
ctxWithTimeout, cancel := getContextWithTimeout(ctx)
defer cancel()
_, err := client.UserGrantRole(ctxWithTimeout, user.Name, role.Name)
if err != nil {
return err
}
return nil
}
func GrantRolePermission(ctx context.Context, client *etcdclient.Client, role Role) error {
ctxWithTimeout, cancel := getContextWithTimeout(ctx)
defer cancel()
permission := etcdclient.PermissionType(authpb.READWRITE)
key := BuildKey(role.Name)
_, err := client.RoleGrantPermission(ctxWithTimeout, role.Name, key, rangeEnd, permission)
if err != nil {
return err
}
return nil
}
func CleanUpPrefix(ctx context.Context, client *etcdclient.Client, name string) error {
ctxWithTimeout, cancel := getContextWithTimeout(ctx)
defer cancel()
withRange := etcdclient.WithRange(rangeEnd)
prefix := BuildKey(name)
_, err := client.Delete(ctxWithTimeout, prefix, withRange)
return err
}
func BuildKey(roleName string) string {
return fmt.Sprintf("/%s/", roleName)
}
func getContextWithTimeout(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, etcdTimeout*time.Second)
}

57
internal/etcd/certs.go Normal file
View File

@@ -0,0 +1,57 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package etcd
import (
"bytes"
"crypto/x509"
"crypto/x509/pkix"
"math/big"
"math/rand"
"time"
"github.com/clastix/kamaji/internal/crypto"
)
func GetETCDCACertificateAndKeyPair(tenant string, caCert []byte, caPrivKey []byte) (*bytes.Buffer, *bytes.Buffer, error) {
template := getCertTemplate(tenant)
caCertBytes, err := crypto.GetCertificate(caCert)
if err != nil {
return nil, nil, err
}
caPrivKeyBytes, err := crypto.GetPrivateKey(caPrivKey)
if err != nil {
return nil, nil, err
}
return crypto.GenerateCertificateKeyPairBytes(template, certBitSize, caCertBytes, caPrivKeyBytes)
}
func IsETCDCertificateAndKeyPairValid(cert []byte, privKey []byte) (bool, error) {
return crypto.IsValidCertificateKeyPairBytes(cert, privKey)
}
func getCertTemplate(tenant string) *x509.Certificate {
serialNumber := big.NewInt(rand.Int63())
return &x509.Certificate{
PublicKeyAlgorithm: x509.RSA,
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: tenant,
Organization: []string{certOrganization},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(certExpirationDelayYears, 0, 0),
SubjectKeyId: []byte{1, 2, 3, 4, 6},
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageClientAuth,
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageCodeSigning,
},
KeyUsage: x509.KeyUsageDigitalSignature,
}
}

10
internal/etcd/constant.go Normal file
View File

@@ -0,0 +1,10 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package etcd
const (
certExpirationDelayYears = 10
certOrganization = "system:masters"
certBitSize = 2048
)

71
internal/etcd/types.go Normal file
View File

@@ -0,0 +1,71 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package etcd
type Config struct {
ETCDCertificate []byte
ETCDPrivateKey []byte
ETCDCA []byte
Endpoints []string
}
type Permission struct {
Type int `json:"type,omitempty"`
Key string `json:"key,omitempty"`
RangeEnd string `json:"rangeEnd,omitempty"`
}
func (in *Permission) DeepCopyInto(out *Permission) {
*out = *in
}
func (in *Permission) DeepCopy() *Permission {
if in == nil {
return nil
}
out := new(Permission)
in.DeepCopyInto(out)
return out
}
type Role struct {
Name string `json:"name"`
Permissions []Permission `json:"permissions,omitempty"`
Exists bool `json:"exists"`
}
func (in *Role) DeepCopyInto(out *Role) {
*out = *in
}
func (in *Role) DeepCopy() *Role {
if in == nil {
return nil
}
out := new(Role)
in.DeepCopyInto(out)
return out
}
type User struct {
Name string `json:"name"`
Roles []string `json:"roles,omitempty"`
Exists bool `json:"exists"`
}
func (in *User) DeepCopyInto(out *User) {
*out = *in
}
func (in *User) DeepCopy() *User {
if in == nil {
return nil
}
out := new(User)
in.DeepCopyInto(out)
return out
}

287
internal/kubeadm/addon.go Normal file
View File

@@ -0,0 +1,287 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package kubeadm
import (
"fmt"
"time"
"github.com/pkg/errors"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api/v1"
"k8s.io/component-base/config/v1alpha1"
kubeproxyconfig "k8s.io/kube-proxy/config/v1alpha1"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/addons/dns"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/addons/proxy"
"k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient"
"k8s.io/utils/pointer"
)
func CoreDNSAddon(client kubernetes.Interface, config *Configuration) error {
return dns.EnsureDNSAddon(&config.InitConfiguration.ClusterConfiguration, client)
}
func KubeProxyAddon(client kubernetes.Interface, config *Configuration) error {
if err := proxy.CreateServiceAccount(client); err != nil {
return errors.Wrap(err, "error when creating kube-proxy service account")
}
if err := createKubeProxyConfigMap(client, config); err != nil {
return err
}
if err := createKubeProxyAddon(client); err != nil {
return err
}
if err := proxy.CreateRBACRules(client); err != nil {
return errors.Wrap(err, "error when creating kube-proxy RBAC rules")
}
return nil
}
func createKubeProxyConfigMap(client kubernetes.Interface, config *Configuration) error {
configConf, err := getKubeproxyConfigmapContent(config)
if err != nil {
return err
}
kubeconfigConf, err := getKubeproxyKubeconfigContent(config)
if err != nil {
return err
}
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: kubeadmconstants.KubeProxyConfigMap,
Namespace: "kube-system",
Labels: map[string]string{
"app": "kube-proxy",
},
},
Data: map[string]string{
kubeadmconstants.KubeProxyConfigMapKey: string(configConf),
"kubeconfig.conf": string(kubeconfigConf),
},
}
return apiclient.CreateOrUpdateConfigMap(client, configMap)
}
func createKubeProxyAddon(client kubernetes.Interface) error {
daemonSet := &appsv1.DaemonSet{
ObjectMeta: metav1.ObjectMeta{
Name: "kube-proxy",
Namespace: "kube-system",
Labels: map[string]string{
"k8s-app": "kube-proxy",
},
},
Spec: appsv1.DaemonSetSpec{
RevisionHistoryLimit: pointer.Int32(10),
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"k8s-app": "kube-proxy",
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"k8s-app": "kube-proxy",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Command: []string{
"/usr/local/bin/kube-proxy",
"--config=/var/lib/kube-proxy/config.conf",
"--hostname-override=$(NODE_NAME)",
},
Env: []corev1.EnvVar{
{
Name: "NODE_NAME",
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
APIVersion: "v1",
FieldPath: "spec.nodeName",
},
},
},
},
Image: "k8s.gcr.io/kube-proxy:v1.21.2",
ImagePullPolicy: corev1.PullIfNotPresent,
Name: "kube-proxy",
SecurityContext: &corev1.SecurityContext{
Privileged: pointer.Bool(true),
},
TerminationMessagePath: "/dev/termination-log",
TerminationMessagePolicy: "File",
VolumeMounts: []corev1.VolumeMount{
{
MountPath: "/var/lib/kube-proxy",
Name: "kube-proxy",
},
{
MountPath: "/run/xtables.lock",
Name: "xtables-lock",
},
{
MountPath: "/lib/modules",
Name: "lib-modules",
ReadOnly: true,
},
},
},
},
DNSPolicy: corev1.DNSClusterFirst,
HostNetwork: true,
NodeSelector: map[string]string{
"kubernetes.io/os": "linux",
},
PriorityClassName: "system-node-critical",
RestartPolicy: corev1.RestartPolicyAlways,
SchedulerName: "default-scheduler",
ServiceAccountName: "kube-proxy",
TerminationGracePeriodSeconds: pointer.Int64(30),
Volumes: []corev1.Volume{
{
Name: "kube-proxy",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
DefaultMode: pointer.Int32(420),
LocalObjectReference: corev1.LocalObjectReference{
Name: "kube-proxy",
},
},
},
},
{
Name: "xtables-lock",
VolumeSource: corev1.VolumeSource{
HostPath: &corev1.HostPathVolumeSource{
Path: "/run/xtables.lock",
Type: (*corev1.HostPathType)(pointer.String(string(corev1.HostPathFileOrCreate))),
},
},
},
{
Name: "lib-modules",
VolumeSource: corev1.VolumeSource{
HostPath: &corev1.HostPathVolumeSource{
Path: "/lib/modules",
Type: (*corev1.HostPathType)(pointer.String(string(corev1.HostPathUnset))),
},
},
},
},
},
},
},
}
return apiclient.CreateOrUpdateDaemonSet(client, daemonSet)
}
func getKubeproxyConfigmapContent(config *Configuration) ([]byte, error) {
zeroDuration := metav1.Duration{Duration: 0}
oneSecondDuration := metav1.Duration{Duration: time.Second}
kubeProxyConfiguration := kubeproxyconfig.KubeProxyConfiguration{
TypeMeta: metav1.TypeMeta{
Kind: "KubeProxyConfiguration",
APIVersion: "kubeproxy.config.k8s.io/v1alpha1",
},
BindAddress: "0.0.0.0",
BindAddressHardFail: false,
ClientConnection: v1alpha1.ClientConnectionConfiguration{
AcceptContentTypes: "",
Burst: 0,
ContentType: "",
Kubeconfig: "/var/lib/kube-proxy/kubeconfig.conf",
QPS: 0,
},
ClusterCIDR: config.Parameters.TenantControlPlanePodCIDR,
ConfigSyncPeriod: zeroDuration,
Conntrack: kubeproxyconfig.KubeProxyConntrackConfiguration{
MaxPerCore: pointer.Int32(0),
Min: nil,
TCPCloseWaitTimeout: nil,
TCPEstablishedTimeout: nil,
},
DetectLocalMode: "",
EnableProfiling: false,
HealthzBindAddress: "",
HostnameOverride: "",
IPTables: kubeproxyconfig.KubeProxyIPTablesConfiguration{
MasqueradeAll: false,
MasqueradeBit: nil,
MinSyncPeriod: oneSecondDuration,
SyncPeriod: zeroDuration,
},
IPVS: kubeproxyconfig.KubeProxyIPVSConfiguration{
ExcludeCIDRs: nil,
MinSyncPeriod: zeroDuration,
Scheduler: "",
StrictARP: false,
SyncPeriod: zeroDuration,
TCPTimeout: zeroDuration,
TCPFinTimeout: zeroDuration,
UDPTimeout: zeroDuration,
},
MetricsBindAddress: "",
Mode: "iptables",
NodePortAddresses: nil,
OOMScoreAdj: nil,
PortRange: "",
ShowHiddenMetricsForVersion: "",
UDPIdleTimeout: zeroDuration,
Winkernel: kubeproxyconfig.KubeProxyWinkernelConfiguration{
EnableDSR: false,
NetworkName: "",
SourceVip: "",
},
}
return EncondeToYaml(&kubeProxyConfiguration)
}
func getKubeproxyKubeconfigContent(config *Configuration) ([]byte, error) {
kubeconfig := clientcmdapi.Config{
APIVersion: "v1",
Kind: "Config",
Clusters: []clientcmdapi.NamedCluster{
{
Name: "default",
Cluster: clientcmdapi.Cluster{
CertificateAuthority: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt",
Server: fmt.Sprintf("https://%s:%d", config.Parameters.TenantControlPlaneAddress, config.Parameters.TenantControlPlanePort),
},
},
},
Contexts: []clientcmdapi.NamedContext{
{
Context: clientcmdapi.Context{
Cluster: "default",
Namespace: "default",
AuthInfo: "default",
},
},
},
AuthInfos: []clientcmdapi.NamedAuthInfo{
{
Name: "default",
AuthInfo: clientcmdapi.AuthInfo{
TokenFile: "/var/run/secrets/kubernetes.io/serviceaccount/token",
},
},
},
}
return EncondeToYaml(&kubeconfig)
}

View File

@@ -0,0 +1,73 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package kubeadm
import (
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/bootstraptoken/clusterinfo"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/bootstraptoken/node"
"k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient"
)
func BootstrapToken(client kubernetes.Interface, config *Configuration) error {
initConfiguration := config.InitConfiguration
if err := node.UpdateOrCreateTokens(client, false, initConfiguration.BootstrapTokens); err != nil {
return errors.Wrap(err, "error updating or creating token")
}
if err := node.AllowBoostrapTokensToGetNodes(client); err != nil {
return errors.Wrap(err, "error allowing bootstrap tokens to get Nodes")
}
if err := node.AllowBootstrapTokensToPostCSRs(client); err != nil {
return errors.Wrap(err, "error allowing bootstrap tokens to post CSRs")
}
if err := node.AutoApproveNodeBootstrapTokens(client); err != nil {
return errors.Wrap(err, "error auto-approving node bootstrap tokens")
}
if err := node.AutoApproveNodeCertificateRotation(client); err != nil {
return err
}
bootstrapConfig := &clientcmdapi.Config{
Clusters: map[string]*clientcmdapi.Cluster{
"": {
Server: config.Kubeconfig.Clusters[0].Cluster.Server,
CertificateAuthorityData: config.Kubeconfig.Clusters[0].Cluster.CertificateAuthorityData,
},
},
}
bootstrapBytes, err := clientcmd.Write(*bootstrapConfig)
if err != nil {
return err
}
err = apiclient.CreateOrUpdateConfigMap(client, &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: bootstrapapi.ConfigMapClusterInfo,
Namespace: metav1.NamespacePublic,
},
Data: map[string]string{
bootstrapapi.KubeConfigKey: string(bootstrapBytes),
},
})
if err != nil {
return err
}
if err := clusterinfo.CreateClusterInfoRBACRules(client); err != nil {
return errors.Wrap(err, "error creating clusterinfo RBAC rules")
}
return nil
}

View File

@@ -0,0 +1,165 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package kubeadm
import (
"crypto"
"crypto/x509"
"fmt"
"io/ioutil"
"os"
"path/filepath"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/certs"
cryptoKamaji "github.com/clastix/kamaji/internal/crypto"
)
func GenerateCACertificatePrivateKeyPair(baseName string, config *Configuration) (*CertificatePrivateKeyPair, error) {
defer deleteCertificateDirectory(config.InitConfiguration.CertificatesDir)
kubeadmCert, err := getKubeadmCert(baseName)
if err != nil {
return nil, err
}
if _, _, err = initPhaseAsCA(kubeadmCert, config); err != nil {
return nil, err
}
contents, err := readCertificateFiles(baseName, config.InitConfiguration.CertificatesDir, "crt", "key")
if err != nil {
return nil, err
}
certificatePrivateKeyPair := &CertificatePrivateKeyPair{
Certificate: contents[0],
PrivateKey: contents[1],
}
return certificatePrivateKeyPair, err
}
func GenerateCertificatePrivateKeyPair(baseName string, config *Configuration, ca CertificatePrivateKeyPair) (*CertificatePrivateKeyPair, error) {
defer deleteCertificateDirectory(config.InitConfiguration.CertificatesDir)
certificate, _ := cryptoKamaji.GetCertificate(ca.Certificate)
signer, _ := cryptoKamaji.GetPrivateKey(ca.PrivateKey)
kubeadmCert, err := getKubeadmCert(baseName)
if err != nil {
return nil, err
}
if err = initPhaseFromCA(kubeadmCert, config, certificate, signer); err != nil {
return nil, err
}
contents, err := readCertificateFiles(baseName, config.InitConfiguration.CertificatesDir, "crt", "key")
if err != nil {
return nil, err
}
certificatePrivateKeyPair := &CertificatePrivateKeyPair{
Certificate: contents[0],
PrivateKey: contents[1],
}
return certificatePrivateKeyPair, err
}
func getKubeadmCert(baseName string) (*certs.KubeadmCert, error) {
switch baseName {
case kubeadmconstants.CACertAndKeyBaseName:
return certs.KubeadmCertRootCA(), nil
case kubeadmconstants.APIServerCertAndKeyBaseName:
return certs.KubeadmCertAPIServer(), nil
case kubeadmconstants.APIServerKubeletClientCertAndKeyBaseName:
return certs.KubeadmCertKubeletClient(), nil
case kubeadmconstants.FrontProxyCACertAndKeyBaseName:
return certs.KubeadmCertFrontProxyCA(), nil
case kubeadmconstants.FrontProxyClientCertAndKeyBaseName:
return certs.KubeadmCertFrontProxyClient(), nil
default:
return nil, fmt.Errorf("wrong ca file name %s", baseName)
}
}
func GeneratePublicKeyPrivateKeyPair(baseName string, config *Configuration) (*PublicKeyPrivateKeyPair, error) {
defer deleteCertificateDirectory(config.InitConfiguration.CertificatesDir)
if err := initPhaseCertsSA(config); err != nil {
return nil, err
}
contents, err := readCertificateFiles(baseName, config.InitConfiguration.CertificatesDir, "pub", "key")
if err != nil {
return nil, err
}
publicKeyPrivateKeyPair := &PublicKeyPrivateKeyPair{
PublicKey: contents[0],
PrivateKey: contents[1],
}
return publicKeyPrivateKeyPair, err
}
func IsCertificatePrivateKeyPairValid(certificate []byte, privKey []byte) (bool, error) {
if len(certificate) == 0 {
return false, nil
}
if len(privKey) == 0 {
return false, nil
}
return cryptoKamaji.IsValidCertificateKeyPairBytes(certificate, privKey)
}
func IsPublicKeyPrivateKeyPairValid(pubKey []byte, privKey []byte) (bool, error) {
if len(pubKey) == 0 {
return false, nil
}
if len(privKey) == 0 {
return false, nil
}
return cryptoKamaji.IsValidKeyPairBytes(pubKey, privKey)
}
func initPhaseCertsSA(config *Configuration) error {
return certs.CreateServiceAccountKeyAndPublicKeyFiles(config.InitConfiguration.CertificatesDir, config.InitConfiguration.PublicKeyAlgorithm())
}
func initPhaseFromCA(kubeadmCert *certs.KubeadmCert, config *Configuration, certificate *x509.Certificate, signer crypto.Signer) error {
return kubeadmCert.CreateFromCA(&config.InitConfiguration, certificate, signer)
}
func initPhaseAsCA(kubeadmCert *certs.KubeadmCert, config *Configuration) (*x509.Certificate, crypto.Signer, error) {
return kubeadmCert.CreateAsCA(&config.InitConfiguration)
}
func readCertificateFiles(name string, directory string, extensions ...string) ([][]byte, error) {
result := make([][]byte, 0, len(extensions))
for _, extension := range extensions {
fileName := fmt.Sprintf("%s.%s", name, extension)
path := filepath.Join(directory, fileName)
content, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
result = append(result, content)
}
return result, nil
}
func deleteCertificateDirectory(certificateDirectory string) {
if err := os.RemoveAll(certificateDirectory); err != nil {
// TODO(prometherion): we should log rather than printing to stdout
fmt.Printf("Error removing %s: %s", certificateDirectory, err.Error()) // nolint:forbidigo
}
}

View File

@@ -0,0 +1,142 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package kubeadm
import (
"encoding/json"
"fmt"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
bootstraptokenv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
)
func CreateKubeadmInitConfiguration(params Parameters) Configuration {
config := kubeadmapi.InitConfiguration{
ClusterConfiguration: getKubeadmClusterConfiguration(params),
BootstrapTokens: []bootstraptokenv1.BootstrapToken{
{
Groups: []string{"system:bootstrappers:kubeadm:default-node-token"},
TTL: &metav1.Duration{Duration: 48 * time.Hour},
Usages: []string{
"signing",
"authentication",
},
},
},
LocalAPIEndpoint: kubeadmapi.APIEndpoint{
AdvertiseAddress: params.TenantControlPlaneAddress,
BindPort: params.TenantControlPlanePort,
},
NodeRegistration: kubeadmapi.NodeRegistrationOptions{
CRISocket: "unix:///run/containerd/containerd.sock",
Name: params.TenantControlPlaneName,
},
}
return Configuration{InitConfiguration: config}
}
func getKubeadmClusterConfiguration(params Parameters) kubeadmapi.ClusterConfiguration {
return kubeadmapi.ClusterConfiguration{
KubernetesVersion: params.TenantControlPlaneVersion,
ClusterName: params.TenantControlPlaneName,
CertificatesDir: "/etc/kubernetes/pki",
ImageRepository: "k8s.gcr.io",
Networking: kubeadmapi.Networking{
DNSDomain: "cluster.local",
PodSubnet: params.TenantControlPlanePodCIDR,
ServiceSubnet: params.TenantControlPlaneServiceCIDR,
},
DNS: kubeadmapi.DNS{
Type: "CoreDNS",
},
ControlPlaneEndpoint: params.TenantControlPlaneEndpoint,
Etcd: kubeadmapi.Etcd{
External: &kubeadmapi.ExternalEtcd{
Endpoints: formatETCDEndpoints(params.ETCDs),
CAFile: "/etc/kubernetes/pki/etcd/ca.crt",
CertFile: "/etc/kubernetes/pki/apiserver-etcd-client.crt",
KeyFile: "/etc/kubernetes/pki/apiserver-etcd-client.key",
},
},
APIServer: kubeadmapi.APIServer{
CertSANs: []string{
"127.0.0.1",
"localhost",
fmt.Sprintf("%s.%s", params.TenantControlPlaneName, params.TenantControlPlaneDomain),
params.TenantControlPlaneName,
fmt.Sprintf("%s.%s.svc", params.TenantControlPlaneName, params.TenantControlPlaneNamespace),
fmt.Sprintf("%s.%s.svc.cluster.local", params.TenantControlPlaneName, params.TenantControlPlaneNamespace),
params.TenantControlPlaneAddress,
},
ControlPlaneComponent: kubeadmapi.ControlPlaneComponent{
ExtraArgs: map[string]string{
"etcd-compaction-interval": params.ETCDCompactionInterval,
"etcd-prefix": fmt.Sprintf("/%s", params.TenantControlPlaneName),
},
},
},
}
}
func GetKubeadmInitConfigurationMap(config Configuration) (map[string]string, error) {
initConfigurationString, err := getJSONStringFromStruct(config.InitConfiguration)
if err != nil {
return map[string]string{}, err
}
clusterConfigurationString, err := getJSONStringFromStruct(config.InitConfiguration.ClusterConfiguration)
if err != nil {
return map[string]string{}, err
}
return map[string]string{
kubeadmconstants.InitConfigurationKind: initConfigurationString,
kubeadmconstants.ClusterConfigurationKind: clusterConfigurationString,
}, nil
}
func GetKubeadmInitConfigurationFromMap(config map[string]string) (*Configuration, error) {
initConfigurationString, ok := config[kubeadmconstants.InitConfigurationKind]
if !ok {
return nil, fmt.Errorf("%s is not in the map", kubeadmconstants.InitConfigurationKind)
}
clusterConfigurationString, ok := config[kubeadmconstants.ClusterConfigurationKind]
if !ok {
return nil, fmt.Errorf("%s is not in the map", kubeadmconstants.ClusterConfigurationKind)
}
initConfiguration := kubeadmapi.InitConfiguration{}
if err := json.Unmarshal([]byte(initConfigurationString), &initConfiguration); err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(clusterConfigurationString), &initConfiguration.ClusterConfiguration); err != nil {
return nil, err
}
return &Configuration{InitConfiguration: initConfiguration}, nil
}
func getJSONStringFromStruct(i interface{}) (string, error) {
b, err := json.Marshal(i)
if err != nil {
return "", err
}
return string(b), nil
}
func formatETCDEndpoints(etcds []string) []string {
formatedETCDs := make([]string, 0, len(etcds))
for _, etcd := range etcds {
formatedETCDs = append(formatedETCDs, fmt.Sprintf("https://%s/", etcd))
}
return formatedETCDs
}

View File

@@ -0,0 +1,52 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package kubeadm
import (
"io/ioutil"
"os"
"path"
"path/filepath"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig"
)
func buildCertificateDirectoryWithCA(ca CertificatePrivateKeyPair, directory string) error {
if err := os.MkdirAll(directory, os.FileMode(0o755)); err != nil {
return err
}
certPath := path.Join(directory, kubeadmconstants.CACertName)
if err := ioutil.WriteFile(certPath, ca.Certificate, os.FileMode(0o600)); err != nil {
return err
}
keyPath := path.Join(directory, kubeadmconstants.CAKeyName)
if err := ioutil.WriteFile(keyPath, ca.PrivateKey, os.FileMode(0o600)); err != nil {
return err
}
return nil
}
func CreateKubeconfig(kubeconfigName string, ca CertificatePrivateKeyPair, config *Configuration) ([]byte, error) {
if err := buildCertificateDirectoryWithCA(ca, config.InitConfiguration.CertificatesDir); err != nil {
return nil, err
}
defer deleteCertificateDirectory(config.InitConfiguration.CertificatesDir)
if err := kubeconfig.CreateKubeConfigFile(kubeconfigName, config.InitConfiguration.CertificatesDir, &config.InitConfiguration); err != nil {
return nil, err
}
path := filepath.Join(config.InitConfiguration.CertificatesDir, kubeconfigName)
return ioutil.ReadFile(path)
}
func IsKubeconfigValid(kubeconfigBytes []byte) bool {
return len(kubeconfigBytes) > 0
}

52
internal/kubeadm/types.go Normal file
View File

@@ -0,0 +1,52 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package kubeadm
import (
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeconfigutil "github.com/clastix/kamaji/internal/kubeconfig"
)
type Configuration struct {
InitConfiguration kubeadmapi.InitConfiguration
Kubeconfig kubeconfigutil.Kubeconfig
Parameters Parameters
}
type Parameters struct {
TenantControlPlaneName string
TenantControlPlaneNamespace string
TenantControlPlaneEndpoint string
TenantControlPlaneAddress string
TenantControlPlanePort int32
TenantControlPlaneDomain string
TenantControlPlanePodCIDR string
TenantControlPlaneServiceCIDR string
TenantDNSServiceIPs []string
TenantControlPlaneVersion string
TenantControlPlaneCGroupDriver string
ETCDs []string
ETCDCompactionInterval string
CertificatesDir string
KubeconfigDir string
}
type KubeletConfiguration struct {
TenantControlPlaneDomain string
TenantControlPlaneDNSServiceIPs []string
TenantControlPlaneCgroupDriver string
}
type CertificatePrivateKeyPair struct {
Name string
Certificate []byte
PrivateKey []byte
}
type PublicKeyPrivateKeyPair struct {
Name string
PublicKey []byte
PrivateKey []byte
}

View File

@@ -0,0 +1,188 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package kubeadm
import (
"fmt"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sversion "k8s.io/apimachinery/pkg/util/version"
"k8s.io/client-go/kubernetes"
kubelettypes "k8s.io/kubelet/config/v1beta1"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/uploadconfig"
"k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient"
"k8s.io/kubernetes/pkg/apis/rbac"
"k8s.io/utils/pointer"
)
func UploadKubeadmConfig(client kubernetes.Interface, config *Configuration) error {
return uploadconfig.UploadConfiguration(&config.InitConfiguration, client)
}
func UploadKubeletConfig(client kubernetes.Interface, config *Configuration) error {
kubeletConfiguration := KubeletConfiguration{
TenantControlPlaneDomain: config.InitConfiguration.Networking.DNSDomain,
TenantControlPlaneDNSServiceIPs: config.Parameters.TenantDNSServiceIPs,
TenantControlPlaneCgroupDriver: config.Parameters.TenantControlPlaneCGroupDriver,
}
content, err := getKubeletConfigmapContent(kubeletConfiguration)
if err != nil {
return err
}
configMapName, err := configMapName(config.Parameters.TenantControlPlaneVersion)
if err != nil {
return err
}
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configMapName,
Namespace: metav1.NamespaceSystem,
},
Data: map[string]string{
kubeadmconstants.KubeletBaseConfigurationConfigMapKey: string(content),
},
}
if err := apiclient.CreateOrUpdateConfigMap(client, configMap); err != nil {
return err
}
if err := createConfigMapRBACRules(client, config.Parameters.TenantControlPlaneVersion); err != nil {
return errors.Wrap(err, "error creating kubelet configuration configmap RBAC rules")
}
return nil
}
func getKubeletConfigmapContent(kubeletConfiguration KubeletConfiguration) ([]byte, error) {
zeroDuration := metav1.Duration{Duration: 0}
kc := kubelettypes.KubeletConfiguration{
TypeMeta: metav1.TypeMeta{
Kind: "KubeletConfiguration",
APIVersion: "kubelet.config.k8s.io/v1beta1",
},
Authentication: kubelettypes.KubeletAuthentication{
Anonymous: kubelettypes.KubeletAnonymousAuthentication{
Enabled: pointer.Bool(false),
},
Webhook: kubelettypes.KubeletWebhookAuthentication{
Enabled: pointer.Bool(true),
CacheTTL: zeroDuration,
},
X509: kubelettypes.KubeletX509Authentication{
ClientCAFile: "/etc/kubernetes/pki/ca.crt",
},
},
Authorization: kubelettypes.KubeletAuthorization{
Mode: kubelettypes.KubeletAuthorizationModeWebhook,
Webhook: kubelettypes.KubeletWebhookAuthorization{
CacheAuthorizedTTL: zeroDuration,
CacheUnauthorizedTTL: zeroDuration,
},
},
CgroupDriver: kubeletConfiguration.TenantControlPlaneCgroupDriver,
ClusterDNS: kubeletConfiguration.TenantControlPlaneDNSServiceIPs,
ClusterDomain: kubeletConfiguration.TenantControlPlaneDomain,
CPUManagerReconcilePeriod: zeroDuration,
EvictionHard: map[string]string{
"imagefs.available": "0%",
"nodefs.available": "0%",
"nodefs.inodesFree": "0%",
},
EvictionPressureTransitionPeriod: zeroDuration,
FileCheckFrequency: zeroDuration,
HealthzBindAddress: "127.0.0.1",
HealthzPort: pointer.Int32(10248),
HTTPCheckFrequency: zeroDuration,
ImageGCHighThresholdPercent: pointer.Int32(100),
NodeStatusUpdateFrequency: zeroDuration,
NodeStatusReportFrequency: zeroDuration,
RotateCertificates: true,
RuntimeRequestTimeout: zeroDuration,
ShutdownGracePeriod: zeroDuration,
ShutdownGracePeriodCriticalPods: zeroDuration,
StaticPodPath: "/etc/kubernetes/manifests",
StreamingConnectionIdleTimeout: zeroDuration,
SyncFrequency: zeroDuration,
VolumeStatsAggPeriod: zeroDuration,
}
return EncondeToYaml(&kc)
}
func createConfigMapRBACRules(client kubernetes.Interface, kubernetesVersion string) error {
configMapName, err := configMapName(kubernetesVersion)
if err != nil {
return err
}
configMapRBACName, err := configMapRBACName(kubernetesVersion)
if err != nil {
return err
}
if err := apiclient.CreateOrUpdateRole(client, &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: configMapRBACName,
Namespace: metav1.NamespaceSystem,
},
Rules: []rbacv1.PolicyRule{
{
Verbs: []string{"get"},
APIGroups: []string{""},
Resources: []string{"configmaps"},
ResourceNames: []string{configMapName},
},
},
}); err != nil {
return err
}
return apiclient.CreateOrUpdateRoleBinding(client, &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: configMapRBACName,
Namespace: metav1.NamespaceSystem,
},
RoleRef: rbacv1.RoleRef{
APIGroup: rbac.GroupName,
Kind: "Role",
Name: configMapRBACName,
},
Subjects: []rbacv1.Subject{
{
Kind: rbac.GroupKind,
Name: kubeadmconstants.NodesGroup,
},
{
Kind: rbac.GroupKind,
Name: kubeadmconstants.NodeBootstrapTokenAuthGroup,
},
},
})
}
func configMapName(kubernetesVersion string) (string, error) {
version, err := k8sversion.ParseSemantic(kubernetesVersion)
if err != nil {
return "", err
}
return kubeadmconstants.GetKubeletConfigMapName(version, true), nil
}
func configMapRBACName(kubernetesVersion string) (string, error) {
version, err := k8sversion.ParseSemantic(kubernetesVersion)
if err != nil {
return "", err
}
return fmt.Sprintf("%s%d.%d", kubeadmconstants.KubeletBaseConfigMapRolePrefix, version.Major(), version.Minor()), nil
}

20
internal/kubeadm/utils.go Normal file
View File

@@ -0,0 +1,20 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package kubeadm
import (
"bytes"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
)
func EncondeToYaml(o runtime.Object) ([]byte, error) {
scheme := runtime.NewScheme()
encoder := json.NewYAMLSerializer(json.SimpleMetaFactory{}, scheme, scheme)
buf := bytes.NewBuffer([]byte{})
err := encoder.Encode(o, buf)
return buf.Bytes(), err
}

View File

@@ -0,0 +1,28 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package kubeconfig
import (
"bytes"
"k8s.io/apimachinery/pkg/util/yaml"
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
)
type Kubeconfig v1.Config
func GetKubeconfigFromBytesBuffer(buffer *bytes.Buffer) (*Kubeconfig, error) {
kubeconfig := &Kubeconfig{}
if err := yaml.NewYAMLOrJSONDecoder(buffer, buffer.Len()).Decode(kubeconfig); err != nil {
return nil, err
}
return kubeconfig, nil
}
func GetKubeconfigFromBytes(b []byte) (*Kubeconfig, error) {
buffer := bytes.NewBuffer(b)
return GetKubeconfigFromBytesBuffer(buffer)
}

View File

@@ -0,0 +1,136 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"context"
"fmt"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8stypes "k8s.io/apimachinery/pkg/types"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/kubeadm"
"github.com/clastix/kamaji/internal/utilities"
)
type APIServerCertificate struct {
resource *corev1.Secret
Client client.Client
Log logr.Logger
Name string
TmpDirectory string
}
func (r *APIServerCertificate) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
return tenantControlPlane.Status.Certificates.APIServer.SecretName != r.resource.GetName()
}
func (r *APIServerCertificate) ShouldCleanup(plane *kamajiv1alpha1.TenantControlPlane) bool {
return false
}
func (r *APIServerCertificate) CleanUp(ctx context.Context) (bool, error) {
return false, nil
}
func (r *APIServerCertificate) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
r.resource = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: r.getPrefixedName(tenantControlPlane),
Namespace: tenantControlPlane.GetNamespace(),
},
}
return nil
}
func (r *APIServerCertificate) getPrefixedName(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) string {
return utilities.AddTenantPrefix(r.Name, tenantControlPlane)
}
func (r *APIServerCertificate) GetClient() client.Client {
return r.Client
}
func (r *APIServerCertificate) GetTmpDirectory() string {
return r.TmpDirectory
}
func (r *APIServerCertificate) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
return controllerutil.CreateOrUpdate(ctx, r.Client, r.resource, r.mutate(ctx, tenantControlPlane))
}
func (r *APIServerCertificate) GetName() string {
return r.Name
}
func (r *APIServerCertificate) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
tenantControlPlane.Status.Certificates.APIServer.LastUpdate = metav1.Now()
tenantControlPlane.Status.Certificates.APIServer.SecretName = r.resource.GetName()
return nil
}
func (r *APIServerCertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn {
return func() error {
latestConfigRV := getLatestConfigRV(*tenantControlPlane)
actualConfigRV := r.resource.GetLabels()["latest-config-rv"]
if latestConfigRV == actualConfigRV {
isValid, err := kubeadm.IsCertificatePrivateKeyPairValid(
r.resource.Data[kubeadmconstants.APIServerCertName],
r.resource.Data[kubeadmconstants.APIServerKeyName],
)
if err != nil {
r.Log.Info(fmt.Sprintf("%s certificate-private_key pair is not valid: %s", kubeadmconstants.APIServerCertAndKeyBaseName, err.Error()))
}
if isValid {
return nil
}
}
config, _, err := getKubeadmConfiguration(ctx, r, tenantControlPlane)
if err != nil {
return err
}
namespacedName := k8stypes.NamespacedName{Namespace: tenantControlPlane.GetNamespace(), Name: tenantControlPlane.Status.Certificates.CA.SecretName}
secretCA := &corev1.Secret{}
if err = r.Client.Get(ctx, namespacedName, secretCA); err != nil {
return err
}
ca := kubeadm.CertificatePrivateKeyPair{
Name: kubeadmconstants.CACertAndKeyBaseName,
Certificate: secretCA.Data[kubeadmconstants.CACertName],
PrivateKey: secretCA.Data[kubeadmconstants.CAKeyName],
}
certificateKeyPair, err := kubeadm.GenerateCertificatePrivateKeyPair(kubeadmconstants.APIServerCertAndKeyBaseName, config, ca)
if err != nil {
return err
}
r.resource.Data = map[string][]byte{
kubeadmconstants.APIServerCertName: certificateKeyPair.Certificate,
kubeadmconstants.APIServerKeyName: certificateKeyPair.PrivateKey,
}
r.resource.SetLabels(utilities.MergeMaps(
utilities.KamajiLabels(),
map[string]string{
"latest-config-rv": latestConfigRV,
"kamaji.clastix.io/name": tenantControlPlane.GetName(),
"kamaji.clastix.io/component": r.GetName(),
},
))
return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
}
}

View File

@@ -0,0 +1,136 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"context"
"fmt"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8stypes "k8s.io/apimachinery/pkg/types"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/kubeadm"
"github.com/clastix/kamaji/internal/utilities"
)
type APIServerKubeletClientCertificate struct {
resource *corev1.Secret
Client client.Client
Log logr.Logger
Name string
TmpDirectory string
}
func (r *APIServerKubeletClientCertificate) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
return tenantControlPlane.Status.Certificates.APIServerKubeletClient.SecretName != r.resource.GetName()
}
func (r *APIServerKubeletClientCertificate) ShouldCleanup(plane *kamajiv1alpha1.TenantControlPlane) bool {
return false
}
func (r *APIServerKubeletClientCertificate) CleanUp(ctx context.Context) (bool, error) {
return false, nil
}
func (r *APIServerKubeletClientCertificate) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
r.resource = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: r.getPrefixedName(tenantControlPlane),
Namespace: tenantControlPlane.GetNamespace(),
},
}
return nil
}
func (r *APIServerKubeletClientCertificate) getPrefixedName(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) string {
return utilities.AddTenantPrefix(r.Name, tenantControlPlane)
}
func (r *APIServerKubeletClientCertificate) GetClient() client.Client {
return r.Client
}
func (r *APIServerKubeletClientCertificate) GetTmpDirectory() string {
return r.TmpDirectory
}
func (r *APIServerKubeletClientCertificate) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
return controllerutil.CreateOrUpdate(ctx, r.Client, r.resource, r.mutate(ctx, tenantControlPlane))
}
func (r *APIServerKubeletClientCertificate) GetName() string {
return r.Name
}
func (r *APIServerKubeletClientCertificate) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
tenantControlPlane.Status.Certificates.APIServerKubeletClient.LastUpdate = metav1.Now()
tenantControlPlane.Status.Certificates.APIServerKubeletClient.SecretName = r.resource.GetName()
return nil
}
func (r *APIServerKubeletClientCertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn {
return func() error {
latestConfigRV := getLatestConfigRV(*tenantControlPlane)
actualConfigRV := r.resource.GetLabels()["latest-config-rv"]
if latestConfigRV == actualConfigRV {
isValid, err := kubeadm.IsCertificatePrivateKeyPairValid(
r.resource.Data[kubeadmconstants.APIServerKubeletClientCertName],
r.resource.Data[kubeadmconstants.APIServerKubeletClientKeyName],
)
if err != nil {
r.Log.Info(fmt.Sprintf("%s certificate-private_key pair is not valid: %s", kubeadmconstants.APIServerKubeletClientCertAndKeyBaseName, err.Error()))
}
if isValid {
return nil
}
}
config, _, err := getKubeadmConfiguration(ctx, r, tenantControlPlane)
if err != nil {
return err
}
namespacedName := k8stypes.NamespacedName{Namespace: tenantControlPlane.GetNamespace(), Name: tenantControlPlane.Status.Certificates.CA.SecretName}
secretCA := &corev1.Secret{}
if err = r.Client.Get(ctx, namespacedName, secretCA); err != nil {
return err
}
ca := kubeadm.CertificatePrivateKeyPair{
Name: kubeadmconstants.CACertAndKeyBaseName,
Certificate: secretCA.Data[kubeadmconstants.CACertName],
PrivateKey: secretCA.Data[kubeadmconstants.CAKeyName],
}
certificateKeyPair, err := kubeadm.GenerateCertificatePrivateKeyPair(kubeadmconstants.APIServerKubeletClientCertAndKeyBaseName, config, ca)
if err != nil {
return err
}
r.resource.Data = map[string][]byte{
kubeadmconstants.APIServerKubeletClientCertName: certificateKeyPair.Certificate,
kubeadmconstants.APIServerKubeletClientKeyName: certificateKeyPair.PrivateKey,
}
r.resource.SetLabels(utilities.MergeMaps(
utilities.KamajiLabels(),
map[string]string{
"latest-config-rv": latestConfigRV,
"kamaji.clastix.io/name": tenantControlPlane.GetName(),
"kamaji.clastix.io/component": r.GetName(),
},
))
return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
}
}

View File

@@ -0,0 +1,119 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"context"
"fmt"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/kubeadm"
"github.com/clastix/kamaji/internal/utilities"
)
type CACertificate struct {
resource *corev1.Secret
Client client.Client
Log logr.Logger
Name string
TmpDirectory string
}
func (r *CACertificate) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
return tenantControlPlane.Status.Certificates.CA.SecretName != r.resource.GetName()
}
func (r *CACertificate) ShouldCleanup(plane *kamajiv1alpha1.TenantControlPlane) bool {
return false
}
func (r *CACertificate) CleanUp(ctx context.Context) (bool, error) {
return false, nil
}
func (r *CACertificate) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
r.resource = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: r.getPrefixedName(tenantControlPlane),
Namespace: tenantControlPlane.GetNamespace(),
},
}
return nil
}
func (r *CACertificate) getPrefixedName(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) string {
return utilities.AddTenantPrefix(r.Name, tenantControlPlane)
}
func (r *CACertificate) GetClient() client.Client {
return r.Client
}
func (r *CACertificate) GetTmpDirectory() string {
return r.TmpDirectory
}
func (r *CACertificate) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
return controllerutil.CreateOrUpdate(ctx, r.Client, r.resource, r.mutate(ctx, tenantControlPlane))
}
func (r *CACertificate) GetName() string {
return r.Name
}
func (r *CACertificate) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
tenantControlPlane.Status.Certificates.CA.LastUpdate = metav1.Now()
tenantControlPlane.Status.Certificates.CA.SecretName = r.resource.GetName()
return nil
}
func (r *CACertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn {
return func() error {
isValid, err := kubeadm.IsCertificatePrivateKeyPairValid(
r.resource.Data[kubeadmconstants.CACertName],
r.resource.Data[kubeadmconstants.CAKeyName],
)
if err != nil {
r.Log.Info(fmt.Sprintf("%s certificate-private_key pair is not valid: %s", kubeadmconstants.CACertAndKeyBaseName, err.Error()))
}
if isValid {
return nil
}
config, _, err := getKubeadmConfiguration(ctx, r, tenantControlPlane)
if err != nil {
return err
}
ca, err := kubeadm.GenerateCACertificatePrivateKeyPair(kubeadmconstants.CACertAndKeyBaseName, config)
if err != nil {
return err
}
r.resource.Data = map[string][]byte{
kubeadmconstants.CACertName: ca.Certificate,
kubeadmconstants.CAKeyName: ca.PrivateKey,
}
r.resource.SetLabels(utilities.MergeMaps(
utilities.KamajiLabels(),
map[string]string{
"kamaji.clastix.io/name": tenantControlPlane.GetName(),
"kamaji.clastix.io/component": r.GetName(),
},
))
return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
}
}

View File

@@ -0,0 +1,8 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
const (
defaultIngressPort = 443
)

View File

@@ -0,0 +1,119 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"context"
"fmt"
"reflect"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8stypes "k8s.io/apimachinery/pkg/types"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/etcd"
"github.com/clastix/kamaji/internal/utilities"
)
type ETCDCACertificatesResource struct {
resource *corev1.Secret
Client client.Client
Log logr.Logger
Name string
ETCDCASecretName string
ETCDCASecretNamespace string
}
func (r *ETCDCACertificatesResource) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
if tenantControlPlane.Status.Certificates.ETCD == nil {
return true
}
return tenantControlPlane.Status.Certificates.ETCD.CA.SecretName != r.resource.GetName()
}
func (r *ETCDCACertificatesResource) ShouldCleanup(plane *kamajiv1alpha1.TenantControlPlane) bool {
return false
}
func (r *ETCDCACertificatesResource) CleanUp(ctx context.Context) (bool, error) {
return false, nil
}
func (r *ETCDCACertificatesResource) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
r.resource = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: r.getPrefixedName(tenantControlPlane),
Namespace: tenantControlPlane.GetNamespace(),
},
}
return nil
}
func (r *ETCDCACertificatesResource) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
return controllerutil.CreateOrUpdate(ctx, r.Client, r.resource, r.mutate(ctx, tenantControlPlane))
}
func (r *ETCDCACertificatesResource) GetName() string {
return r.Name
}
func (r *ETCDCACertificatesResource) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
if tenantControlPlane.Status.Certificates.ETCD == nil {
tenantControlPlane.Status.Certificates.ETCD = &kamajiv1alpha1.ETCDCertificatesStatus{}
}
tenantControlPlane.Status.Certificates.ETCD.CA.SecretName = r.resource.GetName()
tenantControlPlane.Status.Certificates.ETCD.CA.LastUpdate = metav1.Now()
return nil
}
func (r *ETCDCACertificatesResource) getPrefixedName(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) string {
return utilities.AddTenantPrefix(r.Name, tenantControlPlane)
}
func (r *ETCDCACertificatesResource) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn {
return func() error {
r.resource.SetLabels(utilities.KamajiLabels())
etcdCASecretNamespacedName := k8stypes.NamespacedName{Namespace: r.ETCDCASecretNamespace, Name: r.ETCDCASecretName}
etcdCASecret := &corev1.Secret{}
if err := r.Client.Get(ctx, etcdCASecretNamespacedName, etcdCASecret); err != nil {
return err
}
isValid, err := etcd.IsETCDCertificateAndKeyPairValid(r.resource.Data[kubeadmconstants.CACertName], r.resource.Data[kubeadmconstants.CAKeyName])
if err != nil {
r.Log.Info(fmt.Sprintf("etcd certificates are not valid: %s", err.Error()))
}
if reflect.DeepEqual(etcdCASecret.Data[kubeadmconstants.CACertName], r.resource.Data[kubeadmconstants.CACertName]) &&
reflect.DeepEqual(etcdCASecret.Data[kubeadmconstants.CAKeyName], r.resource.Data[kubeadmconstants.CAKeyName]) {
if isValid {
return nil
}
return fmt.Errorf("CA certificates provided into secrets %s/%s are not valid", r.ETCDCASecretNamespace, r.ETCDCASecretName)
}
r.resource.Data = map[string][]byte{
kubeadmconstants.CACertName: etcdCASecret.Data[kubeadmconstants.CACertName],
kubeadmconstants.CAKeyName: etcdCASecret.Data[kubeadmconstants.CAKeyName],
}
if err = ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme()); err != nil {
return err
}
return nil
}
}

View File

@@ -0,0 +1,124 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"context"
"fmt"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8stypes "k8s.io/apimachinery/pkg/types"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/etcd"
"github.com/clastix/kamaji/internal/utilities"
)
type ETCDCertificatesResource struct {
resource *corev1.Secret
Client client.Client
Log logr.Logger
Name string
}
func (r *ETCDCertificatesResource) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
if tenantControlPlane.Status.Certificates.ETCD == nil {
return true
}
return tenantControlPlane.Status.Certificates.ETCD.APIServer.SecretName != r.resource.GetName()
}
func (r *ETCDCertificatesResource) ShouldCleanup(plane *kamajiv1alpha1.TenantControlPlane) bool {
return false
}
func (r *ETCDCertificatesResource) CleanUp(ctx context.Context) (bool, error) {
return false, nil
}
func (r *ETCDCertificatesResource) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
r.resource = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: r.getPrefixedName(tenantControlPlane),
Namespace: tenantControlPlane.GetNamespace(),
},
}
return nil
}
func (r *ETCDCertificatesResource) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
return controllerutil.CreateOrUpdate(ctx, r.Client, r.resource, r.mutate(ctx, tenantControlPlane))
}
func (r *ETCDCertificatesResource) GetName() string {
return r.Name
}
func (r *ETCDCertificatesResource) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
if tenantControlPlane.Status.Certificates.ETCD == nil {
tenantControlPlane.Status.Certificates.ETCD = &kamajiv1alpha1.ETCDCertificatesStatus{}
}
tenantControlPlane.Status.Certificates.ETCD.APIServer.SecretName = r.resource.GetName()
tenantControlPlane.Status.Certificates.ETCD.APIServer.LastUpdate = metav1.Now()
return nil
}
func (r *ETCDCertificatesResource) getPrefixedName(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) string {
return utilities.AddTenantPrefix(r.Name, tenantControlPlane)
}
func (r *ETCDCertificatesResource) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn {
return func() error {
r.resource.SetLabels(utilities.KamajiLabels())
if tenantControlPlane.Status.Certificates.ETCD == nil {
return fmt.Errorf("etcd is still synchronizing latest changes")
}
etcdCASecretNamespacedName := k8stypes.NamespacedName{Namespace: tenantControlPlane.GetNamespace(), Name: tenantControlPlane.Status.Certificates.ETCD.CA.SecretName}
etcdCASecret := &corev1.Secret{}
if err := r.Client.Get(ctx, etcdCASecretNamespacedName, etcdCASecret); err != nil {
return err
}
isValid, err := etcd.IsETCDCertificateAndKeyPairValid(r.resource.Data[kubeadmconstants.APIServerEtcdClientCertName], r.resource.Data[kubeadmconstants.APIServerEtcdClientKeyName])
if err != nil {
r.Log.Info(fmt.Sprintf("etcd certificates are not valid: %s", err.Error()))
}
if isValid {
return nil
}
cert, privKey, err := etcd.GetETCDCACertificateAndKeyPair(
tenantControlPlane.GetName(),
etcdCASecret.Data[kubeadmconstants.CACertName],
etcdCASecret.Data[kubeadmconstants.CAKeyName],
)
if err != nil {
return err
}
r.resource.Data = map[string][]byte{
kubeadmconstants.APIServerEtcdClientCertName: cert.Bytes(),
kubeadmconstants.APIServerEtcdClientKeyName: privKey.Bytes(),
}
if err = ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme()); err != nil {
return err
}
return nil
}
}

View File

@@ -0,0 +1,279 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"context"
"github.com/go-logr/logr"
etcdclient "go.etcd.io/etcd/client/v3"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
k8stypes "k8s.io/apimachinery/pkg/types"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/etcd"
)
const (
caKeyName = kubeadmconstants.CACertName
)
type resource struct {
role etcd.Role
user etcd.User
}
type ETCDSetupResource struct {
resource *resource
Client client.Client
Scheme *runtime.Scheme
Log logr.Logger
Name string
Endpoints []string
ETCDClientCertsSecret k8stypes.NamespacedName
ETCDCACertsSecret k8stypes.NamespacedName
}
func (r *ETCDSetupResource) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
if tenantControlPlane.Status.Storage.ETCD == nil {
return true
}
return tenantControlPlane.Status.Storage.ETCD.Role.Name != r.resource.role.Name ||
tenantControlPlane.Status.Storage.ETCD.User.Name != r.resource.user.Name
}
func (r *ETCDSetupResource) ShouldCleanup(plane *kamajiv1alpha1.TenantControlPlane) bool {
return false
}
func (r *ETCDSetupResource) CleanUp(ctx context.Context) (bool, error) {
return false, nil
}
func (r *ETCDSetupResource) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
r.resource = &resource{
role: etcd.Role{Name: tenantControlPlane.Name, Exists: false},
user: etcd.User{Name: tenantControlPlane.Name, Exists: false},
}
return nil
}
func (r *ETCDSetupResource) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
return r.reconcile(ctx)
}
func (r *ETCDSetupResource) Delete(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
if err := r.Define(ctx, tenantControlPlane); err != nil {
return err
}
client, err := r.getETCDClient(ctx)
if err != nil {
return err
}
if err = r.deleteData(ctx, client, tenantControlPlane); err != nil {
return err
}
if err = r.deleteUser(ctx, client, tenantControlPlane); err != nil {
return err
}
if err = r.deleteRole(ctx, client, tenantControlPlane); err != nil {
return err
}
return nil
}
func (r *ETCDSetupResource) GetName() string {
return r.Name
}
func (r *ETCDSetupResource) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
if tenantControlPlane.Status.Storage.ETCD == nil {
tenantControlPlane.Status.Storage.ETCD = &kamajiv1alpha1.ETCDStatus{}
}
tenantControlPlane.Status.Storage.ETCD.Role = r.resource.role
tenantControlPlane.Status.Storage.ETCD.User = r.resource.user
return nil
}
func (r *ETCDSetupResource) reconcile(ctx context.Context) (controllerutil.OperationResult, error) {
reconcilationResult := controllerutil.OperationResultNone
var operationResult controllerutil.OperationResult
client, err := r.getETCDClient(ctx)
if err != nil {
return reconcilationResult, err
}
operationResult, err = r.reconcileUser(ctx, client)
if err != nil {
return reconcilationResult, err
}
reconcilationResult = updateOperationResult(reconcilationResult, operationResult)
operationResult, err = r.reconcileRole(ctx, client)
if err != nil {
return controllerutil.OperationResultNone, err
}
reconcilationResult = updateOperationResult(reconcilationResult, operationResult)
operationResult, err = r.grantUserRole(ctx, client)
if err != nil {
return controllerutil.OperationResultNone, err
}
reconcilationResult = updateOperationResult(reconcilationResult, operationResult)
operationResult, err = r.grantRolePermissions(ctx, client)
if err != nil {
return controllerutil.OperationResultNone, err
}
reconcilationResult = updateOperationResult(reconcilationResult, operationResult)
return reconcilationResult, nil
}
func (r *ETCDSetupResource) getETCDClient(ctx context.Context) (*etcdclient.Client, error) {
var certsClientSecret corev1.Secret
if err := r.Client.Get(ctx, r.ETCDClientCertsSecret, &certsClientSecret); err != nil {
return nil, err
}
var certsCASecret corev1.Secret
if err := r.Client.Get(ctx, r.ETCDCACertsSecret, &certsCASecret); err != nil {
return nil, err
}
config := etcd.Config{
ETCDCertificate: certsClientSecret.Data[corev1.TLSCertKey],
ETCDPrivateKey: certsClientSecret.Data[corev1.TLSPrivateKeyKey],
ETCDCA: certsCASecret.Data[caKeyName],
Endpoints: r.Endpoints,
}
return etcd.NewClient(config)
}
func (r *ETCDSetupResource) reconcileUser(ctx context.Context, client *etcdclient.Client) (controllerutil.OperationResult, error) {
if err := etcd.GetUser(ctx, client, &r.resource.user); err != nil {
return controllerutil.OperationResultNone, err
}
if !r.resource.user.Exists {
if err := etcd.AddUser(ctx, client, r.resource.user.Name); err != nil {
return controllerutil.OperationResultNone, err
}
return controllerutil.OperationResultCreated, nil
}
return controllerutil.OperationResultNone, nil
}
func (r *ETCDSetupResource) reconcileRole(ctx context.Context, client *etcdclient.Client) (controllerutil.OperationResult, error) {
if err := etcd.GetRole(ctx, client, &r.resource.role); err != nil {
return controllerutil.OperationResultNone, err
}
if !r.resource.role.Exists {
if err := etcd.AddRole(ctx, client, r.resource.role.Name); err != nil {
return controllerutil.OperationResultNone, err
}
return controllerutil.OperationResultCreated, nil
}
return controllerutil.OperationResultNone, nil
}
func (r *ETCDSetupResource) grantUserRole(ctx context.Context, client *etcdclient.Client) (controllerutil.OperationResult, error) {
if err := etcd.GetUser(ctx, client, &r.resource.user); err != nil {
return controllerutil.OperationResultNone, err
}
if len(r.resource.user.Roles) > 0 && isRole(r.resource.user.Roles, r.resource.role.Name) {
return controllerutil.OperationResultNone, nil
}
if err := etcd.GrantUserRole(ctx, client, r.resource.user, r.resource.role); err != nil {
return controllerutil.OperationResultNone, err
}
return controllerutil.OperationResultUpdated, nil
}
func (r *ETCDSetupResource) grantRolePermissions(ctx context.Context, client *etcdclient.Client) (controllerutil.OperationResult, error) {
if err := etcd.GetRole(ctx, client, &r.resource.role); err != nil {
return controllerutil.OperationResultNone, err
}
if len(r.resource.role.Permissions) > 0 && isPermission(r.resource.role.Permissions, r.resource.role.Name) {
return controllerutil.OperationResultNone, nil
}
if err := etcd.GrantRolePermission(ctx, client, r.resource.role); err != nil {
return controllerutil.OperationResultNone, err
}
return controllerutil.OperationResultUpdated, nil
}
func (r *ETCDSetupResource) deleteData(ctx context.Context, client *etcdclient.Client, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
return etcd.CleanUpPrefix(ctx, client, tenantControlPlane.GetName())
}
func (r *ETCDSetupResource) deleteUser(ctx context.Context, client *etcdclient.Client, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
if err := etcd.GetUser(ctx, client, &r.resource.user); err != nil {
return err
}
if !r.resource.user.Exists {
return nil
}
return etcd.RemoveUser(ctx, client, tenantControlPlane.GetName())
}
func (r *ETCDSetupResource) deleteRole(ctx context.Context, client *etcdclient.Client, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
if err := etcd.GetRole(ctx, client, &r.resource.role); err != nil {
return err
}
if !r.resource.role.Exists {
return nil
}
return etcd.RemoveRole(ctx, client, tenantControlPlane.GetName())
}
func isRole(s []string, x string) bool {
for _, o := range s {
if o == x {
return true
}
}
return false
}
func isPermission(s []etcd.Permission, role string) bool {
key := etcd.BuildKey(role)
for _, o := range s {
if o.Key == key {
return true
}
}
return false
}

View File

@@ -0,0 +1,136 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"context"
"fmt"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8stypes "k8s.io/apimachinery/pkg/types"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/kubeadm"
"github.com/clastix/kamaji/internal/utilities"
)
type FrontProxyClientCertificate struct {
resource *corev1.Secret
Client client.Client
Log logr.Logger
Name string
TmpDirectory string
}
func (r *FrontProxyClientCertificate) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
return tenantControlPlane.Status.Certificates.FrontProxyClient.SecretName != r.resource.GetName()
}
func (r *FrontProxyClientCertificate) ShouldCleanup(plane *kamajiv1alpha1.TenantControlPlane) bool {
return false
}
func (r *FrontProxyClientCertificate) CleanUp(ctx context.Context) (bool, error) {
return false, nil
}
func (r *FrontProxyClientCertificate) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
r.resource = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: r.getPrefixedName(tenantControlPlane),
Namespace: tenantControlPlane.GetNamespace(),
},
}
return nil
}
func (r *FrontProxyClientCertificate) getPrefixedName(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) string {
return utilities.AddTenantPrefix(r.Name, tenantControlPlane)
}
func (r *FrontProxyClientCertificate) GetClient() client.Client {
return r.Client
}
func (r *FrontProxyClientCertificate) GetTmpDirectory() string {
return r.TmpDirectory
}
func (r *FrontProxyClientCertificate) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
return controllerutil.CreateOrUpdate(ctx, r.Client, r.resource, r.mutate(ctx, tenantControlPlane))
}
func (r *FrontProxyClientCertificate) GetName() string {
return r.Name
}
func (r *FrontProxyClientCertificate) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
tenantControlPlane.Status.Certificates.FrontProxyClient.LastUpdate = metav1.Now()
tenantControlPlane.Status.Certificates.FrontProxyClient.SecretName = r.resource.GetName()
return nil
}
func (r *FrontProxyClientCertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn {
return func() error {
latestConfigRV := getLatestConfigRV(*tenantControlPlane)
actualConfigRV := r.resource.GetLabels()["latest-config-rv"]
if latestConfigRV == actualConfigRV {
isValid, err := kubeadm.IsCertificatePrivateKeyPairValid(
r.resource.Data[kubeadmconstants.FrontProxyClientCertName],
r.resource.Data[kubeadmconstants.FrontProxyClientKeyName],
)
if err != nil {
r.Log.Info(fmt.Sprintf("%s certificate-private_key pair is not valid: %s", kubeadmconstants.FrontProxyClientCertAndKeyBaseName, err.Error()))
}
if isValid {
return nil
}
}
config, _, err := getKubeadmConfiguration(ctx, r, tenantControlPlane)
if err != nil {
return err
}
namespacedName := k8stypes.NamespacedName{Namespace: tenantControlPlane.GetNamespace(), Name: tenantControlPlane.Status.Certificates.FrontProxyCA.SecretName}
secretCA := &corev1.Secret{}
if err = r.Client.Get(ctx, namespacedName, secretCA); err != nil {
return err
}
ca := kubeadm.CertificatePrivateKeyPair{
Name: kubeadmconstants.FrontProxyCACertAndKeyBaseName,
Certificate: secretCA.Data[kubeadmconstants.FrontProxyCACertName],
PrivateKey: secretCA.Data[kubeadmconstants.FrontProxyCAKeyName],
}
certificateKeyPair, err := kubeadm.GenerateCertificatePrivateKeyPair(kubeadmconstants.FrontProxyClientCertAndKeyBaseName, config, ca)
if err != nil {
return err
}
r.resource.Data = map[string][]byte{
kubeadmconstants.FrontProxyClientCertName: certificateKeyPair.Certificate,
kubeadmconstants.FrontProxyClientKeyName: certificateKeyPair.PrivateKey,
}
r.resource.SetLabels(utilities.MergeMaps(
utilities.KamajiLabels(),
map[string]string{
"latest-config-rv": latestConfigRV,
"kamaji.clastix.io/name": tenantControlPlane.GetName(),
"kamaji.clastix.io/component": r.GetName(),
},
))
return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
}
}

View File

@@ -0,0 +1,121 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"context"
"fmt"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/kubeadm"
"github.com/clastix/kamaji/internal/utilities"
)
type FrontProxyCACertificate struct {
resource *corev1.Secret
Client client.Client
Log logr.Logger
Name string
TmpDirectory string
}
func (r *FrontProxyCACertificate) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
return tenantControlPlane.Status.Certificates.FrontProxyCA.SecretName != r.resource.GetName()
}
func (r *FrontProxyCACertificate) ShouldCleanup(plane *kamajiv1alpha1.TenantControlPlane) bool {
return false
}
func (r *FrontProxyCACertificate) CleanUp(ctx context.Context) (bool, error) {
return false, nil
}
func (r *FrontProxyCACertificate) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
r.resource = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: r.getPrefixedName(tenantControlPlane),
Namespace: tenantControlPlane.GetNamespace(),
},
}
r.Name = "front-proxy-ca-certificate"
return nil
}
func (r *FrontProxyCACertificate) getPrefixedName(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) string {
return utilities.AddTenantPrefix(r.Name, tenantControlPlane)
}
func (r *FrontProxyCACertificate) GetClient() client.Client {
return r.Client
}
func (r *FrontProxyCACertificate) GetTmpDirectory() string {
return r.TmpDirectory
}
func (r *FrontProxyCACertificate) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
return controllerutil.CreateOrUpdate(ctx, r.Client, r.resource, r.mutate(ctx, tenantControlPlane))
}
func (r *FrontProxyCACertificate) GetName() string {
return r.Name
}
func (r *FrontProxyCACertificate) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
tenantControlPlane.Status.Certificates.FrontProxyCA.LastUpdate = metav1.Now()
tenantControlPlane.Status.Certificates.FrontProxyCA.SecretName = r.resource.GetName()
return nil
}
func (r *FrontProxyCACertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn {
return func() error {
isValid, err := kubeadm.IsCertificatePrivateKeyPairValid(
r.resource.Data[kubeadmconstants.FrontProxyCACertName],
r.resource.Data[kubeadmconstants.FrontProxyCAKeyName],
)
if err != nil {
r.Log.Info(fmt.Sprintf("%s certificate-private_key pair is not valid: %s", kubeadmconstants.FrontProxyCACertAndKeyBaseName, err.Error()))
}
if isValid {
return nil
}
config, _, err := getKubeadmConfiguration(ctx, r, tenantControlPlane)
if err != nil {
return err
}
ca, err := kubeadm.GenerateCACertificatePrivateKeyPair(kubeadmconstants.FrontProxyCACertAndKeyBaseName, config)
if err != nil {
return err
}
r.resource.Data = map[string][]byte{
kubeadmconstants.FrontProxyCACertName: ca.Certificate,
kubeadmconstants.FrontProxyCAKeyName: ca.PrivateKey,
}
r.resource.SetLabels(utilities.MergeMaps(
utilities.KamajiLabels(),
map[string]string{
"kamaji.clastix.io/name": tenantControlPlane.GetName(),
"kamaji.clastix.io/component": r.GetName(),
},
))
return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
}
}

View File

@@ -0,0 +1,605 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"context"
"crypto/md5"
"fmt"
"path"
"sort"
"strings"
"github.com/pkg/errors"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
quantity "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/utilities"
)
type KubernetesDeploymentResource struct {
resource *appsv1.Deployment
Client client.Client
ETCDEndpoints []string
ETCDCompactionInterval string
Name string
}
func (r *KubernetesDeploymentResource) isStatusEqual(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
return r.resource.Status.String() == tenantControlPlane.Status.Kubernetes.Deployment.DeploymentStatus.String()
}
func (r *KubernetesDeploymentResource) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
return !r.isStatusEqual(tenantControlPlane) || tenantControlPlane.Spec.Kubernetes.Version != tenantControlPlane.Status.Kubernetes.Version.Version
}
func (r *KubernetesDeploymentResource) ShouldCleanup(plane *kamajiv1alpha1.TenantControlPlane) bool {
return false
}
func (r *KubernetesDeploymentResource) CleanUp(ctx context.Context) (bool, error) {
return false, nil
}
func (r *KubernetesDeploymentResource) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
r.resource = &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: tenantControlPlane.GetName(),
Namespace: tenantControlPlane.GetNamespace(),
Labels: utilities.CommonLabels(tenantControlPlane.GetName()),
},
}
r.Name = "deployment"
return nil
}
// secretHashValue function returns the md5 value for the given secret:
// this will trigger a new rollout in case of value change.
func (r *KubernetesDeploymentResource) secretHashValue(ctx context.Context, namespace, name string) (string, error) {
secret := &corev1.Secret{}
if err := r.Client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, secret); err != nil {
return "", errors.Wrap(err, "cannot retrieve *corev1.Secret for resource version retrieval")
}
// Go access map values in random way, it means we have to sort them
keys := make([]string, 0, len(secret.Data))
for k := range secret.Data {
keys = append(keys, k)
}
sort.Strings(keys)
// Generating MD5 of Secret values, sorted by key
h := md5.New()
for _, key := range keys {
h.Write(secret.Data[key])
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
func (r *KubernetesDeploymentResource) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
maxSurge := intstr.FromString("100%")
maxUnavailable := intstr.FromInt(0)
etcdEndpoints := make([]string, len(r.ETCDEndpoints))
for i, v := range r.ETCDEndpoints {
etcdEndpoints[i] = fmt.Sprintf("https://%s", v)
}
address, err := tenantControlPlane.GetAddress(ctx, r.Client)
if err != nil {
return controllerutil.OperationResultNone, errors.Wrap(err, "cannot create TenantControlPlane Deployment")
}
return controllerutil.CreateOrUpdate(ctx, r.Client, r.resource, func() error {
labels := utilities.MergeMaps(r.resource.GetLabels(), tenantControlPlane.Spec.ControlPlane.Deployment.AdditionalMetadata.Labels)
r.resource.SetLabels(labels)
annotations := utilities.MergeMaps(r.resource.GetAnnotations(), tenantControlPlane.Spec.ControlPlane.Deployment.AdditionalMetadata.Annotations)
r.resource.SetAnnotations(annotations)
r.resource.Spec.Replicas = pointer.Int32(tenantControlPlane.Spec.ControlPlane.Deployment.Replicas)
r.resource.Spec.Selector = &metav1.LabelSelector{
MatchLabels: map[string]string{
"kamaji.clastix.io/soot": tenantControlPlane.GetName(),
},
}
r.resource.Spec.Template.ObjectMeta = metav1.ObjectMeta{
Labels: map[string]string{
"kamaji.clastix.io/soot": tenantControlPlane.GetName(),
"component.kamaji.clastix.io/api-server-certificate": func() (hash string) {
hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.APIServer.SecretName)
return
}(),
"component.kamaji.clastix.io/api-server-kubelet-client-certificate": func() (hash string) {
hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.APIServerKubeletClient.SecretName)
return
}(),
"component.kamaji.clastix.io/ca": func() (hash string) {
hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.CA.SecretName)
return
}(),
"component.kamaji.clastix.io/controller-manager-kubeconfig": func() (hash string) {
hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.KubeConfig.ControllerManager.SecretName)
return
}(),
"component.kamaji.clastix.io/etcd-ca-certificates": func() (hash string) {
hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.ETCD.CA.SecretName)
return
}(),
"component.kamaji.clastix.io/etcd-certificates": func() (hash string) {
hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.ETCD.APIServer.SecretName)
return
}(),
"component.kamaji.clastix.io/front-proxy-ca-certificate": func() (hash string) {
hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.FrontProxyCA.SecretName)
return
}(),
"component.kamaji.clastix.io/front-proxy-client-certificate": func() (hash string) {
hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.FrontProxyClient.SecretName)
return
}(),
"component.kamaji.clastix.io/service-account": func() (hash string) {
hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.SA.SecretName)
return
}(),
"component.kamaji.clastix.io/scheduler-kubeconfig": func() (hash string) {
hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.KubeConfig.Scheduler.SecretName)
return
}(),
},
}
r.resource.Spec.Template.Spec.Volumes = []corev1.Volume{
{
Name: "etc-kubernetes-pki",
VolumeSource: corev1.VolumeSource{
Projected: &corev1.ProjectedVolumeSource{
Sources: []corev1.VolumeProjection{
{
Secret: secretProjection(tenantControlPlane.Status.Certificates.APIServer.SecretName, constants.APIServerCertName, constants.APIServerKeyName),
},
{
Secret: secretProjection(tenantControlPlane.Status.Certificates.CA.SecretName, constants.CACertName, constants.CAKeyName),
},
{
Secret: secretProjection(tenantControlPlane.Status.Certificates.APIServerKubeletClient.SecretName, constants.APIServerKubeletClientCertName, constants.APIServerKubeletClientKeyName),
},
{
Secret: secretProjection(tenantControlPlane.Status.Certificates.FrontProxyCA.SecretName, constants.FrontProxyCACertName, constants.FrontProxyCAKeyName),
},
{
Secret: secretProjection(tenantControlPlane.Status.Certificates.FrontProxyClient.SecretName, constants.FrontProxyClientCertName, constants.FrontProxyClientKeyName),
},
{
Secret: secretProjection(tenantControlPlane.Status.Certificates.SA.SecretName, constants.ServiceAccountPublicKeyName, constants.ServiceAccountPrivateKeyName),
},
{
Secret: secretProjection(tenantControlPlane.Status.Certificates.ETCD.APIServer.SecretName, constants.APIServerEtcdClientCertName, constants.APIServerEtcdClientKeyName),
},
{
Secret: &corev1.SecretProjection{
LocalObjectReference: corev1.LocalObjectReference{
Name: tenantControlPlane.Status.Certificates.ETCD.CA.SecretName,
},
Items: []corev1.KeyToPath{
{
Key: constants.CACertName,
Path: constants.EtcdCACertName,
},
},
},
},
},
DefaultMode: pointer.Int32Ptr(420),
},
},
},
{
Name: "etc-ca-certificates",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: tenantControlPlane.Status.Certificates.CA.SecretName,
DefaultMode: pointer.Int32Ptr(420),
},
},
},
{
Name: "etc-ssl-certs",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: tenantControlPlane.Status.Certificates.CA.SecretName,
DefaultMode: pointer.Int32Ptr(420),
},
},
},
{
Name: "usr-share-ca-certificates",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: tenantControlPlane.Status.Certificates.CA.SecretName,
DefaultMode: pointer.Int32Ptr(420),
},
},
},
{
Name: "usr-local-share-ca-certificates",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: tenantControlPlane.Status.Certificates.CA.SecretName,
DefaultMode: pointer.Int32Ptr(420),
},
},
},
{
Name: "scheduler-kubeconfig",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: tenantControlPlane.Status.KubeConfig.Scheduler.SecretName,
DefaultMode: pointer.Int32Ptr(420),
},
},
},
{
Name: "controller-manager-kubeconfig",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: tenantControlPlane.Status.KubeConfig.ControllerManager.SecretName,
DefaultMode: pointer.Int32Ptr(420),
},
},
},
}
r.resource.Spec.Template.Spec.Containers = []corev1.Container{
{
Name: "kube-apiserver",
Image: fmt.Sprintf("k8s.gcr.io/kube-apiserver:%s", tenantControlPlane.Spec.Kubernetes.Version),
Command: []string{
"kube-apiserver",
"--allow-privileged=true",
"--authorization-mode=Node,RBAC",
fmt.Sprintf("--advertise-address=%s", address),
fmt.Sprintf("--client-ca-file=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.CACertName)),
fmt.Sprintf("--enable-admission-plugins=%s", strings.Join(tenantControlPlane.Spec.Kubernetes.AdmissionControllers.ToSlice(), ",")),
"--enable-bootstrap-token-auth=true",
fmt.Sprintf("--etcd-compaction-interval=%s", r.ETCDCompactionInterval),
fmt.Sprintf("--etcd-cafile=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.EtcdCACertName)),
fmt.Sprintf("--etcd-certfile=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.APIServerEtcdClientCertName)),
fmt.Sprintf("--etcd-keyfile=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.APIServerEtcdClientKeyName)),
fmt.Sprintf("--etcd-servers=%s", strings.Join(etcdEndpoints, ",")),
fmt.Sprintf("--etcd-prefix=/%s", tenantControlPlane.GetName()),
fmt.Sprintf("--service-cluster-ip-range=%s", tenantControlPlane.Spec.NetworkProfile.ServiceCIDR),
fmt.Sprintf("--kubelet-client-certificate=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.APIServerKubeletClientCertName)),
fmt.Sprintf("--kubelet-client-key=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.APIServerKubeletClientKeyName)),
"--kubelet-preferred-address-types=Hostname,InternalIP,ExternalIP",
fmt.Sprintf("--proxy-client-cert-file=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.FrontProxyClientCertName)),
fmt.Sprintf("--proxy-client-key-file=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.FrontProxyClientKeyName)),
"--requestheader-allowed-names=front-proxy-client",
"--requestheader-extra-headers-prefix=X-Remote-Extra-",
"--requestheader-group-headers=X-Remote-Group",
"--requestheader-username-headers=X-Remote-User",
fmt.Sprintf("--secure-port=%d", tenantControlPlane.Spec.NetworkProfile.Port),
fmt.Sprintf("--service-account-issuer=https://localhost:%d", tenantControlPlane.Spec.NetworkProfile.Port),
fmt.Sprintf("--service-account-key-file=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.ServiceAccountPublicKeyName)),
fmt.Sprintf("--service-account-signing-key-file=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.ServiceAccountPrivateKeyName)),
fmt.Sprintf("--tls-cert-file=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.APIServerCertName)),
fmt.Sprintf("--tls-private-key-file=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.APIServerKeyName)),
},
Resources: corev1.ResourceRequirements{
Limits: nil,
Requests: corev1.ResourceList{
corev1.ResourceCPU: quantity.MustParse("250m"),
},
},
LivenessProbe: &corev1.Probe{
ProbeHandler: corev1.ProbeHandler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/livez",
Port: intstr.FromInt(int(tenantControlPlane.Spec.NetworkProfile.Port)),
Scheme: corev1.URISchemeHTTPS,
},
},
InitialDelaySeconds: 0,
TimeoutSeconds: 1,
PeriodSeconds: 10,
SuccessThreshold: 1,
FailureThreshold: 3,
},
ReadinessProbe: &corev1.Probe{
ProbeHandler: corev1.ProbeHandler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/readyz",
Port: intstr.FromInt(int(tenantControlPlane.Spec.NetworkProfile.Port)),
Scheme: corev1.URISchemeHTTPS,
},
},
InitialDelaySeconds: 0,
TimeoutSeconds: 1,
PeriodSeconds: 10,
SuccessThreshold: 1,
FailureThreshold: 3,
},
StartupProbe: &corev1.Probe{
ProbeHandler: corev1.ProbeHandler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/livez",
Port: intstr.FromInt(int(tenantControlPlane.Spec.NetworkProfile.Port)),
Scheme: corev1.URISchemeHTTPS,
},
},
InitialDelaySeconds: 0,
TimeoutSeconds: 1,
PeriodSeconds: 10,
SuccessThreshold: 1,
FailureThreshold: 3,
},
TerminationMessagePath: "/dev/termination-log",
TerminationMessagePolicy: "File",
ImagePullPolicy: corev1.PullAlways,
VolumeMounts: []corev1.VolumeMount{
{
Name: "etc-kubernetes-pki",
ReadOnly: true,
MountPath: v1beta3.DefaultCertificatesDir,
},
{
Name: "etc-ca-certificates",
ReadOnly: true,
MountPath: "/etc/ca-certificates",
},
{
Name: "etc-ssl-certs",
ReadOnly: true,
MountPath: "/etc/ssl/certs",
},
{
Name: "usr-share-ca-certificates",
ReadOnly: true,
MountPath: "/usr/share/ca-certificates",
},
{
Name: "usr-local-share-ca-certificates",
ReadOnly: true,
MountPath: "/usr/local/share/ca-certificates",
},
},
},
{
Name: "kube-scheduler",
Image: fmt.Sprintf("k8s.gcr.io/kube-scheduler:%s", tenantControlPlane.Spec.Kubernetes.Version),
Command: []string{
"kube-scheduler",
"--authentication-kubeconfig=/etc/kubernetes/scheduler.conf",
"--authorization-kubeconfig=/etc/kubernetes/scheduler.conf",
"--bind-address=0.0.0.0",
"--kubeconfig=/etc/kubernetes/scheduler.conf",
"--leader-elect=true",
},
Resources: corev1.ResourceRequirements{
Limits: nil,
Requests: corev1.ResourceList{
corev1.ResourceCPU: quantity.MustParse("100m"),
},
},
VolumeMounts: []corev1.VolumeMount{
{
Name: "scheduler-kubeconfig",
ReadOnly: true,
MountPath: "/etc/kubernetes",
},
},
LivenessProbe: &corev1.Probe{
ProbeHandler: corev1.ProbeHandler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/healthz",
Port: intstr.FromInt(10259),
Scheme: corev1.URISchemeHTTPS,
},
},
InitialDelaySeconds: 0,
TimeoutSeconds: 1,
PeriodSeconds: 10,
SuccessThreshold: 1,
FailureThreshold: 3,
},
StartupProbe: &corev1.Probe{
ProbeHandler: corev1.ProbeHandler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/healthz",
Port: intstr.FromInt(10259),
Scheme: corev1.URISchemeHTTPS,
},
},
InitialDelaySeconds: 0,
TimeoutSeconds: 1,
PeriodSeconds: 10,
SuccessThreshold: 1,
FailureThreshold: 3,
},
TerminationMessagePath: "/dev/termination-log",
TerminationMessagePolicy: "File",
ImagePullPolicy: corev1.PullIfNotPresent,
},
{
Name: "kube-controller-manager",
Image: fmt.Sprintf("k8s.gcr.io/kube-controller-manager:%s", tenantControlPlane.Spec.Kubernetes.Version),
Command: []string{
"kube-controller-manager",
"--allocate-node-cidrs=true",
"--authentication-kubeconfig=/etc/kubernetes/controller-manager.conf",
"--authorization-kubeconfig=/etc/kubernetes/controller-manager.conf",
"--bind-address=0.0.0.0",
fmt.Sprintf("--client-ca-file=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.CACertName)),
fmt.Sprintf("--cluster-name=%s", tenantControlPlane.GetName()),
fmt.Sprintf("--cluster-signing-cert-file=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.CACertName)),
fmt.Sprintf("--cluster-signing-key-file=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.CAKeyName)),
"--controllers=*,bootstrapsigner,tokencleaner",
"--kubeconfig=/etc/kubernetes/controller-manager.conf",
"--leader-elect=true",
fmt.Sprintf("--service-cluster-ip-range=%s", tenantControlPlane.Spec.NetworkProfile.ServiceCIDR),
fmt.Sprintf("--cluster-cidr=%s", tenantControlPlane.Spec.NetworkProfile.PodCIDR),
fmt.Sprintf("--requestheader-client-ca-file=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.FrontProxyCACertName)),
fmt.Sprintf("--root-ca-file=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.CACertName)),
fmt.Sprintf("--service-account-private-key-file=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.ServiceAccountPrivateKeyName)),
"--use-service-account-credentials=true",
},
Resources: corev1.ResourceRequirements{
Limits: nil,
Requests: corev1.ResourceList{
corev1.ResourceCPU: quantity.MustParse("200m"),
},
},
VolumeMounts: []corev1.VolumeMount{
{
Name: "controller-manager-kubeconfig",
ReadOnly: true,
MountPath: "/etc/kubernetes",
},
{
Name: "etc-kubernetes-pki",
ReadOnly: true,
MountPath: v1beta3.DefaultCertificatesDir,
},
{
Name: "etc-ca-certificates",
ReadOnly: true,
MountPath: "/etc/ca-certificates",
},
{
Name: "etc-ssl-certs",
ReadOnly: true,
MountPath: "/etc/ssl/certs",
},
{
Name: "usr-share-ca-certificates",
ReadOnly: true,
MountPath: "/usr/share/ca-certificates",
},
{
Name: "usr-local-share-ca-certificates",
ReadOnly: true,
MountPath: "/usr/local/share/ca-certificates",
},
},
LivenessProbe: &corev1.Probe{
ProbeHandler: corev1.ProbeHandler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/healthz",
Port: intstr.FromInt(10257),
Scheme: corev1.URISchemeHTTPS,
},
},
InitialDelaySeconds: 0,
TimeoutSeconds: 1,
PeriodSeconds: 10,
SuccessThreshold: 1,
FailureThreshold: 3,
},
StartupProbe: &corev1.Probe{
ProbeHandler: corev1.ProbeHandler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/healthz",
Port: intstr.FromInt(10257),
Scheme: corev1.URISchemeHTTPS,
},
},
InitialDelaySeconds: 0,
TimeoutSeconds: 1,
PeriodSeconds: 10,
SuccessThreshold: 1,
FailureThreshold: 3,
},
TerminationMessagePath: "/dev/termination-log",
TerminationMessagePolicy: "File",
ImagePullPolicy: corev1.PullIfNotPresent,
},
}
r.resource.Spec.Strategy = appsv1.DeploymentStrategy{
Type: appsv1.RollingUpdateDeploymentStrategyType,
RollingUpdate: &appsv1.RollingUpdateDeployment{
MaxUnavailable: &maxUnavailable,
MaxSurge: &maxSurge,
},
}
return controllerutil.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
})
}
func (r *KubernetesDeploymentResource) GetName() string {
return r.Name
}
func (r *KubernetesDeploymentResource) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
switch {
case !r.isProgressingUpgrade():
tenantControlPlane.Status.Kubernetes.Version.Status = &kamajiv1alpha1.VersionReady
tenantControlPlane.Status.Kubernetes.Version.Version = tenantControlPlane.Spec.Kubernetes.Version
case r.isUpgrading(tenantControlPlane):
tenantControlPlane.Status.Kubernetes.Version.Status = &kamajiv1alpha1.VersionUpgrading
case r.isProvisioning(tenantControlPlane):
tenantControlPlane.Status.Kubernetes.Version.Status = &kamajiv1alpha1.VersionProvisioning
case r.isNotReady():
tenantControlPlane.Status.Kubernetes.Version.Status = &kamajiv1alpha1.VersionNotReady
}
tenantControlPlane.Status.Kubernetes.Deployment = kamajiv1alpha1.KubernetesDeploymentStatus{
DeploymentStatus: r.resource.Status,
Name: r.resource.GetName(),
Namespace: r.resource.GetNamespace(),
}
return nil
}
func (r *KubernetesDeploymentResource) isProgressingUpgrade() bool {
if r.resource.ObjectMeta.GetGeneration() != r.resource.Status.ObservedGeneration {
return true
}
if r.resource.Status.UnavailableReplicas > 0 {
return true
}
return false
}
func (r *KubernetesDeploymentResource) isUpgrading(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
return len(tenantControlPlane.Status.Kubernetes.Version.Version) > 0 &&
tenantControlPlane.Spec.Kubernetes.Version != tenantControlPlane.Status.Kubernetes.Version.Version &&
r.isProgressingUpgrade()
}
func (r *KubernetesDeploymentResource) isProvisioning(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
return len(tenantControlPlane.Status.Kubernetes.Version.Version) == 0
}
func (r *KubernetesDeploymentResource) isNotReady() bool {
return r.resource.Status.ReadyReplicas == 0
}

View File

@@ -0,0 +1,137 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"context"
"fmt"
networkingv1 "k8s.io/api/networking/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/utilities"
)
type KubernetesIngressResource struct {
resource *networkingv1.Ingress
Client client.Client
Name string
}
func (r *KubernetesIngressResource) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
return !(tenantControlPlane.Status.Kubernetes.Ingress.Name == r.resource.GetName() &&
tenantControlPlane.Status.Kubernetes.Ingress.Namespace == r.resource.GetNamespace())
}
func (r *KubernetesIngressResource) ShouldCleanup(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
return !tenantControlPlane.Spec.ControlPlane.Ingress.Enabled
}
func (r *KubernetesIngressResource) CleanUp(ctx context.Context) (bool, error) {
if err := r.Client.Delete(ctx, r.resource); err != nil {
if !k8serrors.IsNotFound(err) {
return false, err
}
return false, nil
}
return true, nil
}
func (r *KubernetesIngressResource) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
if tenantControlPlane.Spec.ControlPlane.Ingress.Enabled {
tenantControlPlane.Status.Kubernetes.Ingress.IngressStatus = r.resource.Status
tenantControlPlane.Status.Kubernetes.Ingress.Name = r.resource.GetName()
tenantControlPlane.Status.Kubernetes.Ingress.Namespace = r.resource.GetNamespace()
return nil
}
tenantControlPlane.Status.Kubernetes.Ingress = kamajiv1alpha1.KubernetesIngressStatus{}
return nil
}
func (r *KubernetesIngressResource) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
r.resource = &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: tenantControlPlane.GetName(),
Namespace: tenantControlPlane.GetNamespace(),
Labels: utilities.CommonLabels(tenantControlPlane.GetName()),
},
}
r.Name = "ingress"
return nil
}
func (r *KubernetesIngressResource) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
return controllerutil.CreateOrUpdate(ctx, r.Client, r.resource, func() error {
labels := utilities.MergeMaps(r.resource.GetLabels(), tenantControlPlane.Spec.ControlPlane.Ingress.AdditionalMetadata.Labels)
r.resource.SetLabels(labels)
annotations := utilities.MergeMaps(r.resource.GetAnnotations(), tenantControlPlane.Spec.ControlPlane.Ingress.AdditionalMetadata.Annotations)
r.resource.SetAnnotations(annotations)
if tenantControlPlane.Spec.ControlPlane.Ingress.IngressClassName != "" {
r.resource.Spec.IngressClassName = &tenantControlPlane.Spec.ControlPlane.Ingress.IngressClassName
}
var rule networkingv1.IngressRule
if len(r.resource.Spec.Rules) > 0 {
rule = r.resource.Spec.Rules[0]
}
var path networkingv1.HTTPIngressPath
if rule.HTTP != nil && len(rule.HTTP.Paths) > 0 {
path = rule.HTTP.Paths[0]
}
path.Path = "/"
path.PathType = (*networkingv1.PathType)(pointer.StringPtr(string(networkingv1.PathTypePrefix)))
if path.Backend.Service == nil {
path.Backend.Service = &networkingv1.IngressServiceBackend{}
}
if tenantControlPlane.Status.Kubernetes.Service.Name == "" ||
tenantControlPlane.Status.Kubernetes.Service.Port == 0 {
return fmt.Errorf("ingress cannot be configured yet")
}
path.Backend.Service.Name = tenantControlPlane.Status.Kubernetes.Service.Name
path.Backend.Service.Port.Number = tenantControlPlane.Status.Kubernetes.Service.Port
if rule.HTTP == nil {
rule.HTTP = &networkingv1.HTTPIngressRuleValue{
Paths: []networkingv1.HTTPIngressPath{
{},
},
}
}
rule.HTTP.Paths[0] = path
rule.Host = tenantControlPlane.Spec.ControlPlane.Ingress.Hostname
if rule.Host == "" {
rule.Host = fmt.Sprintf("%s.%s.%s", tenantControlPlane.GetName(), tenantControlPlane.GetNamespace(), tenantControlPlane.Spec.NetworkProfile.Domain)
}
r.resource.Spec.Rules = []networkingv1.IngressRule{
rule,
}
return controllerutil.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
})
}
func (r *KubernetesIngressResource) GetName() string {
return r.Name
}

View File

@@ -0,0 +1,117 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"context"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/utilities"
)
// KubernetesServiceResource must be the first Resource processed by the TenantControlPlane:
// when a TenantControlPlan is expecting a dynamic IP address, the Service will get it from the controller-manager.
type KubernetesServiceResource struct {
resource *corev1.Service
Client client.Client
Name string
}
func (r *KubernetesServiceResource) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
return tenantControlPlane.Status.Kubernetes.Service.Name != r.resource.GetName() ||
tenantControlPlane.Status.Kubernetes.Service.Namespace != r.resource.GetNamespace() ||
tenantControlPlane.Status.Kubernetes.Service.Port != r.resource.Spec.Ports[0].Port
}
func (r *KubernetesServiceResource) ShouldCleanup(plane *kamajiv1alpha1.TenantControlPlane) bool {
return false
}
func (r *KubernetesServiceResource) CleanUp(ctx context.Context) (bool, error) {
return false, nil
}
func (r *KubernetesServiceResource) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
tenantControlPlane.Status.Kubernetes.Service.ServiceStatus = r.resource.Status
tenantControlPlane.Status.Kubernetes.Service.Name = r.resource.GetName()
tenantControlPlane.Status.Kubernetes.Service.Namespace = r.resource.GetNamespace()
tenantControlPlane.Status.Kubernetes.Service.Port = r.resource.Spec.Ports[0].Port
return nil
}
func (r *KubernetesServiceResource) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
r.resource = &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: tenantControlPlane.GetName(),
Namespace: tenantControlPlane.GetNamespace(),
Labels: utilities.CommonLabels(tenantControlPlane.GetName()),
},
}
r.Name = "service"
return nil
}
func (r *KubernetesServiceResource) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
// We don't need to check error here: in case of dynamic external IP, the Service must be created in advance.
// After that, the specific cloud controller-manager will provide an IP that will be then used.
address, _ := tenantControlPlane.GetAddress(ctx, r.Client)
return controllerutil.CreateOrUpdate(ctx, r.Client, r.resource, func() error {
var servicePort corev1.ServicePort
if len(r.resource.Spec.Ports) > 0 {
servicePort = r.resource.Spec.Ports[0]
}
servicePort.Protocol = corev1.ProtocolTCP
servicePort.Port = tenantControlPlane.Spec.NetworkProfile.Port
servicePort.TargetPort = intstr.FromInt(int(tenantControlPlane.Spec.NetworkProfile.Port))
r.resource.Spec.Ports = []corev1.ServicePort{servicePort}
r.resource.Spec.Selector = map[string]string{
"kamaji.clastix.io/soot": tenantControlPlane.GetName(),
}
labels := utilities.MergeMaps(r.resource.GetLabels(), tenantControlPlane.Spec.ControlPlane.Service.AdditionalMetadata.Labels)
r.resource.SetLabels(labels)
annotations := utilities.MergeMaps(r.resource.GetAnnotations(), tenantControlPlane.Spec.ControlPlane.Service.AdditionalMetadata.Annotations)
r.resource.SetAnnotations(annotations)
switch tenantControlPlane.Spec.ControlPlane.Service.ServiceType {
case kamajiv1alpha1.ServiceTypeLoadBalancer:
r.resource.Spec.Type = corev1.ServiceTypeLoadBalancer
if len(address) > 0 {
r.resource.Spec.LoadBalancerIP = address
}
case kamajiv1alpha1.ServiceTypeNodePort:
r.resource.Spec.Type = corev1.ServiceTypeNodePort
r.resource.Spec.Ports[0].NodePort = tenantControlPlane.Spec.NetworkProfile.Port
if tenantControlPlane.Spec.NetworkProfile.AllowAddressAsExternalIP && len(address) > 0 {
r.resource.Spec.ExternalIPs = []string{address}
}
default:
r.resource.Spec.Type = corev1.ServiceTypeClusterIP
if tenantControlPlane.Spec.NetworkProfile.AllowAddressAsExternalIP && len(address) > 0 {
r.resource.Spec.ExternalIPs = []string{address}
}
}
return controllerutil.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
})
}
func (r *KubernetesServiceResource) GetName() string {
return r.Name
}

View File

@@ -0,0 +1,142 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"context"
"fmt"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/kubeadm"
"github.com/clastix/kamaji/internal/utilities"
)
type KubeadmConfigResource struct {
resource *corev1.ConfigMap
Client client.Client
Scheme *runtime.Scheme
Log logr.Logger
Name string
Port int32
Domain string
PodCIDR string
ServiceCIDR string
KubernetesVersion string
ETCDs []string
ETCDCompactionInterval string
TmpDirectory string
}
func (r *KubeadmConfigResource) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
address, err := tenantControlPlane.GetAddress(ctx, r.Client)
if err != nil {
return true
}
return !(tenantControlPlane.Status.KubeadmConfig.ResourceVersion == r.resource.ObjectMeta.ResourceVersion &&
tenantControlPlane.Status.KubeadmConfig.ConfigmapName == r.resource.GetName() &&
tenantControlPlane.Status.ControlPlaneEndpoint == r.getControlPlaneEndpoint(tenantControlPlane, address))
}
func (r *KubeadmConfigResource) ShouldCleanup(plane *kamajiv1alpha1.TenantControlPlane) bool {
return false
}
func (r *KubeadmConfigResource) CleanUp(ctx context.Context) (bool, error) {
return false, nil
}
func (r *KubeadmConfigResource) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
r.resource = &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: r.getPrefixedName(tenantControlPlane),
Namespace: tenantControlPlane.GetNamespace(),
},
}
return nil
}
func (r *KubeadmConfigResource) getPrefixedName(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) string {
return utilities.AddTenantPrefix(r.Name, tenantControlPlane)
}
func (r *KubeadmConfigResource) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
address, err := tenantControlPlane.GetAddress(ctx, r.Client)
if err != nil {
return controllerutil.OperationResultNone, err
}
return controllerutil.CreateOrUpdate(ctx, r.Client, r.resource, r.mutate(tenantControlPlane, address))
}
func (r *KubeadmConfigResource) GetName() string {
return r.Name
}
func (r *KubeadmConfigResource) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
address, _ := tenantControlPlane.GetAddress(ctx, r.Client)
tenantControlPlane.Status.KubeadmConfig.LastUpdate = metav1.Now()
tenantControlPlane.Status.KubeadmConfig.ResourceVersion = r.resource.ObjectMeta.ResourceVersion
tenantControlPlane.Status.KubeadmConfig.ConfigmapName = r.resource.GetName()
tenantControlPlane.Status.ControlPlaneEndpoint = r.getControlPlaneEndpoint(tenantControlPlane, address)
return nil
}
func (r *KubeadmConfigResource) getControlPlaneEndpoint(tenantControlPlane *kamajiv1alpha1.TenantControlPlane, address string) string {
if !tenantControlPlane.Spec.ControlPlane.Ingress.Enabled {
return fmt.Sprintf("%s:%d", address, tenantControlPlane.Spec.NetworkProfile.Port)
}
if tenantControlPlane.Spec.ControlPlane.Ingress.Hostname != "" {
return tenantControlPlane.Spec.ControlPlane.Ingress.Hostname
}
return getTenantControllerExternalFQDN(*tenantControlPlane)
}
func (r *KubeadmConfigResource) mutate(tenantControlPlane *kamajiv1alpha1.TenantControlPlane, address string) controllerutil.MutateFn {
return func() error {
r.resource.SetLabels(utilities.KamajiLabels())
params := kubeadm.Parameters{
TenantControlPlaneName: tenantControlPlane.GetName(),
TenantControlPlaneNamespace: tenantControlPlane.GetNamespace(),
TenantControlPlaneEndpoint: r.getControlPlaneEndpoint(tenantControlPlane, address),
TenantControlPlaneAddress: address,
TenantControlPlanePort: r.Port,
TenantControlPlaneDomain: r.Domain,
TenantControlPlanePodCIDR: r.PodCIDR,
TenantControlPlaneServiceCIDR: r.ServiceCIDR,
TenantControlPlaneVersion: r.KubernetesVersion,
ETCDs: r.ETCDs,
ETCDCompactionInterval: r.ETCDCompactionInterval,
CertificatesDir: r.TmpDirectory,
}
config := kubeadm.CreateKubeadmInitConfiguration(params)
data, err := kubeadm.GetKubeadmInitConfigurationMap(config)
if err != nil {
return err
}
r.resource.Data = data
if err := ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme()); err != nil {
return err
}
return nil
}
}

View File

@@ -0,0 +1,271 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"context"
"fmt"
"time"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8stypes "k8s.io/apimachinery/pkg/types"
clientset "k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
bootstraptokenv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/kubeadm"
kubeconfigutil "github.com/clastix/kamaji/internal/kubeconfig"
)
const kubeadmPhaseTimeout = 10 // seconds
type KubeadmPhase int
const (
PhaseUploadConfigKubeadm KubeadmPhase = iota
PhaseUploadConfigKubelet
PhaseAddonCoreDNS
PhaseAddonKubeProxy
PhaseBootstrapToken
)
func (d KubeadmPhase) String() string {
return [...]string{"PhaseUploadConfigKubeadm", "PhaseUploadConfigKubelet", "PhaseAddonCoreDNS", "PhaseAddonKubeProxy", "PhaseBootstrapToken"}[d]
}
const (
kubeconfigAdminKeyName = "admin.conf"
)
type KubeadmPhaseResource struct {
Client client.Client
Log logr.Logger
Name string
KubeadmPhase KubeadmPhase
kubeadmConfigResourceVersion string
}
func (r *KubeadmPhaseResource) isStatusEqual(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
status, err := r.getStatus(tenantControlPlane)
if err != nil {
return true
}
return status.KubeadmConfigResourceVersion == r.kubeadmConfigResourceVersion
}
func (r *KubeadmPhaseResource) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
return !r.isStatusEqual(tenantControlPlane)
}
func (r *KubeadmPhaseResource) ShouldCleanup(plane *kamajiv1alpha1.TenantControlPlane) bool {
return false
}
func (r *KubeadmPhaseResource) CleanUp(ctx context.Context) (bool, error) {
return false, nil
}
func (r *KubeadmPhaseResource) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
return nil
}
func (r *KubeadmPhaseResource) getKubeadmPhaseFunction() (func(clientset.Interface, *kubeadm.Configuration) error, error) {
switch r.KubeadmPhase {
case PhaseUploadConfigKubeadm:
return kubeadm.UploadKubeadmConfig, nil
case PhaseUploadConfigKubelet:
return kubeadm.UploadKubeletConfig, nil
case PhaseAddonCoreDNS:
return kubeadm.CoreDNSAddon, nil
case PhaseAddonKubeProxy:
return kubeadm.KubeProxyAddon, nil
case PhaseBootstrapToken:
return func(client clientset.Interface, config *kubeadm.Configuration) error {
bootstrapTokensEnrichment(config.InitConfiguration.BootstrapTokens)
return kubeadm.BootstrapToken(client, config)
}, nil
default:
return nil, fmt.Errorf("no available functionality for phase %s", r.KubeadmPhase)
}
}
func bootstrapTokensEnrichment(bootstrapTokens []bootstraptokenv1.BootstrapToken) {
var bootstrapToken bootstraptokenv1.BootstrapToken
if len(bootstrapTokens) > 0 {
bootstrapToken = bootstrapTokens[0]
}
enrichBootstrapToken(&bootstrapToken)
bootstrapTokens[0] = bootstrapToken
}
func enrichBootstrapToken(bootstrapToken *bootstraptokenv1.BootstrapToken) {
if bootstrapToken.Token == nil {
bootstrapToken.Token = &bootstraptokenv1.BootstrapTokenString{}
}
if bootstrapToken.Token.ID == "" {
bootstrapToken.Token.ID = fmt.Sprintf("%s.%s", randomString(6), randomString(16))
}
}
func (r *KubeadmPhaseResource) GetClient() client.Client {
return r.Client
}
func (r *KubeadmPhaseResource) GetTmpDirectory() string {
return ""
}
func (r *KubeadmPhaseResource) GetName() string {
return r.Name
}
func (r *KubeadmPhaseResource) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
status, err := r.getStatus(tenantControlPlane)
if err != nil {
return err
}
status.LastUpdate = metav1.Now()
status.KubeadmConfigResourceVersion = r.kubeadmConfigResourceVersion
return nil
}
func (r *KubeadmPhaseResource) getStatus(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (*kamajiv1alpha1.KubeadmPhaseStatus, error) {
switch r.KubeadmPhase {
case PhaseUploadConfigKubeadm:
return &tenantControlPlane.Status.KubeadmPhase.UploadConfigKubeadm, nil
case PhaseUploadConfigKubelet:
return &tenantControlPlane.Status.KubeadmPhase.UploadConfigKubelet, nil
case PhaseAddonCoreDNS:
return &tenantControlPlane.Status.KubeadmPhase.AddonCoreDNS, nil
case PhaseAddonKubeProxy:
return &tenantControlPlane.Status.KubeadmPhase.AddonKubeProxy, nil
case PhaseBootstrapToken:
return &tenantControlPlane.Status.KubeadmPhase.BootstrapToken, nil
default:
return nil, fmt.Errorf("%s is not a right kubeadm phase", r.KubeadmPhase)
}
}
func (r *KubeadmPhaseResource) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
return r.reconcile(ctx, tenantControlPlane)
}
func (r *KubeadmPhaseResource) reconcile(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
config, resourceVersion, err := getKubeadmConfiguration(ctx, r, tenantControlPlane)
if err != nil {
return controllerutil.OperationResultNone, err
}
kubeconfig, err := r.getKubeconfig(ctx, tenantControlPlane)
if err != nil {
return controllerutil.OperationResultNone, err
}
config.Kubeconfig = *kubeconfig
config.Parameters = kubeadm.Parameters{
TenantControlPlaneName: tenantControlPlane.GetName(),
TenantDNSServiceIPs: tenantControlPlane.Spec.NetworkProfile.DNSServiceIPs,
TenantControlPlaneVersion: tenantControlPlane.Spec.Kubernetes.Version,
TenantControlPlanePodCIDR: tenantControlPlane.Spec.NetworkProfile.PodCIDR,
TenantControlPlaneAddress: tenantControlPlane.Spec.NetworkProfile.Address,
TenantControlPlanePort: tenantControlPlane.Spec.NetworkProfile.Port,
TenantControlPlaneCGroupDriver: tenantControlPlane.Spec.Kubernetes.Kubelet.CGroupFS.String(),
}
status, err := r.getStatus(tenantControlPlane)
if err != nil {
return controllerutil.OperationResultNone, err
}
if resourceVersion == status.KubeadmConfigResourceVersion {
r.kubeadmConfigResourceVersion = resourceVersion
return controllerutil.OperationResultNone, nil
}
client, err := r.getRESTClient(ctx, tenantControlPlane)
if err != nil {
return controllerutil.OperationResultNone, err
}
fun, err := r.getKubeadmPhaseFunction()
if err != nil {
return controllerutil.OperationResultNone, err
}
if err = fun(client, config); err != nil {
return controllerutil.OperationResultNone, err
}
r.kubeadmConfigResourceVersion = resourceVersion
if status.LastUpdate.IsZero() {
return controllerutil.OperationResultCreated, nil
}
return controllerutil.OperationResultUpdated, nil
}
func (r *KubeadmPhaseResource) getKubeconfigSecret(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (*corev1.Secret, error) {
kubeconfigSecretName := tenantControlPlane.Status.KubeConfig.Admin.SecretName
namespacedName := k8stypes.NamespacedName{Namespace: tenantControlPlane.GetNamespace(), Name: kubeconfigSecretName}
secret := &corev1.Secret{}
if err := r.Client.Get(ctx, namespacedName, secret); err != nil {
return nil, err
}
return secret, nil
}
func (r *KubeadmPhaseResource) getKubeconfig(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (*kubeconfigutil.Kubeconfig, error) {
secretKubeconfig, err := r.getKubeconfigSecret(ctx, tenantControlPlane)
if err != nil {
return nil, err
}
bytes, ok := secretKubeconfig.Data[kubeconfigAdminKeyName]
if !ok {
return nil, fmt.Errorf("%s is not into kubeconfig secret", kubeconfigAdminKeyName)
}
return kubeconfigutil.GetKubeconfigFromBytes(bytes)
}
func (r *KubeadmPhaseResource) getRESTClient(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (*clientset.Clientset, error) {
config, err := r.getRESTClientConfig(ctx, tenantControlPlane)
if err != nil {
return nil, err
}
return clientset.NewForConfig(config)
}
func (r *KubeadmPhaseResource) getRESTClientConfig(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (*restclient.Config, error) {
kubeconfig, err := r.getKubeconfig(ctx, tenantControlPlane)
if err != nil {
return nil, err
}
config := &restclient.Config{
Host: fmt.Sprintf("https://%s:%d", getTenantControllerInternalFQDN(*tenantControlPlane), tenantControlPlane.Spec.NetworkProfile.Port),
TLSClientConfig: restclient.TLSClientConfig{
CAData: kubeconfig.Clusters[0].Cluster.CertificateAuthorityData,
CertData: kubeconfig.AuthInfos[0].AuthInfo.ClientCertificateData,
KeyData: kubeconfig.AuthInfos[0].AuthInfo.ClientKeyData,
},
Timeout: time.Second * kubeadmPhaseTimeout,
}
return config, nil
}

View File

@@ -0,0 +1,190 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"context"
"fmt"
"time"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
k8stypes "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/version"
clientset "k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/upgrade"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
kubeconfigutil "github.com/clastix/kamaji/internal/kubeconfig"
kamajiupgrade "github.com/clastix/kamaji/internal/upgrade"
)
type KubernetesUpgrade struct {
Name string
Client client.Client
upgrade upgrade.Upgrade
inProgress bool
}
func (k *KubernetesUpgrade) Define(_ context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
k.upgrade = upgrade.Upgrade{
Before: upgrade.ClusterState{
KubeVersion: tenantControlPlane.Status.Kubernetes.Version.Version,
},
After: upgrade.ClusterState{
KubeVersion: tenantControlPlane.Spec.Kubernetes.Version,
},
}
return nil
}
func (k *KubernetesUpgrade) ShouldCleanup(*kamajiv1alpha1.TenantControlPlane) bool {
return false
}
func (k *KubernetesUpgrade) CleanUp(context.Context) (bool, error) {
return false, nil
}
func (k *KubernetesUpgrade) CreateOrUpdate(ctx context.Context, plane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
// A new installation, no need to upgrade
if len(plane.Status.Kubernetes.Version.Version) == 0 {
k.inProgress = false
return controllerutil.OperationResultNone, nil
}
// No version change, no need to upgrade
if plane.Status.Kubernetes.Version.Version == plane.Spec.Kubernetes.Version {
k.inProgress = false
return controllerutil.OperationResultNone, nil
}
// An upgrade is in progress, let it go
if status := plane.Status.Kubernetes.Version.Status; status != nil && *status == kamajiv1alpha1.VersionUpgrading {
return controllerutil.OperationResultNone, nil
}
// Checking if the upgrade is allowed, or not
restClient, err := k.getRESTClient(ctx, plane)
if err != nil {
return controllerutil.OperationResultNone, errors.Wrap(err, "cannot create REST client required for Kubernetes upgrade plan")
}
versionGetter := kamajiupgrade.NewKamajiKubeVersionGetter(restClient)
if _, err = upgrade.GetAvailableUpgrades(versionGetter, false, false, true, restClient, ""); err != nil {
return controllerutil.OperationResultNone, errors.Wrap(err, "cannot retrieve available Upgrades for Kubernetes upgrade plan")
}
if err = k.isUpgradable(); err != nil {
return controllerutil.OperationResultNone, fmt.Errorf("the required upgrade plan is not available")
}
k.inProgress = true
return controllerutil.OperationResultNone, nil
}
func (k *KubernetesUpgrade) GetName() string {
return k.Name
}
func (k *KubernetesUpgrade) ShouldStatusBeUpdated(context.Context, *kamajiv1alpha1.TenantControlPlane) bool {
return k.inProgress
}
func (k *KubernetesUpgrade) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
if k.inProgress {
tenantControlPlane.Status.Kubernetes.Version.Status = &kamajiv1alpha1.VersionUpgrading
}
if tenantControlPlane.Spec.Kubernetes.Version == tenantControlPlane.Status.Kubernetes.Version.Version {
tenantControlPlane.Status.Kubernetes.Version.Status = &kamajiv1alpha1.VersionReady
}
return nil
}
func (k *KubernetesUpgrade) getKubeconfigSecret(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (*corev1.Secret, error) {
kubeconfigSecretName := tenantControlPlane.Status.KubeConfig.Admin.SecretName
namespacedName := k8stypes.NamespacedName{Namespace: tenantControlPlane.GetNamespace(), Name: kubeconfigSecretName}
secret := &corev1.Secret{}
if err := k.Client.Get(ctx, namespacedName, secret); err != nil {
return nil, err
}
return secret, nil
}
func (k *KubernetesUpgrade) getKubeconfig(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (*kubeconfigutil.Kubeconfig, error) {
secretKubeconfig, err := k.getKubeconfigSecret(ctx, tenantControlPlane)
if err != nil {
return nil, err
}
bytes, ok := secretKubeconfig.Data[kubeconfigAdminKeyName]
if !ok {
return nil, fmt.Errorf("%s is not into kubeconfig secret", kubeconfigAdminKeyName)
}
return kubeconfigutil.GetKubeconfigFromBytes(bytes)
}
func (k *KubernetesUpgrade) getRESTClient(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (*clientset.Clientset, error) {
config, err := k.getRESTClientConfig(ctx, tenantControlPlane)
if err != nil {
return nil, err
}
return clientset.NewForConfig(config)
}
func (k *KubernetesUpgrade) getRESTClientConfig(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (*restclient.Config, error) {
kubeconfig, err := k.getKubeconfig(ctx, tenantControlPlane)
if err != nil {
return nil, err
}
config := &restclient.Config{
Host: fmt.Sprintf("https://%s:%d", getTenantControllerInternalFQDN(*tenantControlPlane), tenantControlPlane.Spec.NetworkProfile.Port),
TLSClientConfig: restclient.TLSClientConfig{
CAData: kubeconfig.Clusters[0].Cluster.CertificateAuthorityData,
CertData: kubeconfig.AuthInfos[0].AuthInfo.ClientCertificateData,
KeyData: kubeconfig.AuthInfos[0].AuthInfo.ClientKeyData,
},
Timeout: time.Second * kubeadmPhaseTimeout,
}
return config, nil
}
func (k *KubernetesUpgrade) isUpgradable() error {
newK8sVersion, err := version.ParseSemantic(k.upgrade.After.KubeVersion)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to parse normalized version %q as a semantic version", k.upgrade.After.KubeVersion))
}
oldK8sVersion, err := version.ParseSemantic(k.upgrade.Before.KubeVersion)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to parse normalized version %q as a semantic version", k.upgrade.After.KubeVersion))
}
if newK8sVersion.Minor() < oldK8sVersion.Minor() {
return fmt.Errorf("cannot downgrade to a previous minor version of Kubernetes")
}
// Patch upgrades are allowed
if newK8sVersion.Minor() == oldK8sVersion.Minor() {
return nil
}
// Following minor release upgrades are allowed
if newK8sVersion.Minor() > oldK8sVersion.WithMinor(oldK8sVersion.Minor()+1).Minor() {
return nil
}
return fmt.Errorf("an upgrade to a non consecutive Kubernetes minor release is forbidden")
}

View File

@@ -0,0 +1,186 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"context"
"fmt"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
k8stypes "k8s.io/apimachinery/pkg/types"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/kubeadm"
"github.com/clastix/kamaji/internal/utilities"
)
const (
AdminKubeConfigFileName = kubeadmconstants.AdminKubeConfigFileName
ControllerManagerKubeConfigFileName = kubeadmconstants.ControllerManagerKubeConfigFileName
SchedulerKubeConfigFileName = kubeadmconstants.SchedulerKubeConfigFileName
localhost = "127.0.0.1"
)
type KubeconfigResource struct {
resource *corev1.Secret
Client client.Client
Scheme *runtime.Scheme
Log logr.Logger
Name string
KubeConfigFileName string
TmpDirectory string
}
func (r *KubeconfigResource) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
return false
}
func (r *KubeconfigResource) ShouldCleanup(plane *kamajiv1alpha1.TenantControlPlane) bool {
return false
}
func (r *KubeconfigResource) CleanUp(ctx context.Context) (bool, error) {
return false, nil
}
func (r *KubeconfigResource) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
r.resource = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: r.getPrefixedName(tenantControlPlane),
Namespace: tenantControlPlane.GetNamespace(),
},
}
return nil
}
func (r *KubeconfigResource) getPrefixedName(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) string {
return utilities.AddTenantPrefix(r.Name, tenantControlPlane)
}
func (r *KubeconfigResource) GetClient() client.Client {
return r.Client
}
func (r *KubeconfigResource) GetTmpDirectory() string {
return r.TmpDirectory
}
func (r *KubeconfigResource) GetName() string {
return r.Name
}
func (r *KubeconfigResource) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
status, err := r.getKubeconfigStatus(tenantControlPlane)
if err != nil {
return err
}
status.LastUpdate = metav1.Now()
status.SecretName = r.resource.GetName()
return nil
}
func (r *KubeconfigResource) getKubeconfigStatus(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (*kamajiv1alpha1.KubeconfigStatus, error) {
switch r.KubeConfigFileName {
case kubeadmconstants.AdminKubeConfigFileName:
return &tenantControlPlane.Status.KubeConfig.Admin, nil
case kubeadmconstants.ControllerManagerKubeConfigFileName:
return &tenantControlPlane.Status.KubeConfig.ControllerManager, nil
case kubeadmconstants.SchedulerKubeConfigFileName:
return &tenantControlPlane.Status.KubeConfig.Scheduler, nil
default:
return nil, fmt.Errorf("kubeconfigfilename %s is not a right name", r.KubeConfigFileName)
}
}
func (r *KubeconfigResource) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
return controllerutil.CreateOrUpdate(ctx, r.Client, r.resource, r.mutate(ctx, tenantControlPlane))
}
func (r *KubeconfigResource) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn {
return func() error {
latestConfigRV := getLatestConfigRV(*tenantControlPlane)
actualConfigRV := r.resource.GetLabels()["latest-config-rv"]
if latestConfigRV == actualConfigRV {
if kubeadm.IsKubeconfigValid(r.resource.Data[r.KubeConfigFileName]) {
return nil
}
}
config, _, err := getKubeadmConfiguration(ctx, r, tenantControlPlane)
if err != nil {
return err
}
if err := r.customizeConfig(config); err != nil {
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 {
return err
}
kubeconfig, err := kubeadm.CreateKubeconfig(
r.KubeConfigFileName,
kubeadm.CertificatePrivateKeyPair{
Certificate: apiServerCertificatesSecret.Data[kubeadmconstants.CACertName],
PrivateKey: apiServerCertificatesSecret.Data[kubeadmconstants.CAKeyName],
},
config,
)
if err != nil {
return err
}
r.resource.Data = map[string][]byte{
r.KubeConfigFileName: kubeconfig,
}
r.resource.SetLabels(utilities.MergeMaps(
utilities.KamajiLabels(),
map[string]string{
"latest-config-rv": latestConfigRV,
"kamaji.clastix.io/name": tenantControlPlane.GetName(),
"kamaji.clastix.io/component": r.GetName(),
},
))
return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
}
}
func (r *KubeconfigResource) customizeConfig(config *kubeadm.Configuration) error {
switch r.KubeConfigFileName {
case kubeadmconstants.AdminKubeConfigFileName:
return r.ingressAsAdvertiseAddress(config)
case kubeadmconstants.ControllerManagerKubeConfigFileName:
return r.localhostAsAdvertiseAddress(config)
case kubeadmconstants.SchedulerKubeConfigFileName:
return r.localhostAsAdvertiseAddress(config)
default:
return nil
}
}
func (r *KubeconfigResource) ingressAsAdvertiseAddress(config *kubeadm.Configuration) error {
config.InitConfiguration.LocalAPIEndpoint.BindPort = defaultIngressPort
return nil
}
func (r *KubeconfigResource) localhostAsAdvertiseAddress(config *kubeadm.Configuration) error {
config.InitConfiguration.LocalAPIEndpoint.AdvertiseAddress = localhost
return nil
}

View File

@@ -0,0 +1,96 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"context"
corev1 "k8s.io/api/core/v1"
k8stypes "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/kubeadm"
)
type Resource interface {
Define(context.Context, *kamajiv1alpha1.TenantControlPlane) error
ShouldCleanup(*kamajiv1alpha1.TenantControlPlane) bool
CleanUp(context.Context) (bool, error)
CreateOrUpdate(context.Context, *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error)
GetName() string
ShouldStatusBeUpdated(context.Context, *kamajiv1alpha1.TenantControlPlane) bool
UpdateTenantControlPlaneStatus(context.Context, *kamajiv1alpha1.TenantControlPlane) error
}
type DeleteableResource interface {
Delete(context.Context, *kamajiv1alpha1.TenantControlPlane) error
}
type KubeadmResource interface {
Resource
GetClient() client.Client
GetTmpDirectory() string
}
type HandlerConfig struct {
Resource Resource
TenantControlPlane *kamajiv1alpha1.TenantControlPlane
}
// Handle handles the given resource and returns a boolean to say if the tenantControlPlane has been modified.
func Handle(ctx context.Context, resource Resource, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
if err := resource.Define(ctx, tenantControlPlane); err != nil {
return "", err
}
if !resource.ShouldCleanup(tenantControlPlane) {
return createOrUpdate(ctx, resource, tenantControlPlane)
}
cleanUp, err := resource.CleanUp(ctx)
if err != nil {
return controllerutil.OperationResultNone, err
}
if cleanUp {
return controllerutil.OperationResultUpdated, nil
}
return controllerutil.OperationResultNone, err
}
func createOrUpdate(ctx context.Context, resource Resource, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
result, err := resource.CreateOrUpdate(ctx, tenantControlPlane)
if err != nil {
return "", err
}
if result == controllerutil.OperationResultNone && resource.ShouldStatusBeUpdated(ctx, tenantControlPlane) {
return controllerutil.OperationResultUpdatedStatusOnly, nil
}
return result, nil
}
func getKubeadmConfiguration(ctx context.Context, r KubeadmResource, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (*kubeadm.Configuration, string, error) {
var configmap corev1.ConfigMap
namespacedName := k8stypes.NamespacedName{Namespace: tenantControlPlane.GetNamespace(), Name: tenantControlPlane.Status.KubeadmConfig.ConfigmapName}
if err := r.GetClient().Get(ctx, namespacedName, &configmap); err != nil {
return nil, "", err
}
config, err := kubeadm.GetKubeadmInitConfigurationFromMap(configmap.Data)
if err != nil {
return nil, "", err
}
tmpDirectory := r.GetTmpDirectory()
if tmpDirectory != "" {
config.InitConfiguration.ClusterConfiguration.CertificatesDir = tmpDirectory
}
return config, configmap.ObjectMeta.ResourceVersion, nil
}

View File

@@ -0,0 +1,124 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package resources
import (
"context"
"fmt"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/kubeadm"
"github.com/clastix/kamaji/internal/utilities"
)
type SACertificate struct {
resource *corev1.Secret
Client client.Client
Log logr.Logger
Name string
TmpDirectory string
}
func (r *SACertificate) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool {
return tenantControlPlane.Status.Certificates.SA.SecretName != r.resource.GetName()
}
func (r *SACertificate) ShouldCleanup(plane *kamajiv1alpha1.TenantControlPlane) bool {
return false
}
func (r *SACertificate) CleanUp(ctx context.Context) (bool, error) {
return false, nil
}
func (r *SACertificate) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
r.resource = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: r.getPrefixedName(tenantControlPlane),
Namespace: tenantControlPlane.GetNamespace(),
},
}
return nil
}
func (r *SACertificate) getPrefixedName(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) string {
return utilities.AddTenantPrefix(r.Name, tenantControlPlane)
}
func (r *SACertificate) GetClient() client.Client {
return r.Client
}
func (r *SACertificate) GetTmpDirectory() string {
return r.TmpDirectory
}
func (r *SACertificate) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
return controllerutil.CreateOrUpdate(ctx, r.Client, r.resource, r.mutate(ctx, tenantControlPlane))
}
func (r *SACertificate) GetName() string {
return r.Name
}
func (r *SACertificate) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
tenantControlPlane.Status.Certificates.SA.LastUpdate = metav1.Now()
tenantControlPlane.Status.Certificates.SA.SecretName = r.resource.GetName()
return nil
}
func (r *SACertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn {
return func() error {
latestConfigRV := getLatestConfigRV(*tenantControlPlane)
actualConfigRV := r.resource.GetLabels()["latest-config-rv"]
if latestConfigRV == actualConfigRV {
isValid, err := kubeadm.IsPublicKeyPrivateKeyPairValid(
r.resource.Data[kubeadmconstants.ServiceAccountPublicKeyName],
r.resource.Data[kubeadmconstants.ServiceAccountPrivateKeyName],
)
if err != nil {
r.Log.Info(fmt.Sprintf("%s public_key-private_key pair is not valid: %s", kubeadmconstants.ServiceAccountKeyBaseName, err.Error()))
}
if isValid {
return nil
}
}
config, _, err := getKubeadmConfiguration(ctx, r, tenantControlPlane)
if err != nil {
return err
}
sa, err := kubeadm.GeneratePublicKeyPrivateKeyPair(kubeadmconstants.ServiceAccountKeyBaseName, config)
if err != nil {
return err
}
r.resource.Data = map[string][]byte{
kubeadmconstants.ServiceAccountPublicKeyName: sa.PublicKey,
kubeadmconstants.ServiceAccountPrivateKeyName: sa.PrivateKey,
}
r.resource.SetLabels(utilities.MergeMaps(
utilities.KamajiLabels(),
map[string]string{
"latest-config-rv": latestConfigRV,
"kamaji.clastix.io/name": tenantControlPlane.GetName(),
"kamaji.clastix.io/component": r.GetName(),
},
))
return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
}
}

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