mirror of
https://github.com/clastix/kamaji.git
synced 2026-02-28 16:50:29 +00:00
Compare commits
6 Commits
edge-26.2.
...
edge-26.2.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
309d9889e8 | ||
|
|
1d05f2bb4c | ||
|
|
cc8a8e14fd | ||
|
|
13a3aa70f5 | ||
|
|
49ea678047 | ||
|
|
830d56dac1 |
23
Makefile
23
Makefile
@@ -83,7 +83,7 @@ $(YQ): $(LOCALBIN)
|
||||
.PHONY: helm
|
||||
helm: $(HELM) ## Download helm locally if necessary.
|
||||
$(HELM): $(LOCALBIN)
|
||||
test -s $(LOCALBIN)/helm || GOBIN=$(LOCALBIN) CGO_ENABLED=0 go install -ldflags="-s -w" helm.sh/helm/v3/cmd/helm@v3.9.0
|
||||
test -s $(LOCALBIN)/helm || GOBIN=$(LOCALBIN) CGO_ENABLED=0 go install -ldflags="-s -w" helm.sh/helm/v4/cmd/helm@v4.1.1
|
||||
|
||||
.PHONY: ginkgo
|
||||
ginkgo: $(GINKGO) ## Download ginkgo locally if necessary.
|
||||
@@ -236,16 +236,29 @@ metallb:
|
||||
kubectl apply -f "https://raw.githubusercontent.com/metallb/metallb/$$(curl "https://api.github.com/repos/metallb/metallb/releases/latest" | jq -r ".tag_name")/config/manifests/metallb-native.yaml"
|
||||
kubectl wait pods -n metallb-system -l app=metallb,component=controller --for=condition=Ready --timeout=10m
|
||||
kubectl wait pods -n metallb-system -l app=metallb,component=speaker --for=condition=Ready --timeout=2m
|
||||
cat hack/metallb.yaml | sed -E "s|172.19|$$(docker network inspect -f '{{range .IPAM.Config}}{{.Gateway}}{{end}}' kind | sed -E 's|^([0-9]+\.[0-9]+)\..*$$|\1|g')|g" | kubectl apply -f -
|
||||
@IPV4_PREFIX=$$(docker network inspect kind \
|
||||
-f '{{range .IPAM.Config}}{{println .Subnet " " .Gateway}}{{end}}' \
|
||||
| grep -v ':' \
|
||||
| awk '{print $$2}' \
|
||||
| sed -E 's|^([0-9]+\.[0-9]+)\..*$$|\1|'); \
|
||||
sed -E "s|172\.19|$$IPV4_PREFIX|g" hack/metallb.yaml | kubectl apply -f -
|
||||
|
||||
cert-manager:
|
||||
$(HELM) repo add jetstack https://charts.jetstack.io
|
||||
$(HELM) upgrade --install cert-manager jetstack/cert-manager --namespace certmanager-system --create-namespace --set "installCRDs=true"
|
||||
|
||||
gateway-api:
|
||||
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
|
||||
# Required for the TLSRoutes. Experimentals.
|
||||
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/experimental-install.yaml
|
||||
kubectl apply \
|
||||
--server-side \
|
||||
--force-conflicts \
|
||||
--field-manager=helm \
|
||||
-f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
|
||||
# Required for the TLSRoutes. Experimentals.
|
||||
kubectl apply \
|
||||
--server-side \
|
||||
--force-conflicts \
|
||||
--field-manager=helm \
|
||||
-f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/experimental-install.yaml
|
||||
kubectl wait --for=condition=Established crd/gateways.gateway.networking.k8s.io --timeout=60s
|
||||
|
||||
envoy-gateway: gateway-api helm ## Install Envoy Gateway for Gateway API tests.
|
||||
|
||||
@@ -32,6 +32,7 @@ type Endpoints []string
|
||||
// +kubebuilder:validation:XValidation:rule="(self.driver != \"etcd\" && has(self.basicAuth)) ? ((has(self.basicAuth.username.secretReference) || has(self.basicAuth.username.content))) : true", message="When driver is not etcd and basicAuth exists, username must have secretReference or content"
|
||||
// +kubebuilder:validation:XValidation:rule="(self.driver != \"etcd\" && has(self.basicAuth)) ? ((has(self.basicAuth.password.secretReference) || has(self.basicAuth.password.content))) : true", message="When driver is not etcd and basicAuth exists, password must have secretReference or content"
|
||||
// +kubebuilder:validation:XValidation:rule="(self.driver != \"etcd\") ? (has(self.tlsConfig) || has(self.basicAuth)) : true", message="When driver is not etcd, either tlsConfig or basicAuth must be provided"
|
||||
// +kubebuilder:validation:XValidation:rule="oldSelf == null || self.driver == oldSelf.driver", message="driver is immutable and cannot be changed after creation"
|
||||
type DataStoreSpec struct {
|
||||
// The driver to use to connect to the shared datastore.
|
||||
Driver Driver `json:"driver"`
|
||||
@@ -88,6 +89,13 @@ type SecretReference struct {
|
||||
KeyPath secretReferKeyPath `json:"keyPath"`
|
||||
}
|
||||
|
||||
const (
|
||||
DataStoreTCPFinalizer = "kamaji.clastix.io/TenantControlPlane"
|
||||
|
||||
DataStoreConditionValidType = "kamaji.clastix.io/DataStoreValidation"
|
||||
DataStoreConditionAllowedDeletionType = "kamaji.clastix.io/DataStoreAllowedDeletion"
|
||||
)
|
||||
|
||||
// DataStoreStatus defines the observed state of DataStore.
|
||||
type DataStoreStatus struct {
|
||||
// ObservedGeneration represents the .metadata.generation that was last reconciled.
|
||||
@@ -95,12 +103,18 @@ type DataStoreStatus struct {
|
||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||
// List of the Tenant Control Planes, namespaced named, using this data store.
|
||||
UsedBy []string `json:"usedBy,omitempty"`
|
||||
// Conditions contains the validation conditions for the given Datastore.
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
// Ready returns if the DataStore is accepted and ready to get used:
|
||||
// Kamaji will ensure certificates are available, or correctly referenced.
|
||||
Ready bool `json:"ready"`
|
||||
}
|
||||
|
||||
//+kubebuilder:object:root=true
|
||||
//+kubebuilder:subresource:status
|
||||
//+kubebuilder:resource:scope=Cluster
|
||||
//+kubebuilder:printcolumn:name="Driver",type="string",JSONPath=".spec.driver",description="Kamaji data store driver"
|
||||
//+kubebuilder:printcolumn:name="Ready",type="boolean",JSONPath=".status.ready",description="DataStore validated and ready for use"
|
||||
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
|
||||
//+kubebuilder:metadata:annotations={"cert-manager.io/inject-ca-from=kamaji-system/kamaji-serving-cert"}
|
||||
|
||||
|
||||
@@ -155,7 +155,6 @@ type IngressSpec struct {
|
||||
}
|
||||
|
||||
// GatewaySpec defines the options for the Gateway which will expose API Server of the Tenant Control Plane.
|
||||
// +kubebuilder:validation:XValidation:rule="!has(self.parentRefs) || size(self.parentRefs) == 0 || self.parentRefs.all(ref, !has(ref.port) && !has(ref.sectionName))",message="parentRefs must not specify port or sectionName, these are set automatically by Kamaji"
|
||||
type GatewaySpec struct {
|
||||
// AdditionalMetadata to add Labels and Annotations support.
|
||||
AdditionalMetadata AdditionalMetadata `json:"additionalMetadata,omitempty"`
|
||||
|
||||
@@ -11,6 +11,10 @@ versions:
|
||||
jsonPath: .spec.driver
|
||||
name: Driver
|
||||
type: string
|
||||
- description: DataStore validated and ready for use
|
||||
jsonPath: .status.ready
|
||||
name: Ready
|
||||
type: boolean
|
||||
- description: Age
|
||||
jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
@@ -272,18 +276,83 @@ versions:
|
||||
rule: '(self.driver != "etcd" && has(self.basicAuth)) ? ((has(self.basicAuth.password.secretReference) || has(self.basicAuth.password.content))) : true'
|
||||
- message: When driver is not etcd, either tlsConfig or basicAuth must be provided
|
||||
rule: '(self.driver != "etcd") ? (has(self.tlsConfig) || has(self.basicAuth)) : true'
|
||||
- message: driver is immutable and cannot be changed after creation
|
||||
rule: oldSelf == null || self.driver == oldSelf.driver
|
||||
status:
|
||||
description: DataStoreStatus defines the observed state of DataStore.
|
||||
properties:
|
||||
conditions:
|
||||
description: Conditions contains the validation conditions for the given Datastore.
|
||||
items:
|
||||
description: Condition contains details for one aspect of the current state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
type: string
|
||||
status:
|
||||
description: status of the condition, one of True, False, Unknown.
|
||||
enum:
|
||||
- "True"
|
||||
- "False"
|
||||
- Unknown
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
required:
|
||||
- lastTransitionTime
|
||||
- message
|
||||
- reason
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
observedGeneration:
|
||||
description: ObservedGeneration represents the .metadata.generation that was last reconciled.
|
||||
format: int64
|
||||
type: integer
|
||||
ready:
|
||||
description: |-
|
||||
Ready returns if the DataStore is accepted and ready to get used:
|
||||
Kamaji will ensure certificates are available, or correctly referenced.
|
||||
type: boolean
|
||||
usedBy:
|
||||
description: List of the Tenant Control Planes, namespaced named, using this data store.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- ready
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
|
||||
@@ -6896,9 +6896,6 @@ versions:
|
||||
type: object
|
||||
type: array
|
||||
type: object
|
||||
x-kubernetes-validations:
|
||||
- message: parentRefs must not specify port or sectionName, these are set automatically by Kamaji
|
||||
rule: '!has(self.parentRefs) || size(self.parentRefs) == 0 || self.parentRefs.all(ref, !has(ref.port) && !has(ref.sectionName))'
|
||||
ingress:
|
||||
description: Defining the options for an Optional Ingress which will expose API Server of the Tenant Control Plane
|
||||
properties:
|
||||
|
||||
@@ -19,46 +19,6 @@
|
||||
resources:
|
||||
- tenantcontrolplanes
|
||||
sideEffects: None
|
||||
- admissionReviewVersions:
|
||||
- v1
|
||||
clientConfig:
|
||||
service:
|
||||
name: '{{ include "kamaji.webhookServiceName" . }}'
|
||||
namespace: '{{ .Release.Namespace }}'
|
||||
path: /validate-kamaji-clastix-io-v1alpha1-datastore
|
||||
failurePolicy: Fail
|
||||
name: vdatastore.kb.io
|
||||
rules:
|
||||
- apiGroups:
|
||||
- kamaji.clastix.io
|
||||
apiVersions:
|
||||
- v1alpha1
|
||||
operations:
|
||||
- CREATE
|
||||
- UPDATE
|
||||
- DELETE
|
||||
resources:
|
||||
- datastores
|
||||
sideEffects: None
|
||||
- admissionReviewVersions:
|
||||
- v1
|
||||
clientConfig:
|
||||
service:
|
||||
name: '{{ include "kamaji.webhookServiceName" . }}'
|
||||
namespace: '{{ .Release.Namespace }}'
|
||||
path: /validate--v1-secret
|
||||
failurePolicy: Ignore
|
||||
name: vdatastoresecrets.kb.io
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
apiVersions:
|
||||
- v1
|
||||
operations:
|
||||
- DELETE
|
||||
resources:
|
||||
- secrets
|
||||
sideEffects: None
|
||||
- admissionReviewVersions:
|
||||
- v1
|
||||
clientConfig:
|
||||
|
||||
@@ -20,6 +20,10 @@ spec:
|
||||
jsonPath: .spec.driver
|
||||
name: Driver
|
||||
type: string
|
||||
- description: DataStore validated and ready for use
|
||||
jsonPath: .status.ready
|
||||
name: Ready
|
||||
type: boolean
|
||||
- description: Age
|
||||
jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
@@ -281,18 +285,83 @@ spec:
|
||||
rule: '(self.driver != "etcd" && has(self.basicAuth)) ? ((has(self.basicAuth.password.secretReference) || has(self.basicAuth.password.content))) : true'
|
||||
- message: When driver is not etcd, either tlsConfig or basicAuth must be provided
|
||||
rule: '(self.driver != "etcd") ? (has(self.tlsConfig) || has(self.basicAuth)) : true'
|
||||
- message: driver is immutable and cannot be changed after creation
|
||||
rule: oldSelf == null || self.driver == oldSelf.driver
|
||||
status:
|
||||
description: DataStoreStatus defines the observed state of DataStore.
|
||||
properties:
|
||||
conditions:
|
||||
description: Conditions contains the validation conditions for the given Datastore.
|
||||
items:
|
||||
description: Condition contains details for one aspect of the current state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
type: string
|
||||
status:
|
||||
description: status of the condition, one of True, False, Unknown.
|
||||
enum:
|
||||
- "True"
|
||||
- "False"
|
||||
- Unknown
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
required:
|
||||
- lastTransitionTime
|
||||
- message
|
||||
- reason
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
observedGeneration:
|
||||
description: ObservedGeneration represents the .metadata.generation that was last reconciled.
|
||||
format: int64
|
||||
type: integer
|
||||
ready:
|
||||
description: |-
|
||||
Ready returns if the DataStore is accepted and ready to get used:
|
||||
Kamaji will ensure certificates are available, or correctly referenced.
|
||||
type: boolean
|
||||
usedBy:
|
||||
description: List of the Tenant Control Planes, namespaced named, using this data store.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- ready
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
|
||||
@@ -6904,9 +6904,6 @@ spec:
|
||||
type: object
|
||||
type: array
|
||||
type: object
|
||||
x-kubernetes-validations:
|
||||
- message: parentRefs must not specify port or sectionName, these are set automatically by Kamaji
|
||||
rule: '!has(self.parentRefs) || size(self.parentRefs) == 0 || self.parentRefs.all(ref, !has(ref.port) && !has(ref.sectionName))'
|
||||
ingress:
|
||||
description: Defining the options for an Optional Ingress which will expose API Server of the Tenant Control Plane
|
||||
properties:
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -34,7 +33,6 @@ import (
|
||||
"github.com/clastix/kamaji/controllers/soot"
|
||||
"github.com/clastix/kamaji/internal"
|
||||
"github.com/clastix/kamaji/internal/builders/controlplane"
|
||||
datastoreutils "github.com/clastix/kamaji/internal/datastore/utils"
|
||||
"github.com/clastix/kamaji/internal/utilities"
|
||||
"github.com/clastix/kamaji/internal/webhook"
|
||||
"github.com/clastix/kamaji/internal/webhook/handlers"
|
||||
@@ -87,10 +85,6 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
|
||||
return fmt.Errorf("unable to read webhook CA: %w", err)
|
||||
}
|
||||
|
||||
if err = datastoreutils.CheckExists(context.Background(), scheme, datastore); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if controllerReconcileTimeout.Seconds() == 0 {
|
||||
return fmt.Errorf("the controller reconcile timeout must be greater than zero")
|
||||
}
|
||||
@@ -276,12 +270,6 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
|
||||
KubernetesVersion: k8sVersion,
|
||||
},
|
||||
},
|
||||
routes.DataStoreValidate{}: {
|
||||
handlers.DataStoreValidation{Client: mgr.GetClient()},
|
||||
},
|
||||
routes.DataStoreSecrets{}: {
|
||||
handlers.DataStoreSecretValidation{Client: mgr.GetClient()},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to create webhook")
|
||||
|
||||
@@ -7,19 +7,20 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
k8stypes "k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
controllerruntime "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
@@ -38,19 +39,21 @@ type DataStore struct {
|
||||
//+kubebuilder:rbac:groups=kamaji.clastix.io,resources=datastores/status,verbs=get;update;patch
|
||||
|
||||
func (r *DataStore) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
|
||||
var err error
|
||||
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
var ds kamajiv1alpha1.DataStore
|
||||
if err := r.Client.Get(ctx, request.NamespacedName, &ds); err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
if dsErr := r.Client.Get(ctx, request.NamespacedName, &ds); dsErr != nil {
|
||||
if k8serrors.IsNotFound(dsErr) {
|
||||
logger.Info("resource may have been deleted, skipping")
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
logger.Error(err, "cannot retrieve the required resource")
|
||||
logger.Error(dsErr, "cannot retrieve the required resource")
|
||||
|
||||
return reconcile.Result{}, err
|
||||
return reconcile.Result{}, dsErr
|
||||
}
|
||||
|
||||
if utils.IsPaused(&ds) {
|
||||
@@ -59,45 +62,179 @@ func (r *DataStore) Reconcile(ctx context.Context, request reconcile.Request) (r
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
if ds.GetDeletionTimestamp() == nil && !controllerutil.ContainsFinalizer(&ds, kamajiv1alpha1.DataStoreTCPFinalizer) {
|
||||
logger.Info("missing finalizer, adding it")
|
||||
|
||||
ds.SetFinalizers(append(ds.GetFinalizers(), kamajiv1alpha1.DataStoreTCPFinalizer))
|
||||
if uErr := r.Client.Update(ctx, &ds); uErr != nil {
|
||||
return reconcile.Result{}, uErr
|
||||
}
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
// Extracting the list of TenantControlPlane objects referenced by the given DataStore:
|
||||
// this data is used to reference these in the Status, as well as propagating changes
|
||||
// that would be required, such as changing TLS Configuration, or Basic Auth.
|
||||
var tcpList kamajiv1alpha1.TenantControlPlaneList
|
||||
|
||||
updateErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
if lErr := r.Client.List(ctx, &tcpList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(kamajiv1alpha1.TenantControlPlaneUsedDataStoreKey, ds.GetName()),
|
||||
}); lErr != nil {
|
||||
return fmt.Errorf("cannot retrieve list of the Tenant Control Plane using the following instance: %w", lErr)
|
||||
}
|
||||
// Updating the status with the list of Tenant Control Plane using the following Data Source
|
||||
tcpSets := sets.NewString()
|
||||
for _, tcp := range tcpList.Items {
|
||||
tcpSets.Insert(getNamespacedName(tcp.GetNamespace(), tcp.GetName()).String())
|
||||
if lErr := r.Client.List(ctx, &tcpList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(kamajiv1alpha1.TenantControlPlaneUsedDataStoreKey, ds.GetName()),
|
||||
}); lErr != nil {
|
||||
return reconcile.Result{}, fmt.Errorf("cannot retrieve list of the Tenant Control Plane using the following instance: %w", lErr)
|
||||
}
|
||||
|
||||
tcpSets := sets.NewString()
|
||||
|
||||
for _, tcp := range tcpList.Items {
|
||||
tcpSets.Insert(getNamespacedName(tcp.GetNamespace(), tcp.GetName()).String())
|
||||
}
|
||||
|
||||
ds.Status.UsedBy = tcpSets.List()
|
||||
// Performing the status update only at the end of the reconciliation loop:
|
||||
// this is performed in defer to avoid duplication of code,
|
||||
// and triggering a reconciliation of depending on TenantControlPlanes only if the update was successful.
|
||||
defer func() {
|
||||
if meta.IsStatusConditionTrue(ds.Status.Conditions, kamajiv1alpha1.DataStoreConditionAllowedDeletionType) {
|
||||
logger.Info("removing finalizer upon true condition")
|
||||
|
||||
controllerutil.RemoveFinalizer(&ds, kamajiv1alpha1.DataStoreTCPFinalizer)
|
||||
if uErr := r.Client.Update(ctx, &ds); uErr != nil {
|
||||
logger.Error(uErr, "cannot update object")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("finalizer removed successfully")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ds.Status.ObservedGeneration = ds.Generation
|
||||
ds.Status.UsedBy = tcpSets.List()
|
||||
ds.Status.Ready = meta.IsStatusConditionTrue(ds.Status.Conditions, kamajiv1alpha1.DataStoreConditionValidType)
|
||||
|
||||
if sErr := r.Client.Status().Update(ctx, &ds); sErr != nil {
|
||||
return fmt.Errorf("cannot update the status for the given instance: %w", sErr)
|
||||
if err = r.Client.Status().Update(ctx, &ds); err != nil {
|
||||
logger.Error(err, "cannot update the status for the given instance")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
if !ds.Status.Ready {
|
||||
logger.Info("skipping triggering, DataStore is not ready")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("triggering cascading reconciliation for TenantControlPlanes")
|
||||
|
||||
for _, tcp := range tcpList.Items {
|
||||
var shrunkTCP kamajiv1alpha1.TenantControlPlane
|
||||
|
||||
shrunkTCP.Name = tcp.Name
|
||||
shrunkTCP.Namespace = tcp.Namespace
|
||||
|
||||
go utils.TriggerChannel(ctx, r.TenantControlPlaneTrigger, shrunkTCP)
|
||||
}
|
||||
}()
|
||||
|
||||
if ds.GetDeletionTimestamp() != nil {
|
||||
if len(tcpList.Items) > 0 {
|
||||
logger.Info("deletion is blocked due to DataStore still being referenced")
|
||||
|
||||
meta.SetStatusCondition(&ds.Status.Conditions, metav1.Condition{
|
||||
Type: kamajiv1alpha1.DataStoreConditionAllowedDeletionType,
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: ds.Generation,
|
||||
Reason: "DataStoreStillUsed",
|
||||
Message: "The DataStore is still used and referenced by one (or more) TenantControlPlane objects.",
|
||||
})
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
if meta.IsStatusConditionFalse(ds.Status.Conditions, kamajiv1alpha1.DataStoreConditionAllowedDeletionType) {
|
||||
logger.Info("DataStore is not used by any TenantControlPlane object")
|
||||
|
||||
meta.SetStatusCondition(&ds.Status.Conditions, metav1.Condition{
|
||||
Type: kamajiv1alpha1.DataStoreConditionAllowedDeletionType,
|
||||
Status: metav1.ConditionTrue,
|
||||
ObservedGeneration: ds.Generation,
|
||||
Reason: "DataStoreUnused",
|
||||
Message: "",
|
||||
})
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
logger.Info("DataStore can be safely deleted")
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
if exists := meta.FindStatusCondition(ds.Status.Conditions, kamajiv1alpha1.DataStoreConditionValidType); exists == nil {
|
||||
logger.Info("missing starting condition")
|
||||
|
||||
meta.SetStatusCondition(&ds.Status.Conditions, metav1.Condition{
|
||||
Type: kamajiv1alpha1.DataStoreConditionValidType,
|
||||
Status: metav1.ConditionUnknown,
|
||||
ObservedGeneration: ds.Generation,
|
||||
Reason: "MissingCondition",
|
||||
Message: "Controller will process the validation.",
|
||||
})
|
||||
|
||||
if sErr := r.Client.Status().Update(ctx, &ds); sErr != nil {
|
||||
return reconcile.Result{}, fmt.Errorf("cannot update the status for the given instance: %w", sErr)
|
||||
}
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
if ds.Spec.BasicAuth != nil {
|
||||
logger.Info("validating basic authentication")
|
||||
|
||||
if vErr := r.validateBasicAuth(ctx, ds); vErr != nil {
|
||||
meta.SetStatusCondition(&ds.Status.Conditions, metav1.Condition{
|
||||
Type: kamajiv1alpha1.DataStoreConditionValidType,
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: ds.Generation,
|
||||
Reason: "BasicAuthValidationFailed",
|
||||
Message: vErr.Error(),
|
||||
})
|
||||
|
||||
logger.Info("invalid basic authentication")
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
logger.Info("basic authentication is valid")
|
||||
}
|
||||
|
||||
logger.Info("validating TLS configuration")
|
||||
|
||||
if vErr := r.validateTLSConfig(ctx, ds); vErr != nil {
|
||||
meta.SetStatusCondition(&ds.Status.Conditions, metav1.Condition{
|
||||
Type: kamajiv1alpha1.DataStoreConditionValidType,
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: ds.Generation,
|
||||
Reason: "TLSConfigurationValidationFailed",
|
||||
Message: vErr.Error(),
|
||||
})
|
||||
|
||||
logger.Info("invalid TLS configuration")
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
logger.Info("TLS configuration is valid")
|
||||
|
||||
meta.SetStatusCondition(&ds.Status.Conditions, metav1.Condition{
|
||||
Type: kamajiv1alpha1.DataStoreConditionValidType,
|
||||
Status: metav1.ConditionTrue,
|
||||
ObservedGeneration: ds.Status.ObservedGeneration,
|
||||
Reason: "DataStoreIsValid",
|
||||
Message: "",
|
||||
})
|
||||
if updateErr != nil {
|
||||
logger.Error(updateErr, "cannot update DataStore status")
|
||||
|
||||
return reconcile.Result{}, updateErr
|
||||
}
|
||||
// Triggering the reconciliation of the Tenant Control Plane upon a Secret change
|
||||
for _, tcp := range tcpList.Items {
|
||||
var shrunkTCP kamajiv1alpha1.TenantControlPlane
|
||||
|
||||
shrunkTCP.Name = tcp.Name
|
||||
shrunkTCP.Namespace = tcp.Namespace
|
||||
|
||||
go utils.TriggerChannel(ctx, r.TenantControlPlaneTrigger, shrunkTCP)
|
||||
}
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
func (r *DataStore) SetupWithManager(mgr controllerruntime.Manager) error {
|
||||
@@ -112,9 +249,7 @@ func (r *DataStore) SetupWithManager(mgr controllerruntime.Manager) error {
|
||||
}
|
||||
//nolint:forcetypeassert
|
||||
return controllerruntime.NewControllerManagedBy(mgr).
|
||||
For(&kamajiv1alpha1.DataStore{}, builder.WithPredicates(
|
||||
predicate.GenerationChangedPredicate{},
|
||||
)).
|
||||
For(&kamajiv1alpha1.DataStore{}).
|
||||
Watches(&kamajiv1alpha1.TenantControlPlane{}, handler.Funcs{
|
||||
CreateFunc: func(_ context.Context, createEvent event.TypedCreateEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
enqueueFn(createEvent.Object.(*kamajiv1alpha1.TenantControlPlane), w)
|
||||
@@ -129,3 +264,76 @@ func (r *DataStore) SetupWithManager(mgr controllerruntime.Manager) error {
|
||||
}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *DataStore) validateBasicAuth(ctx context.Context, ds kamajiv1alpha1.DataStore) error {
|
||||
if err := r.validateContentReference(ctx, ds.Spec.BasicAuth.Password); err != nil {
|
||||
return fmt.Errorf("basic-auth password is not valid, %w", err)
|
||||
}
|
||||
|
||||
if err := r.validateContentReference(ctx, ds.Spec.BasicAuth.Username); err != nil {
|
||||
return fmt.Errorf("basic-auth username is not valid, %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DataStore) validateTLSConfig(ctx context.Context, ds kamajiv1alpha1.DataStore) error {
|
||||
if ds.Spec.TLSConfig == nil && ds.Spec.Driver != kamajiv1alpha1.EtcdDriver {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := r.validateContentReference(ctx, ds.Spec.TLSConfig.CertificateAuthority.Certificate); err != nil {
|
||||
return fmt.Errorf("CA certificate is not valid, %w", err)
|
||||
}
|
||||
|
||||
if ds.Spec.Driver == kamajiv1alpha1.EtcdDriver {
|
||||
if ds.Spec.TLSConfig.CertificateAuthority.PrivateKey == nil {
|
||||
return fmt.Errorf("CA private key is required when using the etcd driver")
|
||||
}
|
||||
|
||||
if ds.Spec.TLSConfig.ClientCertificate == nil {
|
||||
return fmt.Errorf("client certificate is required when using the etcd driver")
|
||||
}
|
||||
}
|
||||
|
||||
if ds.Spec.TLSConfig.CertificateAuthority.PrivateKey != nil {
|
||||
if err := r.validateContentReference(ctx, *ds.Spec.TLSConfig.CertificateAuthority.PrivateKey); err != nil {
|
||||
return fmt.Errorf("CA private key is not valid, %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if ds.Spec.TLSConfig.ClientCertificate != nil {
|
||||
if err := r.validateContentReference(ctx, ds.Spec.TLSConfig.ClientCertificate.Certificate); err != nil {
|
||||
return fmt.Errorf("client certificate is not valid, %w", err)
|
||||
}
|
||||
|
||||
if err := r.validateContentReference(ctx, ds.Spec.TLSConfig.ClientCertificate.PrivateKey); err != nil {
|
||||
return fmt.Errorf("client private key is not valid, %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DataStore) validateContentReference(ctx context.Context, ref kamajiv1alpha1.ContentRef) error {
|
||||
switch {
|
||||
case len(ref.Content) > 0:
|
||||
return nil
|
||||
case ref.SecretRef == nil:
|
||||
return fmt.Errorf("the Secret reference is mandatory when bare content is not specified")
|
||||
case len(ref.SecretRef.SecretReference.Name) == 0:
|
||||
return fmt.Errorf("the Secret reference name is mandatory")
|
||||
case len(ref.SecretRef.SecretReference.Namespace) == 0:
|
||||
return fmt.Errorf("the Secret reference namespace is mandatory")
|
||||
}
|
||||
|
||||
if err := r.Client.Get(ctx, k8stypes.NamespacedName{Name: ref.SecretRef.SecretReference.Name, Namespace: ref.SecretRef.SecretReference.Namespace}, &corev1.Secret{}); err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
return fmt.Errorf("secret %s/%s is not found", ref.SecretRef.SecretReference.Namespace, ref.SecretRef.SecretReference.Name)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ type TenantControlPlaneReconcilerConfig struct {
|
||||
//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=tlsroutes,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways,verbs=get;list;watch
|
||||
|
||||
//nolint:maintidx
|
||||
func (r *TenantControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
log := log.FromContext(ctx)
|
||||
|
||||
@@ -137,11 +138,13 @@ func (r *TenantControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R
|
||||
if markedToBeDeleted && !controllerutil.ContainsFinalizer(tenantControlPlane, finalizers.DatastoreFinalizer) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
// Retrieving the DataStore to use for the current reconciliation
|
||||
ds, err := r.dataStore(ctx, tenantControlPlane)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrMissingDataStore) {
|
||||
log.Info(err.Error())
|
||||
// DataStore preflight checks:
|
||||
// 1. DataStore must exist
|
||||
// 2. it must be ready
|
||||
ds, dsErr := r.dataStore(ctx, tenantControlPlane)
|
||||
if dsErr != nil {
|
||||
if errors.Is(dsErr, ErrMissingDataStore) {
|
||||
log.Info(dsErr.Error())
|
||||
|
||||
return ctrl.Result{RequeueAfter: time.Second}, nil
|
||||
}
|
||||
@@ -151,6 +154,12 @@ func (r *TenantControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if !ds.Status.Ready {
|
||||
log.Info("cannot reconcile since DataStore is not ready")
|
||||
|
||||
return ctrl.Result{RequeueAfter: time.Second}, nil
|
||||
}
|
||||
|
||||
dsConnection, err := datastore.NewStorageConnection(ctx, r.Client, *ds)
|
||||
if err != nil {
|
||||
log.Error(err, "cannot generate the DataStore connection for the given instance")
|
||||
|
||||
@@ -5,7 +5,7 @@ NAMESPACE:=nats-system
|
||||
.PHONY: helm
|
||||
HELM = $(shell pwd)/../../../bin/helm
|
||||
helm: ## Download helm locally if necessary.
|
||||
$(call go-install-tool,$(HELM),helm.sh/helm/v3/cmd/helm@v3.9.0)
|
||||
$(call go-install-tool,$(HELM),helm.sh/helm/v3/cmd/helm@v4.1.1)
|
||||
|
||||
nats: nats-certificates nats-secret nats-deployment
|
||||
|
||||
|
||||
@@ -1,106 +1,34 @@
|
||||
# Gateway API Support
|
||||
# Gateway API
|
||||
|
||||
Kamaji provides built-in support for the [Gateway API](https://gateway-api.sigs.k8s.io/), allowing you to expose Tenant Control Planes using TLSRoute resources with SNI-based routing. This enables hostname-based routing to multiple Tenant Control Planes through a single Gateway resource, reducing the need for dedicated LoadBalancer services.
|
||||
Kamaji provides built-in support for the [Gateway API](https://gateway-api.sigs.k8s.io/), allowing Tenant Control Planes to be exposed as SNI-based addresses/urls. This eliminates the need for a dedicated LoadBalancer IP per TCP. A single Gateway resource can be used for multiple Tenant Control Planes and provide access to them with hostname-based routing (like `https://mycluster.xyz.com:6443`).
|
||||
|
||||
## Overview
|
||||
You can configure Gateway in Tenant Control Plane via `tcp.spec.controlPlane.gateway`, Kamaji will automatically create a `TLSRoute` resource with corresponding spec. To make this configuration work, you need to ensure `gateway` exists (is created by you) and `tcp.spec.controlPlane.gateway` points to your gateway.
|
||||
|
||||
Gateway API support in Kamaji automatically creates and manages TLSRoute resources for your Tenant Control Planes. When you configure a Gateway for a Tenant Control Plane, Kamaji automatically creates TLSRoutes for the Control Plane API Server. If konnectivity is enabled, a separate TLSRoute is created for it. Both TLSRoutes use the same hostname and Gateway resource, but route to different ports(listeners) using port-based routing and semantic `sectionName` values.
|
||||
We will cover a few examples below on how this is done.
|
||||
|
||||
Therefore, the target `Gateway` resource must have right listener configurations (see the Gateway [example section](#gateway-resource-setup) below).
|
||||
|
||||
|
||||
## How It Works
|
||||
|
||||
When you configure `spec.controlPlane.gateway` in a TenantControlPlane resource, Kamaji automatically:
|
||||
|
||||
1. **Creates a TLSRoute for the control plane** that routes for port 6443 (or `spec.networkProfile.port`) with sectionName `"kube-apiserver"`
|
||||
2. **Creates a TLSRoute for Konnectivity** (if konnectivity addon is enabled) that routes for port 8132 (or `spec.addons.konnectivity.server.port`) with sectionName `"konnectivity-server"`
|
||||
|
||||
Both TLSRoutes:
|
||||
|
||||
- Use the same hostname from `spec.controlPlane.gateway.hostname`
|
||||
- Reference the same parent Gateway resource via `parentRefs`
|
||||
- The `port` and `sectionName` fields are set automatically by Kamaji
|
||||
- Route to the appropriate Tenant Control Plane service
|
||||
|
||||
The Gateway resource must have listeners configured for both ports (6443 and 8132) to support both routes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using Gateway API support, ensure:
|
||||
Before using Gateway API mode, please ensure:
|
||||
|
||||
1. **Gateway API CRDs are installed** in your cluster (Required CRDs: `GatewayClass`, `Gateway`, `TLSRoute`)
|
||||
|
||||
2. **A Gateway resource exists** with appropriate listeners configured:
|
||||
- At minimum, listeners for ports 6443 (control plane) and 8132 (Konnectivity)
|
||||
- TLS protocol with Passthrough mode
|
||||
- Hostname pattern matching your Tenant Control Plane hostnames
|
||||
2. **A Gateway resource exists** with appropriate configuration (see examples in this guide):
|
||||
- Listeners for kube-apiserver.
|
||||
- Use TLS protocol with Passthrough mode
|
||||
- Hostname (or Hostname pattern) matching your Tenant Control Plane hostname
|
||||
|
||||
3. **DNS is configured** to resolve your hostnames to the Gateway's external address
|
||||
3. (optional) **DNS is configured** to resolve hostnames (or hostname pattern) to the Gateway's LoadBalancer IP address. (This is needed for worker nodes to join, for testing we will use host entries in `/etc/hosts` for this guide)
|
||||
|
||||
4. **Gateway controller is running** (e.g., Envoy Gateway, Istio Gateway, etc.)
|
||||
|
||||
## Configuration
|
||||
To replicate the guide below, please install [Envoy Gateway](https://gateway.envoyproxy.io/docs/tasks/quickstart/).
|
||||
|
||||
### TenantControlPlane Gateway Configuration
|
||||
Next, create a gateway resource:
|
||||
|
||||
Enable Gateway API mode by setting the `spec.controlPlane.gateway` field in your TenantControlPlane resource:
|
||||
#### Gateway Resource Setup
|
||||
|
||||
```yaml
|
||||
apiVersion: kamaji.clastix.io/v1alpha1
|
||||
kind: TenantControlPlane
|
||||
metadata:
|
||||
name: tcp-1
|
||||
spec:
|
||||
controlPlane:
|
||||
# ... gateway configuration:
|
||||
gateway:
|
||||
hostname: "tcp1.cluster.dev"
|
||||
parentRefs:
|
||||
- name: gateway
|
||||
namespace: default
|
||||
additionalMetadata:
|
||||
labels:
|
||||
environment: production
|
||||
annotations:
|
||||
example.com/custom: "value"
|
||||
# ... rest of the spec
|
||||
deployment:
|
||||
replicas: 1
|
||||
service:
|
||||
serviceType: ClusterIP
|
||||
dataStore: default
|
||||
kubernetes:
|
||||
version: v1.29.0
|
||||
kubelet:
|
||||
cgroupfs: systemd
|
||||
networkProfile:
|
||||
port: 6443
|
||||
certSANs:
|
||||
- "c11.cluster.dev" # make sure to set this.
|
||||
addons:
|
||||
coreDNS: {}
|
||||
kubeProxy: {}
|
||||
konnectivity: {}
|
||||
|
||||
```
|
||||
|
||||
**Required fields:**
|
||||
|
||||
- `hostname`: The hostname that will be used for routing (must match Gateway listener hostname pattern)
|
||||
- `parentRefs`: Array of Gateway references (name and namespace)
|
||||
|
||||
**Optional fields:**
|
||||
|
||||
- `additionalMetadata.labels`: Custom labels to add to TLSRoute resources
|
||||
- `additionalMetadata.annotations`: Custom annotations to add to TLSRoute resources
|
||||
|
||||
!!! warning "Port and sectionName are set automatically"
|
||||
Do not specify `port` or `sectionName` in `parentRefs`. Kamaji automatically sets these fields in TLSRoutes.
|
||||
|
||||
### Gateway Resource Setup
|
||||
|
||||
Your Gateway resource must have listeners configured for both the control plane and Konnectivity ports. Here's an example Gateway configuration:
|
||||
Your Gateway resource must have listeners configured for the control plane. Here's an example Gateway configuration:
|
||||
|
||||
```yaml
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
@@ -130,27 +58,94 @@ spec:
|
||||
kind: TLSRoute
|
||||
namespaces:
|
||||
from: All
|
||||
```
|
||||
The above gateway is configured with envoy gateway controller. You can achieve the same results with any other gateway controller that supports `TLSRoutes` and TLS passthrough mode.
|
||||
|
||||
The rest of this guide focuses on TCP.
|
||||
|
||||
## TenantControlPlane Gateway Configuration
|
||||
|
||||
Enable Gateway API mode by setting the `spec.controlPlane.gateway` field in your TenantControlPlane resource:
|
||||
|
||||
```yaml
|
||||
apiVersion: kamaji.clastix.io/v1alpha1
|
||||
kind: TenantControlPlane
|
||||
metadata:
|
||||
name: tcp-1
|
||||
spec:
|
||||
controlPlane:
|
||||
|
||||
# ... gateway configuration:
|
||||
gateway:
|
||||
hostname: "tcp1.cluster.dev"
|
||||
parentRefs:
|
||||
- name: gateway
|
||||
namespace: default
|
||||
sectionName: kube-apiserver
|
||||
port: 6443
|
||||
additionalMetadata:
|
||||
labels:
|
||||
environment: production
|
||||
annotations:
|
||||
example.com/custom: "value"
|
||||
|
||||
# ... rest of the spec
|
||||
deployment:
|
||||
replicas: 1
|
||||
service:
|
||||
serviceType: ClusterIP
|
||||
dataStore: default
|
||||
kubernetes:
|
||||
version: v1.29.0
|
||||
kubelet:
|
||||
cgroupfs: systemd
|
||||
networkProfile:
|
||||
port: 6443
|
||||
certSANs:
|
||||
- "tcp1.cluster.dev" # make sure to set this.
|
||||
addons:
|
||||
coreDNS: {}
|
||||
kubeProxy: {}
|
||||
|
||||
# if konnectivity addon is enabled:
|
||||
- name: konnectivity-server
|
||||
port: 8132
|
||||
protocol: TLS
|
||||
hostname: 'tcp1.cluster.dev'
|
||||
tls:
|
||||
mode: Passthrough
|
||||
allowedRoutes:
|
||||
kinds:
|
||||
- group: gateway.networking.k8s.io
|
||||
kind: TLSRoute
|
||||
namespaces:
|
||||
from: All
|
||||
|
||||
```
|
||||
|
||||
**Required fields:**
|
||||
|
||||
## Multiple Tenant Control Planes
|
||||
- `hostname`: The hostname that will be used for routing (must match Gateway listener hostname pattern)
|
||||
- `parentRefs`: Array of Gateway references
|
||||
|
||||
You can use the same Gateway resource for multiple Tenant Control Planes by using different hostnames:
|
||||
**Optional fields:**
|
||||
|
||||
- `additionalMetadata.labels`: Custom labels to add to TLSRoute resources
|
||||
- `additionalMetadata.annotations`: Custom annotations to add to TLSRoute resources
|
||||
|
||||
!!! info
|
||||
### Verify
|
||||
|
||||
From our kubectl client machine / remote machines we can access the cluster above with the hostname.
|
||||
|
||||
**Step 1:** Fetch load balancer IP of the gateway.
|
||||
|
||||
`kubectl get gateway gateway -n default -o jsonpath='{.status.addresses[0].value}'`
|
||||
|
||||
**Step 2:** Add host entries in `/etc/hosts` with the above hostname and gateway LB IP.
|
||||
|
||||
`echo "<LB_IP> tcp1.cluster.dev" | sudo tee -a /etc/hosts`
|
||||
|
||||
**Step 3:** Fetch kubeconfig of tcp cluster.
|
||||
|
||||
`kubectl get secrets tcp-1-admin-kubeconfig -o jsonpath='{.data.admin\.conf}' | base64 -d > kubeconfig`
|
||||
|
||||
**Step 4:** Test connectivity:
|
||||
|
||||
`kubectl --kubeconfig kubeconfig cluster-info`
|
||||
|
||||
|
||||
|
||||
### Multiple Tenant Control Planes
|
||||
|
||||
We can use the same Gateway resource for multiple Tenant Control Planes by using different hostnames per tenant cluster.
|
||||
Let us extend the above example for multiple tenant control planes behind single gateway (and LB IP).
|
||||
|
||||
```yaml
|
||||
# Gateway with wildcard hostname
|
||||
@@ -160,10 +155,13 @@ metadata:
|
||||
name: gateway
|
||||
spec:
|
||||
listeners:
|
||||
- hostname: '*.cluster.dev'
|
||||
name: kube-apiserver
|
||||
- name: kube-apiserver
|
||||
port: 6443
|
||||
# ...
|
||||
# note: we changed to wildcard hostname pattern matching
|
||||
# for cluster.dev
|
||||
hostname: '*.cluster.dev'
|
||||
|
||||
# ...
|
||||
---
|
||||
# Tenant Control Plane 1
|
||||
apiVersion: kamaji.clastix.io/v1alpha1
|
||||
@@ -194,17 +192,37 @@ spec:
|
||||
# ...
|
||||
```
|
||||
|
||||
Each Tenant Control Plane will get its own TLSRoutes with the respective hostnames, all routing through the same Gateway resource.
|
||||
Each Tenant Control Plane needs to use a different hostname. For each TCP, Kamaji creates a `TLSRoutes` resource with the respective hostnames, all `TLSRoutes` routing through the same Gateway resource.
|
||||
|
||||
You can check the Gateway status in the TenantControlPlane:
|
||||
|
||||
```bash
|
||||
kubectl get tenantcontrolplane tcp-1 -o yaml
|
||||
### Konnectivity
|
||||
|
||||
If konnectivity addon is enabled, Kamaji creates a separate TLSRoute for it. But this is hardcoded with the listener name `konnectivity-server` and port `8132`. All gateways mentioned in `spec.controlPlane.gateway.parentRefs` must contain a listener with the same configuration for the given hostname. Below is example configuration:
|
||||
```yaml
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: gateway
|
||||
namespace: default
|
||||
spec:
|
||||
gatewayClassName: envoy-gw-class
|
||||
listeners:
|
||||
- ...
|
||||
- ...
|
||||
- name: konnectivity-server
|
||||
port: 8132
|
||||
protocol: TLS
|
||||
hostname: 'tcp1.cluster.dev'
|
||||
tls:
|
||||
mode: Passthrough
|
||||
allowedRoutes:
|
||||
kinds:
|
||||
- group: gateway.networking.k8s.io
|
||||
kind: TLSRoute
|
||||
namespaces:
|
||||
from: All
|
||||
```
|
||||
|
||||
Look for the `status.kubernetesResources.gateway` and `status.addons.konnectivity.gateway` fields.
|
||||
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Gateway API Documentation](https://gateway-api.sigs.k8s.io/)
|
||||
|
||||
@@ -27982,6 +27982,21 @@ DataStoreStatus defines the observed state of DataStore.
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>ready</b></td>
|
||||
<td>boolean</td>
|
||||
<td>
|
||||
Ready returns if the DataStore is accepted and ready to get used:
|
||||
Kamaji will ensure certificates are available, or correctly referenced.<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#datastorestatusconditionsindex">conditions</a></b></td>
|
||||
<td>[]object</td>
|
||||
<td>
|
||||
Conditions contains the validation conditions for the given Datastore.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>observedGeneration</b></td>
|
||||
<td>integer</td>
|
||||
<td>
|
||||
@@ -28000,6 +28015,81 @@ DataStoreStatus defines the observed state of DataStore.
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="datastorestatusconditionsindex">`DataStore.status.conditions[index]`</span>
|
||||
|
||||
|
||||
Condition contains details for one aspect of the current state of this API Resource.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>lastTransitionTime</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
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.<br/>
|
||||
<br/>
|
||||
<i>Format</i>: date-time<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>message</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>reason</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
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.<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>status</b></td>
|
||||
<td>enum</td>
|
||||
<td>
|
||||
status of the condition, one of True, False, Unknown.<br/>
|
||||
<br/>
|
||||
<i>Enum</i>: True, False, Unknown<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>type</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
type of condition in CamelCase or in foo.example.com/CamelCase.<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>observedGeneration</b></td>
|
||||
<td>integer</td>
|
||||
<td>
|
||||
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.<br/>
|
||||
<br/>
|
||||
<i>Format</i>: int64<br/>
|
||||
<i>Minimum</i>: 0<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
### KubeconfigGenerator
|
||||
|
||||
|
||||
|
||||
@@ -48,7 +48,9 @@ var _ = Describe("Deploy a TenantControlPlane with Gateway API and Konnectivity"
|
||||
},
|
||||
GatewayParentRefs: []gatewayv1.ParentReference{
|
||||
{
|
||||
Name: "test-gateway",
|
||||
Name: "test-gateway",
|
||||
Port: pointer.To(gatewayv1.PortNumber(6443)),
|
||||
SectionName: pointer.To(gatewayv1.SectionName("cp-listener")),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -96,6 +98,35 @@ var _ = Describe("Deploy a TenantControlPlane with Gateway API and Konnectivity"
|
||||
StatusMustEqualTo(tcp, kamajiv1alpha1.VersionReady)
|
||||
})
|
||||
|
||||
It("Should create control plane TLSRoute preserving user-provided parentRef fields", func() {
|
||||
Eventually(func() error {
|
||||
route := &gatewayv1alpha2.TLSRoute{}
|
||||
if err := k8sClient.Get(context.Background(), types.NamespacedName{
|
||||
Name: tcp.Name,
|
||||
Namespace: tcp.Namespace,
|
||||
}, route); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(route.Spec.ParentRefs) == 0 {
|
||||
return fmt.Errorf("parentRefs is empty")
|
||||
}
|
||||
if route.Spec.ParentRefs[0].SectionName == nil {
|
||||
return fmt.Errorf("sectionName is nil")
|
||||
}
|
||||
if *route.Spec.ParentRefs[0].SectionName != gatewayv1.SectionName("cp-listener") {
|
||||
return fmt.Errorf("expected sectionName 'cp-listener', got '%s'", *route.Spec.ParentRefs[0].SectionName)
|
||||
}
|
||||
if route.Spec.ParentRefs[0].Port == nil {
|
||||
return fmt.Errorf("port is nil")
|
||||
}
|
||||
if *route.Spec.ParentRefs[0].Port != gatewayv1.PortNumber(6443) {
|
||||
return fmt.Errorf("expected port 6443, got '%d'", *route.Spec.ParentRefs[0].Port)
|
||||
}
|
||||
|
||||
return nil
|
||||
}).WithTimeout(time.Minute).Should(Succeed())
|
||||
})
|
||||
|
||||
It("Should create Konnectivity TLSRoute with correct sectionName", func() {
|
||||
Eventually(func() error {
|
||||
route := &gatewayv1alpha2.TLSRoute{}
|
||||
|
||||
@@ -48,7 +48,9 @@ var _ = Describe("Deploy a TenantControlPlane with Gateway API", func() {
|
||||
},
|
||||
GatewayParentRefs: []gatewayv1.ParentReference{
|
||||
{
|
||||
Name: "test-gateway",
|
||||
Name: "test-gateway",
|
||||
Port: pointer.To(gatewayv1.PortNumber(6443)),
|
||||
SectionName: pointer.To(gatewayv1.SectionName("cp-listener")),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -90,7 +92,7 @@ var _ = Describe("Deploy a TenantControlPlane with Gateway API", func() {
|
||||
StatusMustEqualTo(tcp, kamajiv1alpha1.VersionReady)
|
||||
})
|
||||
|
||||
It("Should create control plane TLSRoute with correct sectionName", func() {
|
||||
It("Should create control plane TLSRoute preserving user-provided parentRef fields", func() {
|
||||
Eventually(func() error {
|
||||
route := &gatewayv1alpha2.TLSRoute{}
|
||||
// TODO: Check ownership.
|
||||
@@ -106,8 +108,14 @@ var _ = Describe("Deploy a TenantControlPlane with Gateway API", func() {
|
||||
if route.Spec.ParentRefs[0].SectionName == nil {
|
||||
return fmt.Errorf("sectionName is nil")
|
||||
}
|
||||
if *route.Spec.ParentRefs[0].SectionName != gatewayv1.SectionName("kube-apiserver") {
|
||||
return fmt.Errorf("expected sectionName 'kube-apiserver', got '%s'", *route.Spec.ParentRefs[0].SectionName)
|
||||
if *route.Spec.ParentRefs[0].SectionName != gatewayv1.SectionName("cp-listener") {
|
||||
return fmt.Errorf("expected sectionName 'cp-listener', got '%s'", *route.Spec.ParentRefs[0].SectionName)
|
||||
}
|
||||
if route.Spec.ParentRefs[0].Port == nil {
|
||||
return fmt.Errorf("port is nil")
|
||||
}
|
||||
if *route.Spec.ParentRefs[0].Port != gatewayv1.PortNumber(6443) {
|
||||
return fmt.Errorf("expected port 6443, got '%d'", *route.Spec.ParentRefs[0].Port)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -212,7 +212,7 @@ func ScaleTenantControlPlane(tcp *kamajiv1alpha1.TenantControlPlane, replicas in
|
||||
Expect(err).To(Succeed())
|
||||
}
|
||||
|
||||
// CreateGatewayWithListeners creates a Gateway with both kube-apiserver and konnectivity-server listeners.
|
||||
// CreateGatewayWithListeners creates a Gateway with control plane and konnectivity-server listeners.
|
||||
func CreateGatewayWithListeners(gatewayName, namespace, gatewayClassName, hostname string) {
|
||||
GinkgoHelper()
|
||||
gateway := &gatewayv1.Gateway{
|
||||
@@ -224,7 +224,7 @@ func CreateGatewayWithListeners(gatewayName, namespace, gatewayClassName, hostna
|
||||
GatewayClassName: gatewayv1.ObjectName(gatewayClassName),
|
||||
Listeners: []gatewayv1.Listener{
|
||||
{
|
||||
Name: "kube-apiserver",
|
||||
Name: "cp-listener",
|
||||
Port: 6443,
|
||||
Protocol: gatewayv1.TLSProtocolType,
|
||||
Hostname: pointer.To(gatewayv1.Hostname(hostname)),
|
||||
|
||||
8
go.mod
8
go.mod
@@ -24,8 +24,8 @@ require (
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/testcontainers/testcontainers-go v0.40.0
|
||||
go.etcd.io/etcd/api/v3 v3.6.7
|
||||
go.etcd.io/etcd/client/v3 v3.6.7
|
||||
go.etcd.io/etcd/api/v3 v3.6.8
|
||||
go.etcd.io/etcd/client/v3 v3.6.8
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0
|
||||
k8s.io/api v0.35.0
|
||||
@@ -35,7 +35,7 @@ require (
|
||||
k8s.io/cluster-bootstrap v0.0.0
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/kubelet v0.0.0
|
||||
k8s.io/kubernetes v1.35.0
|
||||
k8s.io/kubernetes v1.35.1
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
|
||||
sigs.k8s.io/controller-runtime v0.22.4
|
||||
sigs.k8s.io/gateway-api v1.4.1
|
||||
@@ -148,7 +148,7 @@ require (
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.7 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.8 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
|
||||
16
go.sum
16
go.sum
@@ -369,12 +369,12 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.etcd.io/etcd/api/v3 v3.6.7 h1:7BNJ2gQmc3DNM+9cRkv7KkGQDayElg8x3X+tFDYS+E0=
|
||||
go.etcd.io/etcd/api/v3 v3.6.7/go.mod h1:xJ81TLj9hxrYYEDmXTeKURMeY3qEDN24hqe+q7KhbnI=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.7 h1:vvzgyozz46q+TyeGBuFzVuI53/yd133CHceNb/AhBVs=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.7/go.mod h1:2IVulJ3FZ/czIGl9T4lMF1uxzrhRahLqe+hSgy+Kh7Q=
|
||||
go.etcd.io/etcd/client/v3 v3.6.7 h1:9WqA5RpIBtdMxAy1ukXLAdtg2pAxNqW5NUoO2wQrE6U=
|
||||
go.etcd.io/etcd/client/v3 v3.6.7/go.mod h1:2XfROY56AXnUqGsvl+6k29wrwsSbEh1lAouQB1vHpeE=
|
||||
go.etcd.io/etcd/api/v3 v3.6.8 h1:gqb1VN92TAI6G2FiBvWcqKtHiIjr4SU2GdXxTwyexbM=
|
||||
go.etcd.io/etcd/api/v3 v3.6.8/go.mod h1:qyQj1HZPUV3B5cbAL8scG62+fyz5dSxxu0w8pn28N6Q=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.8 h1:Qs/5C0LNFiqXxYf2GU8MVjYUEXJ6sZaYOz0zEqQgy50=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.8/go.mod h1:GsiTRUZE2318PggZkAo6sWb6l8JLVrnckTNfbG8PWtw=
|
||||
go.etcd.io/etcd/client/v3 v3.6.8 h1:B3G76t1UykqAOrbio7s/EPatixQDkQBevN8/mwiplrY=
|
||||
go.etcd.io/etcd/client/v3 v3.6.8/go.mod h1:MVG4BpSIuumPi+ELF7wYtySETmoTWBHVcDoHdVupwt8=
|
||||
go.etcd.io/etcd/pkg/v3 v3.6.5 h1:byxWB4AqIKI4SBmquZUG1WGtvMfMaorXFoCcFbVeoxM=
|
||||
go.etcd.io/etcd/pkg/v3 v3.6.5/go.mod h1:uqrXrzmMIJDEy5j00bCqhVLzR5jEJIwDp5wTlLwPGOU=
|
||||
go.etcd.io/etcd/server/v3 v3.6.5 h1:4RbUb1Bd4y1WkBHmuF+cZII83JNQMuNXzyjwigQ06y0=
|
||||
@@ -549,8 +549,8 @@ k8s.io/kube-proxy v0.35.0 h1:erv2wYmGZ6nyu/FtmaIb+ORD3q2rfZ4Fhn7VXs/8cPQ=
|
||||
k8s.io/kube-proxy v0.35.0/go.mod h1:bd9lpN3uLLOOWc/CFZbkPEi9DTkzQQymbE8FqSU4bWk=
|
||||
k8s.io/kubelet v0.35.0 h1:8cgJHCBCKLYuuQ7/Pxb/qWbJfX1LXIw7790ce9xHq7c=
|
||||
k8s.io/kubelet v0.35.0/go.mod h1:ciRzAXn7C4z5iB7FhG1L2CGPPXLTVCABDlbXt/Zz8YA=
|
||||
k8s.io/kubernetes v1.35.0 h1:PUOojD8c8E3csMP5NX+nLLne6SGqZjrYCscptyBfWMY=
|
||||
k8s.io/kubernetes v1.35.0/go.mod h1:Tzk9Y9W/XUFFFgTUVg+BAowoFe+Pc7koGLuaiLHdcFg=
|
||||
k8s.io/kubernetes v1.35.1 h1:qmjXSCDPnOuXPuJb5pv+eLzpXhhlD09Jid1pG/OvFU8=
|
||||
k8s.io/kubernetes v1.35.1/go.mod h1:AaPpCpiS8oAqRbEwpY5r3RitLpwpVp5lVXKFkJril58=
|
||||
k8s.io/system-validators v1.12.1 h1:AY1+COTLJN/Sj0w9QzH1H0yvyF3Kl6CguMnh32WlcUU=
|
||||
k8s.io/system-validators v1.12.1/go.mod h1:awfSS706v9R12VC7u7K89FKfqVy44G+E0L1A0FX9Wmw=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
)
|
||||
|
||||
// CheckExists ensures that the default Datastore exists before starting the manager.
|
||||
func CheckExists(ctx context.Context, scheme *runtime.Scheme, datastoreName string) error {
|
||||
if datastoreName == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctrlClient, err := client.New(ctrl.GetConfigOrDie(), client.Options{Scheme: scheme})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create controlerruntime.Client: %w", err)
|
||||
}
|
||||
|
||||
if err = ctrlClient.Get(ctx, types.NamespacedName{Name: datastoreName}, &kamajiv1alpha1.DataStore{}); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return fmt.Errorf("the default Datastore %s doesn't exist", datastoreName)
|
||||
}
|
||||
|
||||
return fmt.Errorf("an error occurred during datastore retrieval: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -150,6 +150,10 @@ func (r *KubernetesGatewayResource) mutate(tcp *kamajiv1alpha1.TenantControlPlan
|
||||
tcp.Spec.ControlPlane.Gateway.AdditionalMetadata.Annotations)
|
||||
r.resource.SetAnnotations(annotations)
|
||||
|
||||
if tcp.Spec.ControlPlane.Gateway.GatewayParentRefs != nil {
|
||||
r.resource.Spec.ParentRefs = tcp.Spec.ControlPlane.Gateway.GatewayParentRefs
|
||||
}
|
||||
|
||||
serviceName := gatewayv1alpha2.ObjectName(tcp.Status.Kubernetes.Service.Name)
|
||||
servicePort := tcp.Status.Kubernetes.Service.Port
|
||||
|
||||
@@ -157,11 +161,6 @@ func (r *KubernetesGatewayResource) mutate(tcp *kamajiv1alpha1.TenantControlPlan
|
||||
return fmt.Errorf("service not ready, cannot create TLSRoute")
|
||||
}
|
||||
|
||||
if tcp.Spec.ControlPlane.Gateway.GatewayParentRefs != nil {
|
||||
// Copy parentRefs and explicitly set port and sectionName fields
|
||||
r.resource.Spec.ParentRefs = NewParentRefsSpecWithPortAndSection(tcp.Spec.ControlPlane.Gateway.GatewayParentRefs, servicePort, "kube-apiserver")
|
||||
}
|
||||
|
||||
rule := gatewayv1alpha2.TLSRouteRule{
|
||||
BackendRefs: []gatewayv1alpha2.BackendRef{
|
||||
{
|
||||
|
||||
@@ -102,30 +102,6 @@ var _ = Describe("KubernetesGatewayResource", func() {
|
||||
Expect(shouldUpdate).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should set port and sectionName in parentRefs, overriding any user-provided values", func() {
|
||||
customPort := gatewayv1.PortNumber(9999)
|
||||
customSectionName := gatewayv1.SectionName("custom")
|
||||
tcp.Spec.ControlPlane.Gateway.GatewayParentRefs[0].Port = &customPort
|
||||
tcp.Spec.ControlPlane.Gateway.GatewayParentRefs[0].SectionName = &customSectionName
|
||||
|
||||
err := resource.Define(ctx, tcp)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err = resource.CreateOrUpdate(ctx, tcp)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
route := &gatewayv1alpha2.TLSRoute{}
|
||||
err = resource.Client.Get(ctx, client.ObjectKey{Name: tcp.Name, Namespace: tcp.Namespace}, route)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(route.Spec.ParentRefs).To(HaveLen(1))
|
||||
Expect(route.Spec.ParentRefs[0].Port).NotTo(BeNil())
|
||||
Expect(*route.Spec.ParentRefs[0].Port).To(Equal(tcp.Status.Kubernetes.Service.Port))
|
||||
Expect(*route.Spec.ParentRefs[0].Port).NotTo(Equal(customPort))
|
||||
Expect(route.Spec.ParentRefs[0].SectionName).NotTo(BeNil())
|
||||
Expect(*route.Spec.ParentRefs[0].SectionName).To(Equal(gatewayv1.SectionName("kube-apiserver")))
|
||||
Expect(*route.Spec.ParentRefs[0].SectionName).NotTo(Equal(customSectionName))
|
||||
})
|
||||
|
||||
It("should handle multiple parentRefs correctly", func() {
|
||||
namespace := gatewayv1.Namespace("default")
|
||||
tcp.Spec.ControlPlane.Gateway.GatewayParentRefs = []gatewayv1alpha2.ParentReference{
|
||||
@@ -149,13 +125,12 @@ var _ = Describe("KubernetesGatewayResource", func() {
|
||||
err = resource.Client.Get(ctx, client.ObjectKey{Name: tcp.Name, Namespace: tcp.Namespace}, route)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(route.Spec.ParentRefs).To(HaveLen(2))
|
||||
|
||||
for i := range route.Spec.ParentRefs {
|
||||
Expect(route.Spec.ParentRefs[i].Port).NotTo(BeNil())
|
||||
Expect(*route.Spec.ParentRefs[i].Port).To(Equal(tcp.Status.Kubernetes.Service.Port))
|
||||
Expect(route.Spec.ParentRefs[i].SectionName).NotTo(BeNil())
|
||||
Expect(*route.Spec.ParentRefs[i].SectionName).To(Equal(gatewayv1.SectionName("kube-apiserver")))
|
||||
}
|
||||
Expect(route.Spec.ParentRefs[0].Name).To(Equal(gatewayv1alpha2.ObjectName("test-gateway-1")))
|
||||
Expect(route.Spec.ParentRefs[0].Namespace).NotTo(BeNil())
|
||||
Expect(*route.Spec.ParentRefs[0].Namespace).To(Equal(namespace))
|
||||
Expect(route.Spec.ParentRefs[1].Name).To(Equal(gatewayv1alpha2.ObjectName("test-gateway-2")))
|
||||
Expect(route.Spec.ParentRefs[1].Namespace).NotTo(BeNil())
|
||||
Expect(*route.Spec.ParentRefs[1].Namespace).To(Equal(namespace))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -292,81 +267,4 @@ var _ = Describe("KubernetesGatewayResource", func() {
|
||||
Expect(listener.Port).To(Equal(gatewayv1.PortNumber(80)))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("NewParentRefsSpecWithPortAndSection", func() {
|
||||
var (
|
||||
parentRefs []gatewayv1.ParentReference
|
||||
testPort int32
|
||||
testSectionName string
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
namespace := gatewayv1.Namespace("default")
|
||||
namespace2 := gatewayv1.Namespace("other")
|
||||
testPort = int32(6443)
|
||||
testSectionName = "kube-apiserver"
|
||||
originalPort := gatewayv1.PortNumber(9999)
|
||||
originalSectionName := gatewayv1.SectionName("original")
|
||||
parentRefs = []gatewayv1.ParentReference{
|
||||
{
|
||||
Name: "test-gateway-1",
|
||||
Namespace: &namespace,
|
||||
Port: &originalPort,
|
||||
SectionName: &originalSectionName,
|
||||
},
|
||||
{
|
||||
Name: "test-gateway-2",
|
||||
Namespace: &namespace2,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
It("should create copy of parentRefs with port and sectionName set", func() {
|
||||
result := resources.NewParentRefsSpecWithPortAndSection(parentRefs, testPort, testSectionName)
|
||||
|
||||
Expect(result).To(HaveLen(2))
|
||||
for i := range result {
|
||||
Expect(result[i].Name).To(Equal(parentRefs[i].Name))
|
||||
Expect(result[i].Namespace).To(Equal(parentRefs[i].Namespace))
|
||||
Expect(result[i].Port).NotTo(BeNil())
|
||||
Expect(*result[i].Port).To(Equal(testPort))
|
||||
Expect(result[i].SectionName).NotTo(BeNil())
|
||||
Expect(*result[i].SectionName).To(Equal(gatewayv1.SectionName(testSectionName)))
|
||||
}
|
||||
})
|
||||
|
||||
It("should not modify original parentRefs", func() {
|
||||
// Store original values for verification
|
||||
originalFirstPort := parentRefs[0].Port
|
||||
originalFirstSectionName := parentRefs[0].SectionName
|
||||
originalSecondPort := parentRefs[1].Port
|
||||
originalSecondSectionName := parentRefs[1].SectionName
|
||||
|
||||
result := resources.NewParentRefsSpecWithPortAndSection(parentRefs, testPort, testSectionName)
|
||||
|
||||
// Original should remain unchanged
|
||||
Expect(parentRefs[0].Port).To(Equal(originalFirstPort))
|
||||
Expect(parentRefs[0].SectionName).To(Equal(originalFirstSectionName))
|
||||
Expect(parentRefs[1].Port).To(Equal(originalSecondPort))
|
||||
Expect(parentRefs[1].SectionName).To(Equal(originalSecondSectionName))
|
||||
|
||||
// Result should have new values
|
||||
Expect(result[0].Port).NotTo(BeNil())
|
||||
Expect(*result[0].Port).To(Equal(testPort))
|
||||
Expect(result[0].SectionName).NotTo(BeNil())
|
||||
Expect(*result[0].SectionName).To(Equal(gatewayv1.SectionName(testSectionName)))
|
||||
Expect(result[1].Port).NotTo(BeNil())
|
||||
Expect(*result[1].Port).To(Equal(testPort))
|
||||
Expect(result[1].SectionName).NotTo(BeNil())
|
||||
Expect(*result[1].SectionName).To(Equal(gatewayv1.SectionName(testSectionName)))
|
||||
})
|
||||
|
||||
It("should handle empty parentRefs slice", func() {
|
||||
parentRefs = []gatewayv1.ParentReference{}
|
||||
|
||||
result := resources.NewParentRefsSpecWithPortAndSection(parentRefs, testPort, testSectionName)
|
||||
|
||||
Expect(result).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -226,16 +226,3 @@ func BuildGatewayAccessPointsStatus(ctx context.Context, c client.Client, route
|
||||
|
||||
return accessPoints, nil
|
||||
}
|
||||
|
||||
// NewParentRefsSpecWithPortAndSection creates a copy of parentRefs with port and sectionName set for each reference.
|
||||
func NewParentRefsSpecWithPortAndSection(parentRefs []gatewayv1.ParentReference, port int32, sectionName string) []gatewayv1.ParentReference {
|
||||
result := make([]gatewayv1.ParentReference, len(parentRefs))
|
||||
sectionNamePtr := gatewayv1.SectionName(sectionName)
|
||||
for i, parentRef := range parentRefs {
|
||||
result[i] = *parentRef.DeepCopy()
|
||||
result[i].Port = &port
|
||||
result[i].SectionName = §ionNamePtr
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func (r *KubernetesServiceResource) CleanUp(context.Context, *kamajiv1alpha1.Ten
|
||||
}
|
||||
|
||||
func (r *KubernetesServiceResource) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
|
||||
tenantControlPlane.Status.Kubernetes.Service.ServiceStatus = r.resource.Status
|
||||
tenantControlPlane.Status.Kubernetes.Service.ServiceStatus = StripLoadBalancerPortsFromServiceStatus(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
|
||||
|
||||
@@ -182,7 +182,7 @@ func (r *KubernetesKonnectivityGatewayResource) mutate(tcp *kamajiv1alpha1.Tenan
|
||||
if tcp.Spec.ControlPlane.Gateway.GatewayParentRefs == nil {
|
||||
return fmt.Errorf("control plane gateway parentRefs are not specified")
|
||||
}
|
||||
r.resource.Spec.ParentRefs = resources.NewParentRefsSpecWithPortAndSection(tcp.Spec.ControlPlane.Gateway.GatewayParentRefs, servicePort, "konnectivity-server")
|
||||
r.resource.Spec.ParentRefs = newParentRefsSpecWithPortAndSection(tcp.Spec.ControlPlane.Gateway.GatewayParentRefs, servicePort, "konnectivity-server")
|
||||
|
||||
rule := gatewayv1alpha2.TLSRouteRule{
|
||||
BackendRefs: []gatewayv1alpha2.BackendRef{
|
||||
@@ -230,3 +230,16 @@ func (r *KubernetesKonnectivityGatewayResource) CreateOrUpdate(ctx context.Conte
|
||||
func (r *KubernetesKonnectivityGatewayResource) GetName() string {
|
||||
return "konnectivity_gateway_routes"
|
||||
}
|
||||
|
||||
// newParentRefsSpecWithPortAndSection creates a copy of parentRefs with port and sectionName set for each reference.
|
||||
func newParentRefsSpecWithPortAndSection(parentRefs []gatewayv1.ParentReference, port int32, sectionName string) []gatewayv1.ParentReference {
|
||||
result := make([]gatewayv1.ParentReference, len(parentRefs))
|
||||
sectionNamePtr := gatewayv1.SectionName(sectionName)
|
||||
for i, parentRef := range parentRefs {
|
||||
result[i] = *parentRef.DeepCopy()
|
||||
result[i].Port = &port
|
||||
result[i].SectionName = §ionNamePtr
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ func (r *ServiceResource) UpdateTenantControlPlaneStatus(_ context.Context, tena
|
||||
tenantControlPlane.Status.Addons.Konnectivity.Service.Name = r.resource.GetName()
|
||||
tenantControlPlane.Status.Addons.Konnectivity.Service.Namespace = r.resource.GetNamespace()
|
||||
tenantControlPlane.Status.Addons.Konnectivity.Service.Port = r.resource.Spec.Ports[1].Port
|
||||
tenantControlPlane.Status.Addons.Konnectivity.Service.ServiceStatus = r.resource.Status
|
||||
tenantControlPlane.Status.Addons.Konnectivity.Service.ServiceStatus = resources.StripLoadBalancerPortsFromServiceStatus(r.resource.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -132,3 +132,18 @@ func getStoredKubeadmConfiguration(ctx context.Context, client client.Client, tm
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func StripLoadBalancerPortsFromServiceStatus(s corev1.ServiceStatus) corev1.ServiceStatus {
|
||||
sanitized := s
|
||||
|
||||
if len(s.LoadBalancer.Ingress) > 0 {
|
||||
sanitized.LoadBalancer.Ingress = make([]corev1.LoadBalancerIngress, len(s.LoadBalancer.Ingress))
|
||||
copy(sanitized.LoadBalancer.Ingress, s.LoadBalancer.Ingress)
|
||||
}
|
||||
|
||||
for i := range sanitized.LoadBalancer.Ingress {
|
||||
sanitized.LoadBalancer.Ingress[i].Ports = nil
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
111
internal/resources/resource_test.go
Normal file
111
internal/resources/resource_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func TestStripLoadBalancerPortsFromServiceStatus(t *testing.T) {
|
||||
ipModeProxy := corev1.LoadBalancerIPModeProxy
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input corev1.ServiceStatus
|
||||
assert func(t *testing.T, orig, got corev1.ServiceStatus)
|
||||
}{
|
||||
{
|
||||
name: "ip ingress with ports and ipMode",
|
||||
input: corev1.ServiceStatus{
|
||||
LoadBalancer: corev1.LoadBalancerStatus{
|
||||
Ingress: []corev1.LoadBalancerIngress{
|
||||
{
|
||||
IP: "172.18.0.3",
|
||||
IPMode: &ipModeProxy,
|
||||
Ports: []corev1.PortStatus{
|
||||
{Port: 6443, Protocol: corev1.ProtocolTCP},
|
||||
{Port: 8132, Protocol: corev1.ProtocolTCP},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
assert: func(t *testing.T, orig, got corev1.ServiceStatus) {
|
||||
t.Helper()
|
||||
if got.LoadBalancer.Ingress[0].Ports != nil {
|
||||
t.Fatalf("expected ports stripped, got %#v", got.LoadBalancer.Ingress[0].Ports)
|
||||
}
|
||||
if got.LoadBalancer.Ingress[0].IP != "172.18.0.3" {
|
||||
t.Fatalf("IP not preserved")
|
||||
}
|
||||
if got.LoadBalancer.Ingress[0].IPMode == nil || *got.LoadBalancer.Ingress[0].IPMode != ipModeProxy {
|
||||
t.Fatalf("IPMode not preserved")
|
||||
}
|
||||
if orig.LoadBalancer.Ingress[0].Ports == nil {
|
||||
t.Fatalf("original ports mutated")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hostname ingress with ports",
|
||||
input: corev1.ServiceStatus{
|
||||
LoadBalancer: corev1.LoadBalancerStatus{
|
||||
Ingress: []corev1.LoadBalancerIngress{
|
||||
{
|
||||
Hostname: "example.local",
|
||||
Ports: []corev1.PortStatus{
|
||||
{Port: 6443, Protocol: corev1.ProtocolTCP},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
assert: func(t *testing.T, orig, got corev1.ServiceStatus) {
|
||||
t.Helper()
|
||||
if got.LoadBalancer.Ingress[0].Ports != nil {
|
||||
t.Fatalf("expected ports stripped")
|
||||
}
|
||||
if got.LoadBalancer.Ingress[0].Hostname != "example.local" {
|
||||
t.Fatalf("hostname not preserved")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no ingress",
|
||||
input: corev1.ServiceStatus{},
|
||||
assert: func(t *testing.T, _, got corev1.ServiceStatus) {
|
||||
t.Helper()
|
||||
if len(got.LoadBalancer.Ingress) != 0 {
|
||||
t.Fatalf("expected no ingress")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ingress with nil ports",
|
||||
input: corev1.ServiceStatus{
|
||||
LoadBalancer: corev1.LoadBalancerStatus{
|
||||
Ingress: []corev1.LoadBalancerIngress{
|
||||
{IP: "10.0.0.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
assert: func(t *testing.T, _, got corev1.ServiceStatus) {
|
||||
t.Helper()
|
||||
if got.LoadBalancer.Ingress[0].Ports != nil {
|
||||
t.Fatalf("expected ports to stay nil")
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
orig := tt.input
|
||||
got := StripLoadBalancerPortsFromServiceStatus(tt.input)
|
||||
tt.assert(t, orig, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gomodules.xyz/jsonpatch/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
)
|
||||
|
||||
type DataStoreValidation struct {
|
||||
Client client.Client
|
||||
}
|
||||
|
||||
func (d DataStoreValidation) OnCreate(object runtime.Object) AdmissionResponse {
|
||||
return func(ctx context.Context, _ admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
|
||||
ds := object.(*kamajiv1alpha1.DataStore) //nolint:forcetypeassert
|
||||
|
||||
return nil, d.validate(ctx, *ds)
|
||||
}
|
||||
}
|
||||
|
||||
func (d DataStoreValidation) OnDelete(object runtime.Object) AdmissionResponse {
|
||||
return func(ctx context.Context, _ admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
|
||||
ds := object.(*kamajiv1alpha1.DataStore) //nolint:forcetypeassert
|
||||
|
||||
tcpList := &kamajiv1alpha1.TenantControlPlaneList{}
|
||||
if err := d.Client.List(ctx, tcpList, client.MatchingFieldsSelector{Selector: fields.OneTermEqualSelector(kamajiv1alpha1.TenantControlPlaneUsedDataStoreKey, ds.GetName())}); err != nil {
|
||||
return nil, fmt.Errorf("cannot retrieve TenantControlPlane list used by the DataStore: %w", err)
|
||||
}
|
||||
|
||||
if len(tcpList.Items) > 0 {
|
||||
return nil, fmt.Errorf("the DataStore is used by multiple TenantControlPlanes and cannot be removed")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d DataStoreValidation) OnUpdate(object runtime.Object, oldObj runtime.Object) AdmissionResponse {
|
||||
return func(ctx context.Context, _ admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
|
||||
newDs, oldDs := object.(*kamajiv1alpha1.DataStore), oldObj.(*kamajiv1alpha1.DataStore) //nolint:forcetypeassert
|
||||
|
||||
if oldDs.Spec.Driver != newDs.Spec.Driver {
|
||||
return nil, fmt.Errorf("driver of a DataStore cannot be changed")
|
||||
}
|
||||
|
||||
return nil, d.validate(ctx, *newDs)
|
||||
}
|
||||
}
|
||||
|
||||
func (d DataStoreValidation) validate(ctx context.Context, ds kamajiv1alpha1.DataStore) error {
|
||||
if ds.Spec.BasicAuth != nil {
|
||||
if err := d.validateBasicAuth(ctx, ds); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return d.validateTLSConfig(ctx, ds)
|
||||
}
|
||||
|
||||
func (d DataStoreValidation) validateBasicAuth(ctx context.Context, ds kamajiv1alpha1.DataStore) error {
|
||||
if err := d.validateContentReference(ctx, ds.Spec.BasicAuth.Password); err != nil {
|
||||
return fmt.Errorf("basic-auth password is not valid, %w", err)
|
||||
}
|
||||
|
||||
if err := d.validateContentReference(ctx, ds.Spec.BasicAuth.Username); err != nil {
|
||||
return fmt.Errorf("basic-auth username is not valid, %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d DataStoreValidation) validateTLSConfig(ctx context.Context, ds kamajiv1alpha1.DataStore) error {
|
||||
if ds.Spec.TLSConfig == nil && ds.Spec.Driver != kamajiv1alpha1.EtcdDriver {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := d.validateContentReference(ctx, ds.Spec.TLSConfig.CertificateAuthority.Certificate); err != nil {
|
||||
return fmt.Errorf("CA certificate is not valid, %w", err)
|
||||
}
|
||||
|
||||
if ds.Spec.Driver == kamajiv1alpha1.EtcdDriver {
|
||||
if ds.Spec.TLSConfig.CertificateAuthority.PrivateKey == nil {
|
||||
return fmt.Errorf("CA private key is required when using the etcd driver")
|
||||
}
|
||||
|
||||
if ds.Spec.TLSConfig.ClientCertificate == nil {
|
||||
return fmt.Errorf("client certificate is required when using the etcd driver")
|
||||
}
|
||||
}
|
||||
|
||||
if ds.Spec.TLSConfig.CertificateAuthority.PrivateKey != nil {
|
||||
if err := d.validateContentReference(ctx, *ds.Spec.TLSConfig.CertificateAuthority.PrivateKey); err != nil {
|
||||
return fmt.Errorf("CA private key is not valid, %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if ds.Spec.TLSConfig.ClientCertificate != nil {
|
||||
if err := d.validateContentReference(ctx, ds.Spec.TLSConfig.ClientCertificate.Certificate); err != nil {
|
||||
return fmt.Errorf("client certificate is not valid, %w", err)
|
||||
}
|
||||
|
||||
if err := d.validateContentReference(ctx, ds.Spec.TLSConfig.ClientCertificate.PrivateKey); err != nil {
|
||||
return fmt.Errorf("client private key is not valid, %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d DataStoreValidation) validateContentReference(ctx context.Context, ref kamajiv1alpha1.ContentRef) error {
|
||||
switch {
|
||||
case len(ref.Content) > 0:
|
||||
return nil
|
||||
case ref.SecretRef == nil:
|
||||
return fmt.Errorf("the Secret reference is mandatory when bare content is not specified")
|
||||
case len(ref.SecretRef.SecretReference.Name) == 0:
|
||||
return fmt.Errorf("the Secret reference name is mandatory")
|
||||
case len(ref.SecretRef.SecretReference.Namespace) == 0:
|
||||
return fmt.Errorf("the Secret reference namespace is mandatory")
|
||||
}
|
||||
|
||||
if err := d.Client.Get(ctx, types.NamespacedName{Name: ref.SecretRef.SecretReference.Name, Namespace: ref.SecretRef.SecretReference.Namespace}, &corev1.Secret{}); err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
return fmt.Errorf("secret %s/%s is not found", ref.SecretRef.SecretReference.Namespace, ref.SecretRef.SecretReference.Name)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package routes
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
//+kubebuilder:webhook:path=/validate--v1-secret,mutating=false,failurePolicy=ignore,sideEffects=None,groups="",resources=secrets,verbs=delete,versions=v1,name=vdatastoresecrets.kb.io,admissionReviewVersions=v1
|
||||
|
||||
type DataStoreSecrets struct{}
|
||||
|
||||
func (d DataStoreSecrets) GetPath() string {
|
||||
return "/validate--v1-secret"
|
||||
}
|
||||
|
||||
func (d DataStoreSecrets) GetObject() runtime.Object {
|
||||
return &corev1.Secret{}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package routes
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
)
|
||||
|
||||
//+kubebuilder:webhook:path=/validate-kamaji-clastix-io-v1alpha1-datastore,mutating=false,failurePolicy=fail,sideEffects=None,groups=kamaji.clastix.io,resources=datastores,verbs=create;update;delete,versions=v1alpha1,name=vdatastore.kb.io,admissionReviewVersions=v1
|
||||
|
||||
type DataStoreValidate struct{}
|
||||
|
||||
func (d DataStoreValidate) GetPath() string {
|
||||
return "/validate-kamaji-clastix-io-v1alpha1-datastore"
|
||||
}
|
||||
|
||||
func (d DataStoreValidate) GetObject() runtime.Object {
|
||||
return &kamajiv1alpha1.DataStore{}
|
||||
}
|
||||
Reference in New Issue
Block a user