mirror of
https://github.com/clastix/kamaji.git
synced 2026-02-28 16:50:29 +00:00
Compare commits
14 Commits
edge-25.12
...
edge-26.2.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9c2c0de89 | ||
|
|
b40428825d | ||
|
|
c0316956a8 | ||
|
|
4c8f77e883 | ||
|
|
490697ec55 | ||
|
|
3b44dfc210 | ||
|
|
faf26b2254 | ||
|
|
87242ff005 | ||
|
|
b6b4888177 | ||
|
|
bd0c7d354d | ||
|
|
e1c6aa8459 | ||
|
|
11c315289c | ||
|
|
0428024946 | ||
|
|
f55df56eac |
6
Makefile
6
Makefile
@@ -248,6 +248,10 @@ gateway-api:
|
||||
kubectl apply --server-side -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.
|
||||
$(HELM) upgrade --install eg oci://docker.io/envoyproxy/gateway-helm --version v1.6.1 -n envoy-gateway-system --create-namespace
|
||||
kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-gateway --for=condition=Available
|
||||
|
||||
load: kind
|
||||
$(KIND) load docker-image --name kamaji ${CONTAINER_REPOSITORY}:${VERSION}
|
||||
|
||||
@@ -261,7 +265,7 @@ cleanup: kind
|
||||
$(KIND) delete cluster --name kamaji
|
||||
|
||||
.PHONY: e2e
|
||||
e2e: env build load helm ginkgo cert-manager gateway-api ## Create a KinD cluster, install Kamaji on it and run the test suite.
|
||||
e2e: env build load helm ginkgo cert-manager gateway-api envoy-gateway ## Create a KinD cluster, install Kamaji on it and run the test suite.
|
||||
$(HELM) upgrade --debug --install kamaji-crds ./charts/kamaji-crds --create-namespace --namespace kamaji-system
|
||||
$(HELM) repo add clastix https://clastix.github.io/charts
|
||||
$(HELM) dependency build ./charts/kamaji
|
||||
|
||||
@@ -90,6 +90,9 @@ type SecretReference struct {
|
||||
|
||||
// DataStoreStatus defines the observed state of DataStore.
|
||||
type DataStoreStatus struct {
|
||||
// ObservedGeneration represents the .metadata.generation that was last reconciled.
|
||||
// +optional
|
||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||
// List of the Tenant Control Planes, namespaced named, using this data store.
|
||||
UsedBy []string `json:"usedBy,omitempty"`
|
||||
}
|
||||
|
||||
@@ -66,6 +66,9 @@ type KubeconfigGeneratorStatusError struct {
|
||||
|
||||
// KubeconfigGeneratorStatus defines the observed state of KubeconfigGenerator.
|
||||
type KubeconfigGeneratorStatus struct {
|
||||
// ObservedGeneration represents the .metadata.generation that was last reconciled.
|
||||
// +optional
|
||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||
// Resources is the sum of targeted TenantControlPlane objects.
|
||||
//+kubebuilder:default=0
|
||||
Resources int `json:"resources"`
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -27,12 +26,12 @@ func (in *TenantControlPlane) AssignedControlPlaneAddress() (string, int32, erro
|
||||
|
||||
address, portString, err := net.SplitHostPort(in.Status.ControlPlaneEndpoint)
|
||||
if err != nil {
|
||||
return "", 0, errors.Wrap(err, "cannot split host port from Tenant Control Plane endpoint")
|
||||
return "", 0, fmt.Errorf("cannot split host port from Tenant Control Plane endpoint: %w", err)
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(portString)
|
||||
if err != nil {
|
||||
return "", 0, errors.Wrap(err, "cannot convert Tenant Control Plane port from endpoint")
|
||||
return "", 0, fmt.Errorf("cannot convert Tenant Control Plane port from endpoint: %w", err)
|
||||
}
|
||||
|
||||
return address, int32(port), nil
|
||||
@@ -47,7 +46,7 @@ func (in *TenantControlPlane) DeclaredControlPlaneAddress(ctx context.Context, c
|
||||
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")
|
||||
return "", fmt.Errorf("cannot retrieve Service for the TenantControlPlane: %w", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
|
||||
83
api/v1alpha1/tenantcontrolplane_jsonpatch.go
Normal file
83
api/v1alpha1/tenantcontrolplane_jsonpatch.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
)
|
||||
|
||||
type JSONPatches []JSONPatch
|
||||
|
||||
type JSONPatch struct {
|
||||
// Op is the RFC 6902 JSON Patch operation.
|
||||
//+kubebuilder:validation:Enum=add;remove;replace;move;copy;test
|
||||
Op string `json:"op"`
|
||||
// Path specifies the target location in the JSON document. Use "/" to separate keys; "-" for appending to arrays.
|
||||
Path string `json:"path"`
|
||||
// From specifies the source location for move or copy operations.
|
||||
From string `json:"from,omitempty"`
|
||||
// Value is the operation value to be used when Op is add, replace, test.
|
||||
Value *apiextensionsv1.JSON `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (p JSONPatches) ToJSON() ([]byte, error) {
|
||||
if len(p) == 0 {
|
||||
return []byte("[]"), nil
|
||||
}
|
||||
|
||||
buf := make([]byte, 0, 256)
|
||||
buf = append(buf, '[')
|
||||
|
||||
for i, patch := range p {
|
||||
if i > 0 {
|
||||
buf = append(buf, ',')
|
||||
}
|
||||
|
||||
buf = append(buf, '{')
|
||||
|
||||
buf = append(buf, `"op":"`...)
|
||||
buf = appendEscapedString(buf, patch.Op)
|
||||
buf = append(buf, '"')
|
||||
|
||||
buf = append(buf, `,"path":"`...)
|
||||
buf = appendEscapedString(buf, patch.Path)
|
||||
buf = append(buf, '"')
|
||||
|
||||
if patch.From != "" {
|
||||
buf = append(buf, `,"from":"`...)
|
||||
buf = appendEscapedString(buf, patch.From)
|
||||
buf = append(buf, '"')
|
||||
}
|
||||
|
||||
if patch.Value != nil {
|
||||
buf = append(buf, `,"value":`...)
|
||||
buf = append(buf, patch.Value.Raw...)
|
||||
}
|
||||
|
||||
buf = append(buf, '}')
|
||||
}
|
||||
|
||||
buf = append(buf, ']')
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func appendEscapedString(dst []byte, s string) []byte {
|
||||
for i := range s {
|
||||
switch s[i] {
|
||||
case '\\', '"':
|
||||
dst = append(dst, '\\', s[i])
|
||||
case '\n':
|
||||
dst = append(dst, '\\', 'n')
|
||||
case '\r':
|
||||
dst = append(dst, '\\', 'r')
|
||||
case '\t':
|
||||
dst = append(dst, '\\', 't')
|
||||
default:
|
||||
dst = append(dst, s[i])
|
||||
}
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
||||
@@ -139,6 +139,7 @@ type KonnectivityStatus struct {
|
||||
ClusterRoleBinding ExternalKubernetesObjectStatus `json:"clusterrolebinding,omitempty"`
|
||||
Agent KonnectivityAgentStatus `json:"agent,omitempty"`
|
||||
Service KubernetesServiceStatus `json:"service,omitempty"`
|
||||
Gateway *KubernetesGatewayStatus `json:"gateway,omitempty"`
|
||||
}
|
||||
|
||||
type KonnectivityConfigMap struct {
|
||||
@@ -161,6 +162,9 @@ type AddonsStatus struct {
|
||||
|
||||
// TenantControlPlaneStatus defines the observed state of TenantControlPlane.
|
||||
type TenantControlPlaneStatus struct {
|
||||
// ObservedGeneration represents the .metadata.generation that was last reconciled.
|
||||
// +optional
|
||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||
// Storage Status contains information about Kubernetes storage system
|
||||
Storage StorageStatus `json:"storage,omitempty"`
|
||||
// Certificates contains information about the different certificates
|
||||
|
||||
@@ -68,6 +68,12 @@ const (
|
||||
)
|
||||
|
||||
type KubeletSpec struct {
|
||||
// ConfigurationJSONPatches contains the RFC 6902 JSON patches to customise the kubeadm generate configuration,
|
||||
// useful to customise and mangling the configuration according to your needs;
|
||||
// e.g.: configuring the cgroup driver used by Kubelet is possible via the following patch:
|
||||
//
|
||||
// [{"op": "replace", "path": "/cgroupDriver", "value": "systemd"}]
|
||||
ConfigurationJSONPatches JSONPatches `json:"configurationJSONPatches,omitempty"`
|
||||
// Ordered list of the preferred NodeAddressTypes to use for kubelet connections.
|
||||
// Default to InternalIP, ExternalIP, Hostname.
|
||||
//+kubebuilder:default={"InternalIP","ExternalIP","Hostname"}
|
||||
@@ -76,6 +82,8 @@ type KubeletSpec struct {
|
||||
PreferredAddressTypes []KubeletPreferredAddressType `json:"preferredAddressTypes,omitempty"`
|
||||
// CGroupFS defines the cgroup driver for Kubelet
|
||||
// https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/configure-cgroup-driver/
|
||||
//
|
||||
// Deprecated: use ConfigurationJSONPatches.
|
||||
CGroupFS CGroupDriver `json:"cgroupfs,omitempty"`
|
||||
}
|
||||
|
||||
@@ -147,6 +155,7 @@ 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"`
|
||||
@@ -289,7 +298,7 @@ var (
|
||||
KonnectivityAgentModeDeployment KonnectivityAgentMode = "Deployment"
|
||||
)
|
||||
|
||||
//+kubebuilder:validation:XValidation:rule="!(self.mode == 'DaemonSet' && has(self.replicas) && self.replicas != 0) && !(self.mode == 'Deployment' && self.replicas == 0)",message="replicas must be 0 when mode is DaemonSet, and greater than 0 when mode is Deployment"
|
||||
//+kubebuilder:validation:XValidation:rule="!(self.mode == 'DaemonSet' && has(self.replicas) && self.replicas != 0) && !(self.mode == 'Deployment' && has(self.replicas) && self.replicas == 0)",message="replicas must be 0 (or unset) when mode is DaemonSet, and greater than 0 (or unset) when mode is Deployment"
|
||||
|
||||
type KonnectivityAgentSpec struct {
|
||||
// AgentImage defines the container image for Konnectivity's agent.
|
||||
@@ -318,7 +327,7 @@ type KonnectivityAgentSpec struct {
|
||||
// Replicas defines the number of replicas when Mode is Deployment.
|
||||
// Must be 0 if Mode is DaemonSet.
|
||||
//+kubebuilder:validation:Optional
|
||||
Replicas int32 `json:"replicas,omitempty"`
|
||||
Replicas *int32 `json:"replicas,omitempty"`
|
||||
}
|
||||
|
||||
// KonnectivitySpec defines the spec for Konnectivity.
|
||||
|
||||
@@ -9,8 +9,9 @@ package v1alpha1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/gateway-api/apis/v1"
|
||||
apisv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
@@ -812,7 +813,7 @@ func (in *GatewayAccessPoint) DeepCopyInto(out *GatewayAccessPoint) {
|
||||
*out = *in
|
||||
if in.Type != nil {
|
||||
in, out := &in.Type, &out.Type
|
||||
*out = new(v1.AddressType)
|
||||
*out = new(apisv1.AddressType)
|
||||
**out = **in
|
||||
}
|
||||
if in.URLs != nil {
|
||||
@@ -853,7 +854,7 @@ func (in *GatewaySpec) DeepCopyInto(out *GatewaySpec) {
|
||||
in.AdditionalMetadata.DeepCopyInto(&out.AdditionalMetadata)
|
||||
if in.GatewayParentRefs != nil {
|
||||
in, out := &in.GatewayParentRefs, &out.GatewayParentRefs
|
||||
*out = make([]v1.ParentReference, len(*in))
|
||||
*out = make([]apisv1.ParentReference, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
@@ -901,6 +902,47 @@ func (in *IngressSpec) DeepCopy() *IngressSpec {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *JSONPatch) DeepCopyInto(out *JSONPatch) {
|
||||
*out = *in
|
||||
if in.Value != nil {
|
||||
in, out := &in.Value, &out.Value
|
||||
*out = new(v1.JSON)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JSONPatch.
|
||||
func (in *JSONPatch) DeepCopy() *JSONPatch {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(JSONPatch)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in JSONPatches) DeepCopyInto(out *JSONPatches) {
|
||||
{
|
||||
in := &in
|
||||
*out = make(JSONPatches, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JSONPatches.
|
||||
func (in JSONPatches) DeepCopy() JSONPatches {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(JSONPatches)
|
||||
in.DeepCopyInto(out)
|
||||
return *out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *KonnectivityAgentSpec) DeepCopyInto(out *KonnectivityAgentSpec) {
|
||||
*out = *in
|
||||
@@ -916,6 +958,11 @@ func (in *KonnectivityAgentSpec) DeepCopyInto(out *KonnectivityAgentSpec) {
|
||||
*out = make(ExtraArgs, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Replicas != nil {
|
||||
in, out := &in.Replicas, &out.Replicas
|
||||
*out = new(int32)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KonnectivityAgentSpec.
|
||||
@@ -1011,6 +1058,11 @@ func (in *KonnectivityStatus) DeepCopyInto(out *KonnectivityStatus) {
|
||||
in.ClusterRoleBinding.DeepCopyInto(&out.ClusterRoleBinding)
|
||||
in.Agent.DeepCopyInto(&out.Agent)
|
||||
in.Service.DeepCopyInto(&out.Service)
|
||||
if in.Gateway != nil {
|
||||
in, out := &in.Gateway, &out.Gateway
|
||||
*out = new(KubernetesGatewayStatus)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KonnectivityStatus.
|
||||
@@ -1225,6 +1277,13 @@ func (in *KubeconfigsStatus) DeepCopy() *KubeconfigsStatus {
|
||||
// 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
|
||||
if in.ConfigurationJSONPatches != nil {
|
||||
in, out := &in.ConfigurationJSONPatches, &out.ConfigurationJSONPatches
|
||||
*out = make(JSONPatches, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.PreferredAddressTypes != nil {
|
||||
in, out := &in.PreferredAddressTypes, &out.PreferredAddressTypes
|
||||
*out = make([]KubeletPreferredAddressType, len(*in))
|
||||
|
||||
@@ -275,6 +275,10 @@ versions:
|
||||
status:
|
||||
description: DataStoreStatus defines the observed state of DataStore.
|
||||
properties:
|
||||
observedGeneration:
|
||||
description: ObservedGeneration represents the .metadata.generation that was last reconciled.
|
||||
format: int64
|
||||
type: integer
|
||||
usedBy:
|
||||
description: List of the Tenant Control Planes, namespaced named, using this data store.
|
||||
items:
|
||||
|
||||
@@ -199,6 +199,10 @@ versions:
|
||||
- resource
|
||||
type: object
|
||||
type: array
|
||||
observedGeneration:
|
||||
description: ObservedGeneration represents the .metadata.generation that was last reconciled.
|
||||
format: int64
|
||||
type: integer
|
||||
resources:
|
||||
default: 0
|
||||
description: Resources is the sum of targeted TenantControlPlane objects.
|
||||
|
||||
@@ -178,8 +178,8 @@ versions:
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-validations:
|
||||
- message: replicas must be 0 when mode is DaemonSet, and greater than 0 when mode is Deployment
|
||||
rule: '!(self.mode == ''DaemonSet'' && has(self.replicas) && self.replicas != 0) && !(self.mode == ''Deployment'' && self.replicas == 0)'
|
||||
- message: replicas must be 0 (or unset) when mode is DaemonSet, and greater than 0 (or unset) when mode is Deployment
|
||||
rule: '!(self.mode == ''DaemonSet'' && has(self.replicas) && self.replicas != 0) && !(self.mode == ''Deployment'' && has(self.replicas) && self.replicas == 0)'
|
||||
server:
|
||||
default:
|
||||
image: registry.k8s.io/kas-network-proxy/proxy-server
|
||||
@@ -6896,6 +6896,9 @@ 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:
|
||||
@@ -7114,10 +7117,45 @@ versions:
|
||||
description: |-
|
||||
CGroupFS defines the cgroup driver for Kubelet
|
||||
https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/configure-cgroup-driver/
|
||||
|
||||
Deprecated: use ConfigurationJSONPatches.
|
||||
enum:
|
||||
- systemd
|
||||
- cgroupfs
|
||||
type: string
|
||||
configurationJSONPatches:
|
||||
description: |-
|
||||
ConfigurationJSONPatches contains the RFC 6902 JSON patches to customise the kubeadm generate configuration,
|
||||
useful to customise and mangling the configuration according to your needs;
|
||||
e.g.: configuring the cgroup driver used by Kubelet is possible via the following patch:
|
||||
|
||||
[{"op": "replace", "path": "/cgroupDriver", "value": "systemd"}]
|
||||
items:
|
||||
properties:
|
||||
from:
|
||||
description: From specifies the source location for move or copy operations.
|
||||
type: string
|
||||
op:
|
||||
description: Op is the RFC 6902 JSON Patch operation.
|
||||
enum:
|
||||
- add
|
||||
- remove
|
||||
- replace
|
||||
- move
|
||||
- copy
|
||||
- test
|
||||
type: string
|
||||
path:
|
||||
description: Path specifies the target location in the JSON document. Use "/" to separate keys; "-" for appending to arrays.
|
||||
type: string
|
||||
value:
|
||||
description: Value is the operation value to be used when Op is add, replace, test.
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
required:
|
||||
- op
|
||||
- path
|
||||
type: object
|
||||
type: array
|
||||
preferredAddressTypes:
|
||||
default:
|
||||
- InternalIP
|
||||
@@ -7314,6 +7352,383 @@ versions:
|
||||
type: object
|
||||
enabled:
|
||||
type: boolean
|
||||
gateway:
|
||||
description: KubernetesGatewayStatus defines the status for the Tenant Control Plane Gateway in the management cluster.
|
||||
properties:
|
||||
accessPoints:
|
||||
description: A list of valid access points that the route exposes.
|
||||
items:
|
||||
properties:
|
||||
port:
|
||||
format: int32
|
||||
type: integer
|
||||
type:
|
||||
description: |-
|
||||
AddressType defines how a network address is represented as a text string.
|
||||
This may take two possible forms:
|
||||
|
||||
* A predefined CamelCase string identifier (currently limited to `IPAddress` or `Hostname`)
|
||||
* A domain-prefixed string identifier (like `acme.io/CustomAddressType`)
|
||||
|
||||
Values `IPAddress` and `Hostname` have Extended support.
|
||||
|
||||
The `NamedAddress` value has been deprecated in favor of implementation
|
||||
specific domain-prefixed strings.
|
||||
|
||||
All other values, including domain-prefixed values have Implementation-specific support,
|
||||
which are used in implementation-specific behaviors. Support for additional
|
||||
predefined CamelCase identifiers may be added in future releases.
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
pattern: ^Hostname|IPAddress|NamedAddress|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
|
||||
type: string
|
||||
urls:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
value:
|
||||
type: string
|
||||
required:
|
||||
- port
|
||||
- type
|
||||
- value
|
||||
type: object
|
||||
type: array
|
||||
parents:
|
||||
description: |-
|
||||
Parents is a list of parent resources (usually Gateways) that are
|
||||
associated with the route, and the status of the route with respect to
|
||||
each parent. When this route attaches to a parent, the controller that
|
||||
manages the parent must add an entry to this list when the controller
|
||||
first sees the route and should update the entry as appropriate when the
|
||||
route or gateway is modified.
|
||||
|
||||
Note that parent references that cannot be resolved by an implementation
|
||||
of this API will not be added to this list. Implementations of this API
|
||||
can only populate Route status for the Gateways/parent resources they are
|
||||
responsible for.
|
||||
|
||||
A maximum of 32 Gateways will be represented in this list. An empty list
|
||||
means the route has not been attached to any Gateway.
|
||||
|
||||
<gateway:util:excludeFromCRD>
|
||||
Notes for implementors:
|
||||
|
||||
While parents is not a listType `map`, this is due to the fact that the
|
||||
list key is not scalar, and Kubernetes is unable to represent this.
|
||||
|
||||
Parent status MUST be considered to be namespaced by the combination of
|
||||
the parentRef and controllerName fields, and implementations should keep
|
||||
the following rules in mind when updating this status:
|
||||
|
||||
* Implementations MUST update only entries that have a matching value of
|
||||
`controllerName` for that implementation.
|
||||
* Implementations MUST NOT update entries with non-matching `controllerName`
|
||||
fields.
|
||||
* Implementations MUST treat each `parentRef`` in the Route separately and
|
||||
update its status based on the relationship with that parent.
|
||||
* Implementations MUST perform a read-modify-write cycle on this field
|
||||
before modifying it. That is, when modifying this field, implementations
|
||||
must be confident they have fetched the most recent version of this field,
|
||||
and ensure that changes they make are on that recent version.
|
||||
|
||||
</gateway:util:excludeFromCRD>
|
||||
items:
|
||||
description: |-
|
||||
RouteParentStatus describes the status of a route with respect to an
|
||||
associated Parent.
|
||||
properties:
|
||||
conditions:
|
||||
description: |-
|
||||
Conditions describes the status of the route with respect to the Gateway.
|
||||
Note that the route's availability is also subject to the Gateway's own
|
||||
status conditions and listener status.
|
||||
|
||||
If the Route's ParentRef specifies an existing Gateway that supports
|
||||
Routes of this kind AND that Gateway's controller has sufficient access,
|
||||
then that Gateway's controller MUST set the "Accepted" condition on the
|
||||
Route, to indicate whether the route has been accepted or rejected by the
|
||||
Gateway, and why.
|
||||
|
||||
A Route MUST be considered "Accepted" if at least one of the Route's
|
||||
rules is implemented by the Gateway.
|
||||
|
||||
There are a number of cases where the "Accepted" condition may not be set
|
||||
due to lack of controller visibility, that includes when:
|
||||
|
||||
* The Route refers to a nonexistent parent.
|
||||
* The Route is of a type that the controller does not support.
|
||||
* The Route is in a namespace the controller does not have access to.
|
||||
|
||||
<gateway:util:excludeFromCRD>
|
||||
|
||||
Notes for implementors:
|
||||
|
||||
Conditions are a listType `map`, which means that they function like a
|
||||
map with a key of the `type` field _in the k8s apiserver_.
|
||||
|
||||
This means that implementations must obey some rules when updating this
|
||||
section.
|
||||
|
||||
* Implementations MUST perform a read-modify-write cycle on this field
|
||||
before modifying it. That is, when modifying this field, implementations
|
||||
must be confident they have fetched the most recent version of this field,
|
||||
and ensure that changes they make are on that recent version.
|
||||
* Implementations MUST NOT remove or reorder Conditions that they are not
|
||||
directly responsible for. For example, if an implementation sees a Condition
|
||||
with type `special.io/SomeField`, it MUST NOT remove, change or update that
|
||||
Condition.
|
||||
* Implementations MUST always _merge_ changes into Conditions of the same Type,
|
||||
rather than creating more than one Condition of the same Type.
|
||||
* Implementations MUST always update the `observedGeneration` field of the
|
||||
Condition to the `metadata.generation` of the Gateway at the time of update creation.
|
||||
* If the `observedGeneration` of a Condition is _greater than_ the value the
|
||||
implementation knows about, then it MUST NOT perform the update on that Condition,
|
||||
but must wait for a future reconciliation and status update. (The assumption is that
|
||||
the implementation's copy of the object is stale and an update will be re-triggered
|
||||
if relevant.)
|
||||
|
||||
</gateway:util:excludeFromCRD>
|
||||
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
|
||||
maxItems: 8
|
||||
minItems: 1
|
||||
type: array
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
controllerName:
|
||||
description: |-
|
||||
ControllerName is a domain/path string that indicates the name of the
|
||||
controller that wrote this status. This corresponds with the
|
||||
controllerName field on GatewayClass.
|
||||
|
||||
Example: "example.net/gateway-controller".
|
||||
|
||||
The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are
|
||||
valid Kubernetes names
|
||||
(https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).
|
||||
|
||||
Controllers MUST populate this field when writing status. Controllers should ensure that
|
||||
entries to status populated with their ControllerName are cleaned up when they are no
|
||||
longer necessary.
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
|
||||
type: string
|
||||
parentRef:
|
||||
description: |-
|
||||
ParentRef corresponds with a ParentRef in the spec that this
|
||||
RouteParentStatus struct describes the status of.
|
||||
properties:
|
||||
group:
|
||||
default: gateway.networking.k8s.io
|
||||
description: |-
|
||||
Group is the group of the referent.
|
||||
When unspecified, "gateway.networking.k8s.io" is inferred.
|
||||
To set the core API group (such as for a "Service" kind referent),
|
||||
Group must be explicitly set to "" (empty string).
|
||||
|
||||
Support: Core
|
||||
maxLength: 253
|
||||
pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
|
||||
type: string
|
||||
kind:
|
||||
default: Gateway
|
||||
description: |-
|
||||
Kind is kind of the referent.
|
||||
|
||||
There are two kinds of parent resources with "Core" support:
|
||||
|
||||
* Gateway (Gateway conformance profile)
|
||||
* Service (Mesh conformance profile, ClusterIP Services only)
|
||||
|
||||
Support for other resources is Implementation-Specific.
|
||||
maxLength: 63
|
||||
minLength: 1
|
||||
pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
|
||||
type: string
|
||||
name:
|
||||
description: |-
|
||||
Name is the name of the referent.
|
||||
|
||||
Support: Core
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
type: string
|
||||
namespace:
|
||||
description: |-
|
||||
Namespace is the namespace of the referent. When unspecified, this refers
|
||||
to the local namespace of the Route.
|
||||
|
||||
Note that there are specific rules for ParentRefs which cross namespace
|
||||
boundaries. Cross-namespace references are only valid if they are explicitly
|
||||
allowed by something in the namespace they are referring to. For example:
|
||||
Gateway has the AllowedRoutes field, and ReferenceGrant provides a
|
||||
generic way to enable any other kind of cross-namespace reference.
|
||||
|
||||
<gateway:experimental:description>
|
||||
ParentRefs from a Route to a Service in the same namespace are "producer"
|
||||
routes, which apply default routing rules to inbound connections from
|
||||
any namespace to the Service.
|
||||
|
||||
ParentRefs from a Route to a Service in a different namespace are
|
||||
"consumer" routes, and these routing rules are only applied to outbound
|
||||
connections originating from the same namespace as the Route, for which
|
||||
the intended destination of the connections are a Service targeted as a
|
||||
ParentRef of the Route.
|
||||
</gateway:experimental:description>
|
||||
|
||||
Support: Core
|
||||
maxLength: 63
|
||||
minLength: 1
|
||||
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
|
||||
type: string
|
||||
port:
|
||||
description: |-
|
||||
Port is the network port this Route targets. It can be interpreted
|
||||
differently based on the type of parent resource.
|
||||
|
||||
When the parent resource is a Gateway, this targets all listeners
|
||||
listening on the specified port that also support this kind of Route(and
|
||||
select this Route). It's not recommended to set `Port` unless the
|
||||
networking behaviors specified in a Route must apply to a specific port
|
||||
as opposed to a listener(s) whose port(s) may be changed. When both Port
|
||||
and SectionName are specified, the name and port of the selected listener
|
||||
must match both specified values.
|
||||
|
||||
<gateway:experimental:description>
|
||||
When the parent resource is a Service, this targets a specific port in the
|
||||
Service spec. When both Port (experimental) and SectionName are specified,
|
||||
the name and port of the selected port must match both specified values.
|
||||
</gateway:experimental:description>
|
||||
|
||||
Implementations MAY choose to support other parent resources.
|
||||
Implementations supporting other types of parent resources MUST clearly
|
||||
document how/if Port is interpreted.
|
||||
|
||||
For the purpose of status, an attachment is considered successful as
|
||||
long as the parent resource accepts it partially. For example, Gateway
|
||||
listeners can restrict which Routes can attach to them by Route kind,
|
||||
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
|
||||
from the referencing Route, the Route MUST be considered successfully
|
||||
attached. If no Gateway listeners accept attachment from this Route,
|
||||
the Route MUST be considered detached from the Gateway.
|
||||
|
||||
Support: Extended
|
||||
format: int32
|
||||
maximum: 65535
|
||||
minimum: 1
|
||||
type: integer
|
||||
sectionName:
|
||||
description: |-
|
||||
SectionName is the name of a section within the target resource. In the
|
||||
following resources, SectionName is interpreted as the following:
|
||||
|
||||
* Gateway: Listener name. When both Port (experimental) and SectionName
|
||||
are specified, the name and port of the selected listener must match
|
||||
both specified values.
|
||||
* Service: Port name. When both Port (experimental) and SectionName
|
||||
are specified, the name and port of the selected listener must match
|
||||
both specified values.
|
||||
|
||||
Implementations MAY choose to support attaching Routes to other resources.
|
||||
If that is the case, they MUST clearly document how SectionName is
|
||||
interpreted.
|
||||
|
||||
When unspecified (empty string), this will reference the entire resource.
|
||||
For the purpose of status, an attachment is considered successful if at
|
||||
least one section in the parent resource accepts it. For example, Gateway
|
||||
listeners can restrict which Routes can attach to them by Route kind,
|
||||
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
|
||||
the referencing Route, the Route MUST be considered successfully
|
||||
attached. If no Gateway listeners accept attachment from this Route, the
|
||||
Route MUST be considered detached from the Gateway.
|
||||
|
||||
Support: Core
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
required:
|
||||
- conditions
|
||||
- controllerName
|
||||
- parentRef
|
||||
type: object
|
||||
maxItems: 32
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
routeRef:
|
||||
description: Reference to the route created for this tenant.
|
||||
properties:
|
||||
name:
|
||||
default: ""
|
||||
description: |-
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
required:
|
||||
- parents
|
||||
type: object
|
||||
kubeconfig:
|
||||
description: KubeconfigStatus contains information about the generated kubeconfig.
|
||||
properties:
|
||||
@@ -8373,6 +8788,10 @@ versions:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
observedGeneration:
|
||||
description: ObservedGeneration represents the .metadata.generation that was last reconciled.
|
||||
format: int64
|
||||
type: integer
|
||||
storage:
|
||||
description: Storage Status contains information about Kubernetes storage system
|
||||
properties:
|
||||
|
||||
@@ -284,6 +284,10 @@ spec:
|
||||
status:
|
||||
description: DataStoreStatus defines the observed state of DataStore.
|
||||
properties:
|
||||
observedGeneration:
|
||||
description: ObservedGeneration represents the .metadata.generation that was last reconciled.
|
||||
format: int64
|
||||
type: integer
|
||||
usedBy:
|
||||
description: List of the Tenant Control Planes, namespaced named, using this data store.
|
||||
items:
|
||||
|
||||
@@ -207,6 +207,10 @@ spec:
|
||||
- resource
|
||||
type: object
|
||||
type: array
|
||||
observedGeneration:
|
||||
description: ObservedGeneration represents the .metadata.generation that was last reconciled.
|
||||
format: int64
|
||||
type: integer
|
||||
resources:
|
||||
default: 0
|
||||
description: Resources is the sum of targeted TenantControlPlane objects.
|
||||
|
||||
@@ -186,8 +186,8 @@ spec:
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-validations:
|
||||
- message: replicas must be 0 when mode is DaemonSet, and greater than 0 when mode is Deployment
|
||||
rule: '!(self.mode == ''DaemonSet'' && has(self.replicas) && self.replicas != 0) && !(self.mode == ''Deployment'' && self.replicas == 0)'
|
||||
- message: replicas must be 0 (or unset) when mode is DaemonSet, and greater than 0 (or unset) when mode is Deployment
|
||||
rule: '!(self.mode == ''DaemonSet'' && has(self.replicas) && self.replicas != 0) && !(self.mode == ''Deployment'' && has(self.replicas) && self.replicas == 0)'
|
||||
server:
|
||||
default:
|
||||
image: registry.k8s.io/kas-network-proxy/proxy-server
|
||||
@@ -6904,6 +6904,9 @@ 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:
|
||||
@@ -7122,10 +7125,45 @@ spec:
|
||||
description: |-
|
||||
CGroupFS defines the cgroup driver for Kubelet
|
||||
https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/configure-cgroup-driver/
|
||||
|
||||
Deprecated: use ConfigurationJSONPatches.
|
||||
enum:
|
||||
- systemd
|
||||
- cgroupfs
|
||||
type: string
|
||||
configurationJSONPatches:
|
||||
description: |-
|
||||
ConfigurationJSONPatches contains the RFC 6902 JSON patches to customise the kubeadm generate configuration,
|
||||
useful to customise and mangling the configuration according to your needs;
|
||||
e.g.: configuring the cgroup driver used by Kubelet is possible via the following patch:
|
||||
|
||||
[{"op": "replace", "path": "/cgroupDriver", "value": "systemd"}]
|
||||
items:
|
||||
properties:
|
||||
from:
|
||||
description: From specifies the source location for move or copy operations.
|
||||
type: string
|
||||
op:
|
||||
description: Op is the RFC 6902 JSON Patch operation.
|
||||
enum:
|
||||
- add
|
||||
- remove
|
||||
- replace
|
||||
- move
|
||||
- copy
|
||||
- test
|
||||
type: string
|
||||
path:
|
||||
description: Path specifies the target location in the JSON document. Use "/" to separate keys; "-" for appending to arrays.
|
||||
type: string
|
||||
value:
|
||||
description: Value is the operation value to be used when Op is add, replace, test.
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
required:
|
||||
- op
|
||||
- path
|
||||
type: object
|
||||
type: array
|
||||
preferredAddressTypes:
|
||||
default:
|
||||
- InternalIP
|
||||
@@ -7322,6 +7360,383 @@ spec:
|
||||
type: object
|
||||
enabled:
|
||||
type: boolean
|
||||
gateway:
|
||||
description: KubernetesGatewayStatus defines the status for the Tenant Control Plane Gateway in the management cluster.
|
||||
properties:
|
||||
accessPoints:
|
||||
description: A list of valid access points that the route exposes.
|
||||
items:
|
||||
properties:
|
||||
port:
|
||||
format: int32
|
||||
type: integer
|
||||
type:
|
||||
description: |-
|
||||
AddressType defines how a network address is represented as a text string.
|
||||
This may take two possible forms:
|
||||
|
||||
* A predefined CamelCase string identifier (currently limited to `IPAddress` or `Hostname`)
|
||||
* A domain-prefixed string identifier (like `acme.io/CustomAddressType`)
|
||||
|
||||
Values `IPAddress` and `Hostname` have Extended support.
|
||||
|
||||
The `NamedAddress` value has been deprecated in favor of implementation
|
||||
specific domain-prefixed strings.
|
||||
|
||||
All other values, including domain-prefixed values have Implementation-specific support,
|
||||
which are used in implementation-specific behaviors. Support for additional
|
||||
predefined CamelCase identifiers may be added in future releases.
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
pattern: ^Hostname|IPAddress|NamedAddress|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
|
||||
type: string
|
||||
urls:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
value:
|
||||
type: string
|
||||
required:
|
||||
- port
|
||||
- type
|
||||
- value
|
||||
type: object
|
||||
type: array
|
||||
parents:
|
||||
description: |-
|
||||
Parents is a list of parent resources (usually Gateways) that are
|
||||
associated with the route, and the status of the route with respect to
|
||||
each parent. When this route attaches to a parent, the controller that
|
||||
manages the parent must add an entry to this list when the controller
|
||||
first sees the route and should update the entry as appropriate when the
|
||||
route or gateway is modified.
|
||||
|
||||
Note that parent references that cannot be resolved by an implementation
|
||||
of this API will not be added to this list. Implementations of this API
|
||||
can only populate Route status for the Gateways/parent resources they are
|
||||
responsible for.
|
||||
|
||||
A maximum of 32 Gateways will be represented in this list. An empty list
|
||||
means the route has not been attached to any Gateway.
|
||||
|
||||
<gateway:util:excludeFromCRD>
|
||||
Notes for implementors:
|
||||
|
||||
While parents is not a listType `map`, this is due to the fact that the
|
||||
list key is not scalar, and Kubernetes is unable to represent this.
|
||||
|
||||
Parent status MUST be considered to be namespaced by the combination of
|
||||
the parentRef and controllerName fields, and implementations should keep
|
||||
the following rules in mind when updating this status:
|
||||
|
||||
* Implementations MUST update only entries that have a matching value of
|
||||
`controllerName` for that implementation.
|
||||
* Implementations MUST NOT update entries with non-matching `controllerName`
|
||||
fields.
|
||||
* Implementations MUST treat each `parentRef`` in the Route separately and
|
||||
update its status based on the relationship with that parent.
|
||||
* Implementations MUST perform a read-modify-write cycle on this field
|
||||
before modifying it. That is, when modifying this field, implementations
|
||||
must be confident they have fetched the most recent version of this field,
|
||||
and ensure that changes they make are on that recent version.
|
||||
|
||||
</gateway:util:excludeFromCRD>
|
||||
items:
|
||||
description: |-
|
||||
RouteParentStatus describes the status of a route with respect to an
|
||||
associated Parent.
|
||||
properties:
|
||||
conditions:
|
||||
description: |-
|
||||
Conditions describes the status of the route with respect to the Gateway.
|
||||
Note that the route's availability is also subject to the Gateway's own
|
||||
status conditions and listener status.
|
||||
|
||||
If the Route's ParentRef specifies an existing Gateway that supports
|
||||
Routes of this kind AND that Gateway's controller has sufficient access,
|
||||
then that Gateway's controller MUST set the "Accepted" condition on the
|
||||
Route, to indicate whether the route has been accepted or rejected by the
|
||||
Gateway, and why.
|
||||
|
||||
A Route MUST be considered "Accepted" if at least one of the Route's
|
||||
rules is implemented by the Gateway.
|
||||
|
||||
There are a number of cases where the "Accepted" condition may not be set
|
||||
due to lack of controller visibility, that includes when:
|
||||
|
||||
* The Route refers to a nonexistent parent.
|
||||
* The Route is of a type that the controller does not support.
|
||||
* The Route is in a namespace the controller does not have access to.
|
||||
|
||||
<gateway:util:excludeFromCRD>
|
||||
|
||||
Notes for implementors:
|
||||
|
||||
Conditions are a listType `map`, which means that they function like a
|
||||
map with a key of the `type` field _in the k8s apiserver_.
|
||||
|
||||
This means that implementations must obey some rules when updating this
|
||||
section.
|
||||
|
||||
* Implementations MUST perform a read-modify-write cycle on this field
|
||||
before modifying it. That is, when modifying this field, implementations
|
||||
must be confident they have fetched the most recent version of this field,
|
||||
and ensure that changes they make are on that recent version.
|
||||
* Implementations MUST NOT remove or reorder Conditions that they are not
|
||||
directly responsible for. For example, if an implementation sees a Condition
|
||||
with type `special.io/SomeField`, it MUST NOT remove, change or update that
|
||||
Condition.
|
||||
* Implementations MUST always _merge_ changes into Conditions of the same Type,
|
||||
rather than creating more than one Condition of the same Type.
|
||||
* Implementations MUST always update the `observedGeneration` field of the
|
||||
Condition to the `metadata.generation` of the Gateway at the time of update creation.
|
||||
* If the `observedGeneration` of a Condition is _greater than_ the value the
|
||||
implementation knows about, then it MUST NOT perform the update on that Condition,
|
||||
but must wait for a future reconciliation and status update. (The assumption is that
|
||||
the implementation's copy of the object is stale and an update will be re-triggered
|
||||
if relevant.)
|
||||
|
||||
</gateway:util:excludeFromCRD>
|
||||
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
|
||||
maxItems: 8
|
||||
minItems: 1
|
||||
type: array
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
controllerName:
|
||||
description: |-
|
||||
ControllerName is a domain/path string that indicates the name of the
|
||||
controller that wrote this status. This corresponds with the
|
||||
controllerName field on GatewayClass.
|
||||
|
||||
Example: "example.net/gateway-controller".
|
||||
|
||||
The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are
|
||||
valid Kubernetes names
|
||||
(https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).
|
||||
|
||||
Controllers MUST populate this field when writing status. Controllers should ensure that
|
||||
entries to status populated with their ControllerName are cleaned up when they are no
|
||||
longer necessary.
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
|
||||
type: string
|
||||
parentRef:
|
||||
description: |-
|
||||
ParentRef corresponds with a ParentRef in the spec that this
|
||||
RouteParentStatus struct describes the status of.
|
||||
properties:
|
||||
group:
|
||||
default: gateway.networking.k8s.io
|
||||
description: |-
|
||||
Group is the group of the referent.
|
||||
When unspecified, "gateway.networking.k8s.io" is inferred.
|
||||
To set the core API group (such as for a "Service" kind referent),
|
||||
Group must be explicitly set to "" (empty string).
|
||||
|
||||
Support: Core
|
||||
maxLength: 253
|
||||
pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
|
||||
type: string
|
||||
kind:
|
||||
default: Gateway
|
||||
description: |-
|
||||
Kind is kind of the referent.
|
||||
|
||||
There are two kinds of parent resources with "Core" support:
|
||||
|
||||
* Gateway (Gateway conformance profile)
|
||||
* Service (Mesh conformance profile, ClusterIP Services only)
|
||||
|
||||
Support for other resources is Implementation-Specific.
|
||||
maxLength: 63
|
||||
minLength: 1
|
||||
pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
|
||||
type: string
|
||||
name:
|
||||
description: |-
|
||||
Name is the name of the referent.
|
||||
|
||||
Support: Core
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
type: string
|
||||
namespace:
|
||||
description: |-
|
||||
Namespace is the namespace of the referent. When unspecified, this refers
|
||||
to the local namespace of the Route.
|
||||
|
||||
Note that there are specific rules for ParentRefs which cross namespace
|
||||
boundaries. Cross-namespace references are only valid if they are explicitly
|
||||
allowed by something in the namespace they are referring to. For example:
|
||||
Gateway has the AllowedRoutes field, and ReferenceGrant provides a
|
||||
generic way to enable any other kind of cross-namespace reference.
|
||||
|
||||
<gateway:experimental:description>
|
||||
ParentRefs from a Route to a Service in the same namespace are "producer"
|
||||
routes, which apply default routing rules to inbound connections from
|
||||
any namespace to the Service.
|
||||
|
||||
ParentRefs from a Route to a Service in a different namespace are
|
||||
"consumer" routes, and these routing rules are only applied to outbound
|
||||
connections originating from the same namespace as the Route, for which
|
||||
the intended destination of the connections are a Service targeted as a
|
||||
ParentRef of the Route.
|
||||
</gateway:experimental:description>
|
||||
|
||||
Support: Core
|
||||
maxLength: 63
|
||||
minLength: 1
|
||||
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
|
||||
type: string
|
||||
port:
|
||||
description: |-
|
||||
Port is the network port this Route targets. It can be interpreted
|
||||
differently based on the type of parent resource.
|
||||
|
||||
When the parent resource is a Gateway, this targets all listeners
|
||||
listening on the specified port that also support this kind of Route(and
|
||||
select this Route). It's not recommended to set `Port` unless the
|
||||
networking behaviors specified in a Route must apply to a specific port
|
||||
as opposed to a listener(s) whose port(s) may be changed. When both Port
|
||||
and SectionName are specified, the name and port of the selected listener
|
||||
must match both specified values.
|
||||
|
||||
<gateway:experimental:description>
|
||||
When the parent resource is a Service, this targets a specific port in the
|
||||
Service spec. When both Port (experimental) and SectionName are specified,
|
||||
the name and port of the selected port must match both specified values.
|
||||
</gateway:experimental:description>
|
||||
|
||||
Implementations MAY choose to support other parent resources.
|
||||
Implementations supporting other types of parent resources MUST clearly
|
||||
document how/if Port is interpreted.
|
||||
|
||||
For the purpose of status, an attachment is considered successful as
|
||||
long as the parent resource accepts it partially. For example, Gateway
|
||||
listeners can restrict which Routes can attach to them by Route kind,
|
||||
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
|
||||
from the referencing Route, the Route MUST be considered successfully
|
||||
attached. If no Gateway listeners accept attachment from this Route,
|
||||
the Route MUST be considered detached from the Gateway.
|
||||
|
||||
Support: Extended
|
||||
format: int32
|
||||
maximum: 65535
|
||||
minimum: 1
|
||||
type: integer
|
||||
sectionName:
|
||||
description: |-
|
||||
SectionName is the name of a section within the target resource. In the
|
||||
following resources, SectionName is interpreted as the following:
|
||||
|
||||
* Gateway: Listener name. When both Port (experimental) and SectionName
|
||||
are specified, the name and port of the selected listener must match
|
||||
both specified values.
|
||||
* Service: Port name. When both Port (experimental) and SectionName
|
||||
are specified, the name and port of the selected listener must match
|
||||
both specified values.
|
||||
|
||||
Implementations MAY choose to support attaching Routes to other resources.
|
||||
If that is the case, they MUST clearly document how SectionName is
|
||||
interpreted.
|
||||
|
||||
When unspecified (empty string), this will reference the entire resource.
|
||||
For the purpose of status, an attachment is considered successful if at
|
||||
least one section in the parent resource accepts it. For example, Gateway
|
||||
listeners can restrict which Routes can attach to them by Route kind,
|
||||
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
|
||||
the referencing Route, the Route MUST be considered successfully
|
||||
attached. If no Gateway listeners accept attachment from this Route, the
|
||||
Route MUST be considered detached from the Gateway.
|
||||
|
||||
Support: Core
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
required:
|
||||
- conditions
|
||||
- controllerName
|
||||
- parentRef
|
||||
type: object
|
||||
maxItems: 32
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
routeRef:
|
||||
description: Reference to the route created for this tenant.
|
||||
properties:
|
||||
name:
|
||||
default: ""
|
||||
description: |-
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
required:
|
||||
- parents
|
||||
type: object
|
||||
kubeconfig:
|
||||
description: KubeconfigStatus contains information about the generated kubeconfig.
|
||||
properties:
|
||||
@@ -8381,6 +8796,10 @@ spec:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
observedGeneration:
|
||||
description: ObservedGeneration represents the .metadata.generation that was last reconciled.
|
||||
format: int64
|
||||
type: integer
|
||||
storage:
|
||||
description: Storage Status contains information about Kubernetes storage system
|
||||
properties:
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
@@ -12,12 +13,12 @@ import (
|
||||
func KubernetesVersion(config *rest.Config) (string, error) {
|
||||
cs, csErr := kubernetes.NewForConfig(config)
|
||||
if csErr != nil {
|
||||
return "", errors.Wrap(csErr, "cannot create kubernetes clientset")
|
||||
return "", fmt.Errorf("cannot create kubernetes clientset: %w", csErr)
|
||||
}
|
||||
|
||||
sv, svErr := cs.ServerVersion()
|
||||
if svErr != nil {
|
||||
return "", errors.Wrap(svErr, "cannot get Kubernetes version")
|
||||
return "", fmt.Errorf("cannot get Kubernetes version: %w", svErr)
|
||||
}
|
||||
|
||||
return sv.GitVersion, nil
|
||||
|
||||
@@ -13,7 +13,15 @@ spec:
|
||||
kubernetes:
|
||||
version: "v1.33.0"
|
||||
kubelet:
|
||||
cgroupfs: systemd
|
||||
configurationJSONPatches:
|
||||
- op: add
|
||||
path: /featureGates
|
||||
value:
|
||||
KubeletCrashLoopBackOffMax: false
|
||||
KubeletEnsureSecretPulledImages: false
|
||||
- op: replace
|
||||
path: /cgroupDriver
|
||||
value: systemd
|
||||
networkProfile:
|
||||
port: 6443
|
||||
addons:
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Copyright 2022 Clastix Labs
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# This example demonstrates how to configure Gateway API support for a Tenant Control Plane.
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. Gateway API CRDs must be installed (GatewayClass, Gateway, TLSRoute)
|
||||
# 2. A Gateway resource must exist with listeners for ports 6443 and 8132
|
||||
# 3. DNS(or worker nodes hosts entries) must be configured to resolve the hostname to the Gateway's external address
|
||||
#
|
||||
# Example GatewayClass and Gateway configuration:
|
||||
#
|
||||
# apiVersion: gateway.networking.k8s.io/v1
|
||||
# kind: GatewayClass
|
||||
# metadata:
|
||||
# name: envoy-gw-class
|
||||
# spec:
|
||||
# controllerName: gateway.envoyproxy.io/gatewayclass-controller
|
||||
# ---
|
||||
# apiVersion: gateway.networking.k8s.io/v1
|
||||
# kind: Gateway
|
||||
# metadata:
|
||||
# name: gateway
|
||||
# namespace: default
|
||||
# spec:
|
||||
# gatewayClassName: envoy-gw-class
|
||||
# listeners:
|
||||
# - allowedRoutes:
|
||||
# kinds:
|
||||
# - group: gateway.networking.k8s.io
|
||||
# kind: TLSRoute
|
||||
# namespaces:
|
||||
# from: All
|
||||
# hostname: '*.cluster.dev'
|
||||
# name: kube-apiserver
|
||||
# port: 6443
|
||||
# protocol: TLS
|
||||
# tls:
|
||||
# mode: Passthrough
|
||||
# - allowedRoutes:
|
||||
# kinds:
|
||||
# - group: gateway.networking.k8s.io
|
||||
# kind: TLSRoute
|
||||
# namespaces:
|
||||
# from: All
|
||||
# hostname: '*.cluster.dev'
|
||||
# name: konnectivity-server
|
||||
# port: 8132
|
||||
# protocol: TLS
|
||||
# tls:
|
||||
# mode: Passthrough
|
||||
|
||||
apiVersion: kamaji.clastix.io/v1alpha1
|
||||
kind: TenantControlPlane
|
||||
metadata:
|
||||
name: demo-tcp-1
|
||||
spec:
|
||||
addons:
|
||||
coreDNS: {}
|
||||
kubeProxy: {}
|
||||
konnectivity: {}
|
||||
dataStore: default
|
||||
controlPlane:
|
||||
gateway:
|
||||
hostname: "c11.cluster.dev" # worker nodes or kubectl clients must be able to resolve this hostname to the Gateway's external address.
|
||||
parentRefs:
|
||||
- name: gateway
|
||||
namespace: default
|
||||
deployment:
|
||||
replicas: 1
|
||||
service:
|
||||
serviceType: ClusterIP
|
||||
kubernetes:
|
||||
version: v1.32.0
|
||||
kubelet:
|
||||
cgroupfs: systemd
|
||||
networkProfile:
|
||||
port: 6443
|
||||
certSANs:
|
||||
- "c11.cluster.dev"
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -168,7 +167,7 @@ func (s *CertificateLifecycle) extractCertificateFromKubeconfig(secret corev1.Se
|
||||
|
||||
crt, err := crypto.ParseCertificateBytes(kc.AuthInfos[0].AuthInfo.ClientCertificateData)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot parse kubeconfig certificate bytes")
|
||||
return nil, fmt.Errorf("cannot parse kubeconfig certificate bytes: %w", err)
|
||||
}
|
||||
|
||||
return crt, nil
|
||||
|
||||
@@ -5,8 +5,8 @@ package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
k8stypes "k8s.io/apimachinery/pkg/types"
|
||||
@@ -65,7 +65,7 @@ func (r *DataStore) Reconcile(ctx context.Context, request reconcile.Request) (r
|
||||
if lErr := r.Client.List(ctx, &tcpList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(kamajiv1alpha1.TenantControlPlaneUsedDataStoreKey, ds.GetName()),
|
||||
}); lErr != nil {
|
||||
return errors.Wrap(lErr, "cannot retrieve list of the Tenant Control Plane using the following instance")
|
||||
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()
|
||||
@@ -73,10 +73,11 @@ func (r *DataStore) Reconcile(ctx context.Context, request reconcile.Request) (r
|
||||
tcpSets.Insert(getNamespacedName(tcp.GetNamespace(), tcp.GetName()).String())
|
||||
}
|
||||
|
||||
ds.Status.ObservedGeneration = ds.Generation
|
||||
ds.Status.UsedBy = tcpSets.List()
|
||||
|
||||
if sErr := r.Client.Status().Update(ctx, &ds); sErr != nil {
|
||||
return errors.Wrap(sErr, "cannot update the status for the given instance")
|
||||
return fmt.Errorf("cannot update the status for the given instance: %w", sErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -88,6 +87,7 @@ func (r *KubeconfigGeneratorReconciler) Reconcile(ctx context.Context, req ctrl.
|
||||
}
|
||||
|
||||
generator.Status = status
|
||||
generator.Status.ObservedGeneration = generator.Generation
|
||||
|
||||
if statusErr := r.Client.Status().Update(ctx, &generator); statusErr != nil {
|
||||
logger.Error(statusErr, "cannot update resource status")
|
||||
@@ -103,12 +103,12 @@ func (r *KubeconfigGeneratorReconciler) Reconcile(ctx context.Context, req ctrl.
|
||||
func (r *KubeconfigGeneratorReconciler) handle(ctx context.Context, generator *kamajiv1alpha1.KubeconfigGenerator) (kamajiv1alpha1.KubeconfigGeneratorStatus, error) {
|
||||
nsSelector, nsErr := metav1.LabelSelectorAsSelector(&generator.Spec.NamespaceSelector)
|
||||
if nsErr != nil {
|
||||
return kamajiv1alpha1.KubeconfigGeneratorStatus{}, errors.Wrap(nsErr, "NamespaceSelector contains an error")
|
||||
return kamajiv1alpha1.KubeconfigGeneratorStatus{}, fmt.Errorf("NamespaceSelector contains an error: %w", nsErr)
|
||||
}
|
||||
|
||||
var namespaceList corev1.NamespaceList
|
||||
if err := r.Client.List(ctx, &namespaceList, &client.ListOptions{LabelSelector: nsSelector}); err != nil {
|
||||
return kamajiv1alpha1.KubeconfigGeneratorStatus{}, errors.Wrap(err, "cannot filter Namespace objects using provided selector")
|
||||
return kamajiv1alpha1.KubeconfigGeneratorStatus{}, fmt.Errorf("cannot filter Namespace objects using provided selector: %w", err)
|
||||
}
|
||||
|
||||
var targets []kamajiv1alpha1.TenantControlPlane
|
||||
@@ -116,12 +116,12 @@ func (r *KubeconfigGeneratorReconciler) handle(ctx context.Context, generator *k
|
||||
for _, ns := range namespaceList.Items {
|
||||
tcpSelector, tcpErr := metav1.LabelSelectorAsSelector(&generator.Spec.TenantControlPlaneSelector)
|
||||
if tcpErr != nil {
|
||||
return kamajiv1alpha1.KubeconfigGeneratorStatus{}, errors.Wrap(tcpErr, "TenantControlPlaneSelector contains an error")
|
||||
return kamajiv1alpha1.KubeconfigGeneratorStatus{}, fmt.Errorf("TenantControlPlaneSelector contains an error: %w", tcpErr)
|
||||
}
|
||||
|
||||
var tcpList kamajiv1alpha1.TenantControlPlaneList
|
||||
if err := r.Client.List(ctx, &tcpList, &client.ListOptions{Namespace: ns.GetName(), LabelSelector: tcpSelector}); err != nil {
|
||||
return kamajiv1alpha1.KubeconfigGeneratorStatus{}, errors.Wrap(err, "cannot filter TenantControlPlane objects using provided selector")
|
||||
return kamajiv1alpha1.KubeconfigGeneratorStatus{}, fmt.Errorf("cannot filter TenantControlPlane objects using provided selector: %w", err)
|
||||
}
|
||||
|
||||
targets = append(targets, tcpList.Items...)
|
||||
@@ -290,17 +290,17 @@ func (r *KubeconfigGeneratorReconciler) generate(ctx context.Context, generator
|
||||
|
||||
var caSecret corev1.Secret
|
||||
if caErr := r.Client.Get(ctx, types.NamespacedName{Namespace: tcp.Namespace, Name: tcp.Status.Certificates.CA.SecretName}, &caSecret); caErr != nil {
|
||||
return errors.Wrap(caErr, "cannot retrieve Certificate Authority")
|
||||
return fmt.Errorf("cannot retrieve Certificate Authority: %w", caErr)
|
||||
}
|
||||
|
||||
caCert, crtErr := crypto.ParseCertificateBytes(caSecret.Data[kubeadmconstants.CACertName])
|
||||
if crtErr != nil {
|
||||
return errors.Wrap(crtErr, "cannot parse Certificate Authority certificate")
|
||||
return fmt.Errorf("cannot parse Certificate Authority certificate: %w", crtErr)
|
||||
}
|
||||
|
||||
caKey, keyErr := crypto.ParsePrivateKeyBytes(caSecret.Data[kubeadmconstants.CAKeyName])
|
||||
if keyErr != nil {
|
||||
return errors.Wrap(keyErr, "cannot parse Certificate Authority key")
|
||||
return fmt.Errorf("cannot parse Certificate Authority key: %w", keyErr)
|
||||
}
|
||||
|
||||
clientCert, clientKey, err := pkiutil.NewCertAndKey(caCert, caKey, &clientCertConfig)
|
||||
@@ -312,7 +312,7 @@ func (r *KubeconfigGeneratorReconciler) generate(ctx context.Context, generator
|
||||
tmpl.AuthInfos[name].AuthInfo.ClientCertificateData = pkiutil.EncodeCertPEM(clientCert)
|
||||
tmpl.AuthInfos[name].AuthInfo.ClientKeyData, err = keyutil.MarshalPrivateKeyToPEM(clientKey)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot marshal private key to PEM")
|
||||
return fmt.Errorf("cannot marshal private key to PEM: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,7 +341,7 @@ func (r *KubeconfigGeneratorReconciler) generate(ctx context.Context, generator
|
||||
|
||||
secret.Data["value"], err = utilities.EncodeToYaml(tmpl)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot encode generated Kubeconfig to YAML")
|
||||
return fmt.Errorf("cannot encode generated Kubeconfig to YAML: %w", err)
|
||||
}
|
||||
|
||||
if utilities.IsRotationRequested(secret) {
|
||||
@@ -355,7 +355,7 @@ func (r *KubeconfigGeneratorReconciler) generate(ctx context.Context, generator
|
||||
return ctrl.SetControllerReference(tcp, secret, r.Client.Scheme())
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot create or update generated Kubeconfig")
|
||||
return fmt.Errorf("cannot create or update generated Kubeconfig: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -74,6 +74,7 @@ func GetResources(ctx context.Context, config GroupResourceBuilderConfiguration)
|
||||
// Conditionally add Gateway resources
|
||||
if utilities.AreGatewayResourcesAvailable(ctx, config.client, config.DiscoveryClient) {
|
||||
resources = append(resources, getKubernetesGatewayResources(config.client)...)
|
||||
resources = append(resources, getKonnectivityGatewayResources(config.client)...)
|
||||
}
|
||||
|
||||
return resources
|
||||
@@ -146,6 +147,14 @@ func getKubernetesGatewayResources(c client.Client) []resources.Resource {
|
||||
}
|
||||
}
|
||||
|
||||
func getKonnectivityGatewayResources(c client.Client) []resources.Resource {
|
||||
return []resources.Resource{
|
||||
&konnectivity.KubernetesKonnectivityGatewayResource{
|
||||
Client: c,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getKubeadmConfigResources(c client.Client, tmpDirectory string, dataStore kamajiv1alpha1.DataStore) []resources.Resource {
|
||||
var endpoints []string
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/pkg/errors"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
@@ -36,6 +36,7 @@ type CoreDNS struct {
|
||||
AdminClient client.Client
|
||||
GetTenantControlPlaneFunc utils.TenantControlPlaneRetrievalFn
|
||||
TriggerChannel chan event.GenericEvent
|
||||
ControllerName string
|
||||
}
|
||||
|
||||
func (c *CoreDNS) Reconcile(ctx context.Context, _ reconcile.Request) (reconcile.Result, error) {
|
||||
@@ -80,6 +81,7 @@ func (c *CoreDNS) Reconcile(ctx context.Context, _ reconcile.Request) (reconcile
|
||||
|
||||
func (c *CoreDNS) SetupWithManager(mgr manager.Manager) error {
|
||||
return controllerruntime.NewControllerManagedBy(mgr).
|
||||
Named(c.ControllerName).
|
||||
WithOptions(controller.TypedOptions[reconcile.Request]{SkipNameValidation: ptr.To(true)}).
|
||||
For(&rbacv1.ClusterRoleBinding{}, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
|
||||
return object.GetName() == kubeadm.CoreDNSClusterRoleBindingName
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
import "errors"
|
||||
|
||||
var ErrPausedReconciliation = errors.New("paused reconciliation, no further actions")
|
||||
|
||||
@@ -5,9 +5,9 @@ package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/pkg/errors"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/api/rbac/v1"
|
||||
@@ -37,6 +37,7 @@ type KonnectivityAgent struct {
|
||||
AdminClient client.Client
|
||||
GetTenantControlPlaneFunc utils.TenantControlPlaneRetrievalFn
|
||||
TriggerChannel chan event.GenericEvent
|
||||
ControllerName string
|
||||
}
|
||||
|
||||
func (k *KonnectivityAgent) Reconcile(ctx context.Context, _ reconcile.Request) (reconcile.Result, error) {
|
||||
@@ -87,6 +88,7 @@ func (k *KonnectivityAgent) Reconcile(ctx context.Context, _ reconcile.Request)
|
||||
|
||||
func (k *KonnectivityAgent) SetupWithManager(mgr manager.Manager) error {
|
||||
return controllerruntime.NewControllerManagedBy(mgr).
|
||||
Named(k.ControllerName).
|
||||
WithOptions(controller.TypedOptions[reconcile.Request]{SkipNameValidation: ptr.To(true)}).
|
||||
For(&appsv1.DaemonSet{}, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
|
||||
return object.GetName() == konnectivity.AgentName && object.GetNamespace() == konnectivity.AgentNamespace
|
||||
|
||||
@@ -5,9 +5,9 @@ package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/utils/ptr"
|
||||
controllerruntime "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
@@ -29,6 +29,7 @@ type KubeadmPhase struct {
|
||||
GetTenantControlPlaneFunc utils.TenantControlPlaneRetrievalFn
|
||||
TriggerChannel chan event.GenericEvent
|
||||
Phase resources.KubeadmPhaseResource
|
||||
ControllerName string
|
||||
|
||||
logger logr.Logger
|
||||
}
|
||||
@@ -75,6 +76,7 @@ func (k *KubeadmPhase) SetupWithManager(mgr manager.Manager) error {
|
||||
k.logger = mgr.GetLogger().WithName(k.Phase.GetName())
|
||||
|
||||
return controllerruntime.NewControllerManagedBy(mgr).
|
||||
Named(k.ControllerName).
|
||||
WithOptions(controller.TypedOptions[reconcile.Request]{SkipNameValidation: ptr.To(true)}).
|
||||
For(k.Phase.GetWatchedObject(), builder.WithPredicates(predicate.NewPredicateFuncs(k.Phase.GetPredicateFunc()))).
|
||||
WatchesRawSource(source.Channel(k.TriggerChannel, &handler.EnqueueRequestForObject{})).
|
||||
|
||||
@@ -5,9 +5,9 @@ package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/pkg/errors"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
@@ -36,6 +36,7 @@ type KubeProxy struct {
|
||||
AdminClient client.Client
|
||||
GetTenantControlPlaneFunc utils.TenantControlPlaneRetrievalFn
|
||||
TriggerChannel chan event.GenericEvent
|
||||
ControllerName string
|
||||
}
|
||||
|
||||
func (k *KubeProxy) Reconcile(ctx context.Context, _ reconcile.Request) (reconcile.Result, error) {
|
||||
@@ -82,6 +83,7 @@ func (k *KubeProxy) Reconcile(ctx context.Context, _ reconcile.Request) (reconci
|
||||
|
||||
func (k *KubeProxy) SetupWithManager(mgr manager.Manager) error {
|
||||
return controllerruntime.NewControllerManagedBy(mgr).
|
||||
Named(k.ControllerName).
|
||||
WithOptions(controller.TypedOptions[reconcile.Request]{SkipNameValidation: ptr.To(true)}).
|
||||
For(&rbacv1.ClusterRoleBinding{}, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
|
||||
return object.GetName() == kubeadm.KubeProxyClusterRoleBindingName
|
||||
|
||||
@@ -5,11 +5,11 @@ package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/pkg/errors"
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -39,6 +39,7 @@ type Migrate struct {
|
||||
WebhookServiceName string
|
||||
WebhookCABundle []byte
|
||||
TriggerChannel chan event.GenericEvent
|
||||
ControllerName string
|
||||
}
|
||||
|
||||
func (m *Migrate) Reconcile(ctx context.Context, _ reconcile.Request) (reconcile.Result, error) {
|
||||
@@ -189,6 +190,7 @@ func (m *Migrate) SetupWithManager(mgr manager.Manager) error {
|
||||
m.TriggerChannel = make(chan event.GenericEvent)
|
||||
|
||||
return controllerruntime.NewControllerManagedBy(mgr).
|
||||
Named(m.ControllerName).
|
||||
WithOptions(controller.TypedOptions[reconcile.Request]{SkipNameValidation: pointer.To(true)}).
|
||||
For(&admissionregistrationv1.ValidatingWebhookConfiguration{}, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
|
||||
vwc := m.object()
|
||||
|
||||
@@ -39,6 +39,7 @@ type WritePermissions struct {
|
||||
WebhookServiceName string
|
||||
WebhookCABundle []byte
|
||||
TriggerChannel chan event.GenericEvent
|
||||
ControllerName string
|
||||
}
|
||||
|
||||
func (r *WritePermissions) Reconcile(ctx context.Context, _ reconcile.Request) (reconcile.Result, error) {
|
||||
@@ -190,6 +191,7 @@ func (r *WritePermissions) SetupWithManager(mgr manager.Manager) error {
|
||||
r.TriggerChannel = make(chan event.GenericEvent)
|
||||
|
||||
return controllerruntime.NewControllerManagedBy(mgr).
|
||||
Named(r.ControllerName).
|
||||
WithOptions(controller.TypedOptions[reconcile.Request]{SkipNameValidation: ptr.To(true)}).
|
||||
For(&admissionregistrationv1.ValidatingWebhookConfiguration{}, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
|
||||
return object.GetName() == r.object().GetName()
|
||||
|
||||
@@ -253,6 +253,9 @@ func (m *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
|
||||
//
|
||||
// Register all the controllers of the soot here:
|
||||
//
|
||||
// Generate unique controller name prefix from TenantControlPlane to avoid metric conflicts
|
||||
controllerNamePrefix := fmt.Sprintf("%s-%s", tcp.GetNamespace(), tcp.GetName())
|
||||
|
||||
writePermissions := &controllers.WritePermissions{
|
||||
Logger: mgr.GetLogger().WithName("writePermissions"),
|
||||
Client: mgr.GetClient(),
|
||||
@@ -261,6 +264,7 @@ func (m *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
|
||||
WebhookServiceName: m.MigrateServiceName,
|
||||
WebhookCABundle: m.MigrateCABundle,
|
||||
TriggerChannel: nil,
|
||||
ControllerName: fmt.Sprintf("%s-writepermissions", controllerNamePrefix),
|
||||
}
|
||||
if err = writePermissions.SetupWithManager(mgr); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
@@ -273,6 +277,7 @@ func (m *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
|
||||
GetTenantControlPlaneFunc: m.retrieveTenantControlPlane(tcpCtx, request),
|
||||
Client: mgr.GetClient(),
|
||||
Logger: mgr.GetLogger().WithName("migrate"),
|
||||
ControllerName: fmt.Sprintf("%s-migrate", controllerNamePrefix),
|
||||
}
|
||||
if err = migrate.SetupWithManager(mgr); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
@@ -283,6 +288,7 @@ func (m *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
|
||||
GetTenantControlPlaneFunc: m.retrieveTenantControlPlane(tcpCtx, request),
|
||||
Logger: mgr.GetLogger().WithName("konnectivity_agent"),
|
||||
TriggerChannel: make(chan event.GenericEvent),
|
||||
ControllerName: fmt.Sprintf("%s-konnectivity", controllerNamePrefix),
|
||||
}
|
||||
if err = konnectivityAgent.SetupWithManager(mgr); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
@@ -293,6 +299,7 @@ func (m *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
|
||||
GetTenantControlPlaneFunc: m.retrieveTenantControlPlane(tcpCtx, request),
|
||||
Logger: mgr.GetLogger().WithName("kube_proxy"),
|
||||
TriggerChannel: make(chan event.GenericEvent),
|
||||
ControllerName: fmt.Sprintf("%s-kubeproxy", controllerNamePrefix),
|
||||
}
|
||||
if err = kubeProxy.SetupWithManager(mgr); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
@@ -303,6 +310,7 @@ func (m *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
|
||||
GetTenantControlPlaneFunc: m.retrieveTenantControlPlane(tcpCtx, request),
|
||||
Logger: mgr.GetLogger().WithName("coredns"),
|
||||
TriggerChannel: make(chan event.GenericEvent),
|
||||
ControllerName: fmt.Sprintf("%s-coredns", controllerNamePrefix),
|
||||
}
|
||||
if err = coreDNS.SetupWithManager(mgr); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
@@ -315,6 +323,7 @@ func (m *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
|
||||
Phase: resources.PhaseUploadConfigKubeadm,
|
||||
},
|
||||
TriggerChannel: make(chan event.GenericEvent),
|
||||
ControllerName: fmt.Sprintf("%s-kubeadmconfig", controllerNamePrefix),
|
||||
}
|
||||
if err = uploadKubeadmConfig.SetupWithManager(mgr); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
@@ -327,6 +336,7 @@ func (m *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
|
||||
Phase: resources.PhaseUploadConfigKubelet,
|
||||
},
|
||||
TriggerChannel: make(chan event.GenericEvent),
|
||||
ControllerName: fmt.Sprintf("%s-kubeletconfig", controllerNamePrefix),
|
||||
}
|
||||
if err = uploadKubeletConfig.SetupWithManager(mgr); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
@@ -339,6 +349,7 @@ func (m *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
|
||||
Phase: resources.PhaseBootstrapToken,
|
||||
},
|
||||
TriggerChannel: make(chan event.GenericEvent),
|
||||
ControllerName: fmt.Sprintf("%s-bootstraptoken", controllerNamePrefix),
|
||||
}
|
||||
if err = bootstrapToken.SetupWithManager(mgr); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
@@ -351,6 +362,7 @@ func (m *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
|
||||
Phase: resources.PhaseClusterAdminRBAC,
|
||||
},
|
||||
TriggerChannel: make(chan event.GenericEvent),
|
||||
ControllerName: fmt.Sprintf("%s-kubeadmrbac", controllerNamePrefix),
|
||||
}
|
||||
if err = kubeadmRbac.SetupWithManager(mgr); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
|
||||
@@ -5,11 +5,11 @@ package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/clastix/kamaji-telemetry/api"
|
||||
telemetry "github.com/clastix/kamaji-telemetry/pkg/client"
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/utils/ptr"
|
||||
@@ -31,7 +31,7 @@ type TelemetryController struct {
|
||||
func (m *TelemetryController) retrieveControllerUID(ctx context.Context) (string, error) {
|
||||
var defaultSvc corev1.Service
|
||||
if err := m.Client.Get(ctx, types.NamespacedName{Name: "kubernetes", Namespace: "default"}, &defaultSvc); err != nil {
|
||||
return "", errors.Wrap(err, "cannot start the telemetry controller")
|
||||
return "", fmt.Errorf("cannot start the telemetry controller: %w", err)
|
||||
}
|
||||
|
||||
return string(defaultSvc.UID), nil
|
||||
|
||||
@@ -5,12 +5,12 @@ package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/juju/mutex/v2"
|
||||
"github.com/pkg/errors"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
k8stypes "k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
"k8s.io/utils/clock"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
@@ -115,11 +116,11 @@ func (r *TenantControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R
|
||||
releaser, err := mutex.Acquire(r.mutexSpec(tenantControlPlane))
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.As(err, &mutex.ErrTimeout):
|
||||
case errors.Is(err, mutex.ErrTimeout):
|
||||
log.Info("acquire timed out, current process is blocked by another reconciliation")
|
||||
|
||||
return ctrl.Result{RequeueAfter: time.Second}, nil
|
||||
case errors.As(err, &mutex.ErrCancelled):
|
||||
case errors.Is(err, mutex.ErrCancelled):
|
||||
log.Info("acquire cancelled")
|
||||
|
||||
return ctrl.Result{RequeueAfter: time.Second}, nil
|
||||
@@ -260,6 +261,23 @@ func (r *TenantControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R
|
||||
|
||||
log.Info(fmt.Sprintf("%s has been reconciled", tenantControlPlane.GetName()))
|
||||
|
||||
// Set ObservedGeneration only on successful reconciliation completion.
|
||||
// This follows Cluster API conventions where ObservedGeneration indicates
|
||||
// the controller has fully processed the given generation.
|
||||
if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
if getErr := r.Client.Get(ctx, req.NamespacedName, tenantControlPlane); getErr != nil {
|
||||
return getErr
|
||||
}
|
||||
|
||||
tenantControlPlane.Status.ObservedGeneration = tenantControlPlane.Generation
|
||||
|
||||
return r.Client.Status().Update(ctx, tenantControlPlane)
|
||||
}); err != nil {
|
||||
log.Error(err, "failed to update ObservedGeneration")
|
||||
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
@@ -379,7 +397,7 @@ func (r *TenantControlPlaneReconciler) dataStore(ctx context.Context, tenantCont
|
||||
|
||||
var ds kamajiv1alpha1.DataStore
|
||||
if err := r.Client.Get(ctx, k8stypes.NamespacedName{Name: tenantControlPlane.Spec.DataStore}, &ds); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot retrieve *kamajiv1alpha.DataStore object")
|
||||
return nil, fmt.Errorf("cannot retrieve *kamajiv1alpha.DataStore object: %w", err)
|
||||
}
|
||||
|
||||
return &ds, nil
|
||||
@@ -391,7 +409,7 @@ func (r *TenantControlPlaneReconciler) dataStoreOverride(ctx context.Context, te
|
||||
for _, dso := range tenantControlPlane.Spec.DataStoreOverrides {
|
||||
var ds kamajiv1alpha1.DataStore
|
||||
if err := r.Client.Get(ctx, k8stypes.NamespacedName{Name: dso.DataStore}, &ds); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot retrieve *kamajiv1alpha.DataStore object")
|
||||
return nil, fmt.Errorf("cannot retrieve *kamajiv1alpha.DataStore object: %w", err)
|
||||
}
|
||||
if ds.Spec.Driver != kamajiv1alpha1.EtcdDriver {
|
||||
return nil, errors.New("DataStoreOverrides can only use ETCD driver")
|
||||
|
||||
@@ -32,16 +32,18 @@ Kamaji has a dependency on Cert Manager, as it uses dynamic admission control, v
|
||||
Add the Bitnami Repo to the Helm Manager.
|
||||
|
||||
```
|
||||
helm repo add bitnami https://charts.bitnami.com/bitnami
|
||||
helm repo add jetstack https://charts.jetstack.io
|
||||
helm repo update
|
||||
```
|
||||
|
||||
Install Cert Manager using Helm
|
||||
|
||||
```
|
||||
helm upgrade --install cert-manager bitnami/cert-manager \
|
||||
--namespace certmanager-system \
|
||||
helm install \
|
||||
cert-manager jetstack/cert-manager \
|
||||
--namespace cert-manager \
|
||||
--create-namespace \
|
||||
--set "installCRDs=true"
|
||||
--set installCRDs=true
|
||||
```
|
||||
|
||||
This will install cert-manager to the cluster. You can watch the progress of the installation on the cluster using the command
|
||||
@@ -55,7 +57,7 @@ kubectl get pods -Aw
|
||||
MetalLB is used in order to dynamically assign IP addresses to the components, and also define custom IP Address Pools. Install MetalLb using the `kubectl` command for apply the manifest:
|
||||
|
||||
```
|
||||
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.7/config/manifests/metallb-native.yaml
|
||||
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.15.3/config/manifests/metallb-native.yaml
|
||||
```
|
||||
|
||||
This will install MetalLb onto the cluster with all the necessary resources.
|
||||
@@ -65,7 +67,11 @@ This will install MetalLb onto the cluster with all the necessary resources.
|
||||
Extract the Gateway IP of the network Kind is running on.
|
||||
|
||||
```
|
||||
# docker
|
||||
GW_IP=$(docker network inspect -f '{{range .IPAM.Config}}{{.Gateway}}{{end}}' kind)
|
||||
|
||||
# podman
|
||||
GW_IP=$(podman network inspect kind --format '{{(index .Subnets 1).Gateway}}')
|
||||
```
|
||||
|
||||
Modify the IP Address, and create the resource to be added to the cluster to create the IP Address Pool
|
||||
|
||||
213
docs/content/guides/gateway-api.md
Normal file
213
docs/content/guides/gateway-api.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# Gateway API Support
|
||||
|
||||
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.
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
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
|
||||
|
||||
3. **DNS is configured** to resolve your hostnames to the Gateway's external address
|
||||
|
||||
4. **Gateway controller is running** (e.g., Envoy Gateway, Istio Gateway, etc.)
|
||||
|
||||
## Configuration
|
||||
|
||||
### 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
|
||||
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:
|
||||
|
||||
```yaml
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: GatewayClass
|
||||
metadata:
|
||||
name: envoy-gw-class
|
||||
spec:
|
||||
controllerName: gateway.envoyproxy.io/gatewayclass-controller
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: gateway
|
||||
namespace: default
|
||||
spec:
|
||||
gatewayClassName: envoy-gw-class
|
||||
listeners:
|
||||
- name: kube-apiserver
|
||||
port: 6443
|
||||
protocol: TLS
|
||||
hostname: 'tcp1.cluster.dev'
|
||||
tls:
|
||||
mode: Passthrough
|
||||
allowedRoutes:
|
||||
kinds:
|
||||
- group: gateway.networking.k8s.io
|
||||
kind: TLSRoute
|
||||
namespaces:
|
||||
from: All
|
||||
|
||||
# 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
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Multiple Tenant Control Planes
|
||||
|
||||
You can use the same Gateway resource for multiple Tenant Control Planes by using different hostnames:
|
||||
|
||||
```yaml
|
||||
# Gateway with wildcard hostname
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: gateway
|
||||
spec:
|
||||
listeners:
|
||||
- hostname: '*.cluster.dev'
|
||||
name: kube-apiserver
|
||||
port: 6443
|
||||
# ...
|
||||
---
|
||||
# Tenant Control Plane 1
|
||||
apiVersion: kamaji.clastix.io/v1alpha1
|
||||
kind: TenantControlPlane
|
||||
metadata:
|
||||
name: tcp-1
|
||||
spec:
|
||||
controlPlane:
|
||||
gateway:
|
||||
hostname: "tcp1.cluster.dev"
|
||||
parentRefs:
|
||||
- name: gateway
|
||||
namespace: default
|
||||
# ...
|
||||
---
|
||||
# Tenant Control Plane 2
|
||||
apiVersion: kamaji.clastix.io/v1alpha1
|
||||
kind: TenantControlPlane
|
||||
metadata:
|
||||
name: tcp-2
|
||||
spec:
|
||||
controlPlane:
|
||||
gateway:
|
||||
hostname: "tcp2.cluster.dev"
|
||||
parentRefs:
|
||||
- name: gateway
|
||||
namespace: default
|
||||
# ...
|
||||
```
|
||||
|
||||
Each Tenant Control Plane will get its own TLSRoutes with the respective hostnames, all routing through the same Gateway resource.
|
||||
|
||||
You can check the Gateway status in the TenantControlPlane:
|
||||
|
||||
```bash
|
||||
kubectl get tenantcontrolplane tcp-1 -o yaml
|
||||
```
|
||||
|
||||
Look for the `status.kubernetesResources.gateway` and `status.addons.konnectivity.gateway` fields.
|
||||
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Gateway API Documentation](https://gateway-api.sigs.k8s.io/)
|
||||
- [Quickstart with Envoy Gateway](https://gateway.envoyproxy.io/docs/tasks/quickstart/)
|
||||
|
||||
|
||||
@@ -27982,6 +27982,15 @@ DataStoreStatus defines the observed state of DataStore.
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>observedGeneration</b></td>
|
||||
<td>integer</td>
|
||||
<td>
|
||||
ObservedGeneration represents the .metadata.generation that was last reconciled.<br/>
|
||||
<br/>
|
||||
<i>Format</i>: int64<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>usedBy</b></td>
|
||||
<td>[]string</td>
|
||||
<td>
|
||||
@@ -28365,6 +28374,15 @@ In case of a different value compared to Resources, check the field errors.<br/>
|
||||
Errors is the list of failed kubeconfig generations.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>observedGeneration</b></td>
|
||||
<td>integer</td>
|
||||
<td>
|
||||
ObservedGeneration represents the .metadata.generation that was last reconciled.<br/>
|
||||
<br/>
|
||||
<i>Format</i>: int64<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
@@ -41774,11 +41792,24 @@ Full reference available here: https://kubernetes.io/docs/reference/access-authn
|
||||
<td>enum</td>
|
||||
<td>
|
||||
CGroupFS defines the cgroup driver for Kubelet
|
||||
https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/configure-cgroup-driver/<br/>
|
||||
https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/configure-cgroup-driver/
|
||||
|
||||
Deprecated: use ConfigurationJSONPatches.<br/>
|
||||
<br/>
|
||||
<i>Enum</i>: systemd, cgroupfs<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantcontrolplanespeckuberneteskubeletconfigurationjsonpatchesindex">configurationJSONPatches</a></b></td>
|
||||
<td>[]object</td>
|
||||
<td>
|
||||
ConfigurationJSONPatches contains the RFC 6902 JSON patches to customise the kubeadm generate configuration,
|
||||
useful to customise and mangling the configuration according to your needs;
|
||||
e.g.: configuring the cgroup driver used by Kubelet is possible via the following patch:
|
||||
|
||||
[{"op": "replace", "path": "/cgroupDriver", "value": "systemd"}]<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>preferredAddressTypes</b></td>
|
||||
<td>[]enum</td>
|
||||
@@ -41794,6 +41825,54 @@ Default to InternalIP, ExternalIP, Hostname.<br/>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanespeckuberneteskubeletconfigurationjsonpatchesindex">`TenantControlPlane.spec.kubernetes.kubelet.configurationJSONPatches[index]`</span>
|
||||
|
||||
|
||||
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>op</b></td>
|
||||
<td>enum</td>
|
||||
<td>
|
||||
Op is the RFC 6902 JSON Patch operation.<br/>
|
||||
<br/>
|
||||
<i>Enum</i>: add, remove, replace, move, copy, test<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>path</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Path specifies the target location in the JSON document. Use "/" to separate keys; "-" for appending to arrays.<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>from</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
From specifies the source location for move or copy operations.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>value</b></td>
|
||||
<td>JSON</td>
|
||||
<td>
|
||||
Value is the operation value to be used when Op is add, replace, test.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanespecaddons">`TenantControlPlane.spec.addons`</span>
|
||||
|
||||
|
||||
@@ -42494,6 +42573,15 @@ that are necessary to run a kubernetes control plane<br/>
|
||||
Kubernetes contains information about the reconciliation of the required Kubernetes resources deployed in the admin cluster<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>observedGeneration</b></td>
|
||||
<td>integer</td>
|
||||
<td>
|
||||
ObservedGeneration represents the .metadata.generation that was last reconciled.<br/>
|
||||
<br/>
|
||||
<i>Format</i>: int64<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantcontrolplanestatusstorage">storage</a></b></td>
|
||||
<td>object</td>
|
||||
@@ -42627,6 +42715,13 @@ KonnectivityStatus defines the status of Konnectivity as Addon.
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantcontrolplanestatusaddonskonnectivitygateway">gateway</a></b></td>
|
||||
<td>object</td>
|
||||
<td>
|
||||
KubernetesGatewayStatus defines the status for the Tenant Control Plane Gateway in the management cluster.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantcontrolplanestatusaddonskonnectivitykubeconfig">kubeconfig</a></b></td>
|
||||
<td>object</td>
|
||||
@@ -42814,6 +42909,505 @@ CertificatePrivateKeyPairStatus defines the status.
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanestatusaddonskonnectivitygateway">`TenantControlPlane.status.addons.konnectivity.gateway`</span>
|
||||
|
||||
|
||||
KubernetesGatewayStatus defines the status for the Tenant Control Plane Gateway in the management cluster.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b><a href="#tenantcontrolplanestatusaddonskonnectivitygatewayparentsindex">parents</a></b></td>
|
||||
<td>[]object</td>
|
||||
<td>
|
||||
Parents is a list of parent resources (usually Gateways) that are
|
||||
associated with the route, and the status of the route with respect to
|
||||
each parent. When this route attaches to a parent, the controller that
|
||||
manages the parent must add an entry to this list when the controller
|
||||
first sees the route and should update the entry as appropriate when the
|
||||
route or gateway is modified.
|
||||
|
||||
Note that parent references that cannot be resolved by an implementation
|
||||
of this API will not be added to this list. Implementations of this API
|
||||
can only populate Route status for the Gateways/parent resources they are
|
||||
responsible for.
|
||||
|
||||
A maximum of 32 Gateways will be represented in this list. An empty list
|
||||
means the route has not been attached to any Gateway.
|
||||
|
||||
<gateway:util:excludeFromCRD>
|
||||
Notes for implementors:
|
||||
|
||||
While parents is not a listType `map`, this is due to the fact that the
|
||||
list key is not scalar, and Kubernetes is unable to represent this.
|
||||
|
||||
Parent status MUST be considered to be namespaced by the combination of
|
||||
the parentRef and controllerName fields, and implementations should keep
|
||||
the following rules in mind when updating this status:
|
||||
|
||||
* Implementations MUST update only entries that have a matching value of
|
||||
`controllerName` for that implementation.
|
||||
* Implementations MUST NOT update entries with non-matching `controllerName`
|
||||
fields.
|
||||
* Implementations MUST treat each `parentRef`` in the Route separately and
|
||||
update its status based on the relationship with that parent.
|
||||
* Implementations MUST perform a read-modify-write cycle on this field
|
||||
before modifying it. That is, when modifying this field, implementations
|
||||
must be confident they have fetched the most recent version of this field,
|
||||
and ensure that changes they make are on that recent version.
|
||||
|
||||
</gateway:util:excludeFromCRD><br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantcontrolplanestatusaddonskonnectivitygatewayaccesspointsindex">accessPoints</a></b></td>
|
||||
<td>[]object</td>
|
||||
<td>
|
||||
A list of valid access points that the route exposes.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantcontrolplanestatusaddonskonnectivitygatewayrouteref">routeRef</a></b></td>
|
||||
<td>object</td>
|
||||
<td>
|
||||
Reference to the route created for this tenant.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanestatusaddonskonnectivitygatewayparentsindex">`TenantControlPlane.status.addons.konnectivity.gateway.parents[index]`</span>
|
||||
|
||||
|
||||
RouteParentStatus describes the status of a route with respect to an
|
||||
associated Parent.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b><a href="#tenantcontrolplanestatusaddonskonnectivitygatewayparentsindexconditionsindex">conditions</a></b></td>
|
||||
<td>[]object</td>
|
||||
<td>
|
||||
Conditions describes the status of the route with respect to the Gateway.
|
||||
Note that the route's availability is also subject to the Gateway's own
|
||||
status conditions and listener status.
|
||||
|
||||
If the Route's ParentRef specifies an existing Gateway that supports
|
||||
Routes of this kind AND that Gateway's controller has sufficient access,
|
||||
then that Gateway's controller MUST set the "Accepted" condition on the
|
||||
Route, to indicate whether the route has been accepted or rejected by the
|
||||
Gateway, and why.
|
||||
|
||||
A Route MUST be considered "Accepted" if at least one of the Route's
|
||||
rules is implemented by the Gateway.
|
||||
|
||||
There are a number of cases where the "Accepted" condition may not be set
|
||||
due to lack of controller visibility, that includes when:
|
||||
|
||||
* The Route refers to a nonexistent parent.
|
||||
* The Route is of a type that the controller does not support.
|
||||
* The Route is in a namespace the controller does not have access to.
|
||||
|
||||
<gateway:util:excludeFromCRD>
|
||||
|
||||
Notes for implementors:
|
||||
|
||||
Conditions are a listType `map`, which means that they function like a
|
||||
map with a key of the `type` field _in the k8s apiserver_.
|
||||
|
||||
This means that implementations must obey some rules when updating this
|
||||
section.
|
||||
|
||||
* Implementations MUST perform a read-modify-write cycle on this field
|
||||
before modifying it. That is, when modifying this field, implementations
|
||||
must be confident they have fetched the most recent version of this field,
|
||||
and ensure that changes they make are on that recent version.
|
||||
* Implementations MUST NOT remove or reorder Conditions that they are not
|
||||
directly responsible for. For example, if an implementation sees a Condition
|
||||
with type `special.io/SomeField`, it MUST NOT remove, change or update that
|
||||
Condition.
|
||||
* Implementations MUST always _merge_ changes into Conditions of the same Type,
|
||||
rather than creating more than one Condition of the same Type.
|
||||
* Implementations MUST always update the `observedGeneration` field of the
|
||||
Condition to the `metadata.generation` of the Gateway at the time of update creation.
|
||||
* If the `observedGeneration` of a Condition is _greater than_ the value the
|
||||
implementation knows about, then it MUST NOT perform the update on that Condition,
|
||||
but must wait for a future reconciliation and status update. (The assumption is that
|
||||
the implementation's copy of the object is stale and an update will be re-triggered
|
||||
if relevant.)
|
||||
|
||||
</gateway:util:excludeFromCRD><br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>controllerName</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
ControllerName is a domain/path string that indicates the name of the
|
||||
controller that wrote this status. This corresponds with the
|
||||
controllerName field on GatewayClass.
|
||||
|
||||
Example: "example.net/gateway-controller".
|
||||
|
||||
The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are
|
||||
valid Kubernetes names
|
||||
(https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).
|
||||
|
||||
Controllers MUST populate this field when writing status. Controllers should ensure that
|
||||
entries to status populated with their ControllerName are cleaned up when they are no
|
||||
longer necessary.<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantcontrolplanestatusaddonskonnectivitygatewayparentsindexparentref">parentRef</a></b></td>
|
||||
<td>object</td>
|
||||
<td>
|
||||
ParentRef corresponds with a ParentRef in the spec that this
|
||||
RouteParentStatus struct describes the status of.<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanestatusaddonskonnectivitygatewayparentsindexconditionsindex">`TenantControlPlane.status.addons.konnectivity.gateway.parents[index].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>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanestatusaddonskonnectivitygatewayparentsindexparentref">`TenantControlPlane.status.addons.konnectivity.gateway.parents[index].parentRef`</span>
|
||||
|
||||
|
||||
ParentRef corresponds with a ParentRef in the spec that this
|
||||
RouteParentStatus struct describes the status of.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>name</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Name is the name of the referent.
|
||||
|
||||
Support: Core<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>group</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Group is the group of the referent.
|
||||
When unspecified, "gateway.networking.k8s.io" is inferred.
|
||||
To set the core API group (such as for a "Service" kind referent),
|
||||
Group must be explicitly set to "" (empty string).
|
||||
|
||||
Support: Core<br/>
|
||||
<br/>
|
||||
<i>Default</i>: gateway.networking.k8s.io<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>kind</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Kind is kind of the referent.
|
||||
|
||||
There are two kinds of parent resources with "Core" support:
|
||||
|
||||
* Gateway (Gateway conformance profile)
|
||||
* Service (Mesh conformance profile, ClusterIP Services only)
|
||||
|
||||
Support for other resources is Implementation-Specific.<br/>
|
||||
<br/>
|
||||
<i>Default</i>: Gateway<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>namespace</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Namespace is the namespace of the referent. When unspecified, this refers
|
||||
to the local namespace of the Route.
|
||||
|
||||
Note that there are specific rules for ParentRefs which cross namespace
|
||||
boundaries. Cross-namespace references are only valid if they are explicitly
|
||||
allowed by something in the namespace they are referring to. For example:
|
||||
Gateway has the AllowedRoutes field, and ReferenceGrant provides a
|
||||
generic way to enable any other kind of cross-namespace reference.
|
||||
|
||||
<gateway:experimental:description>
|
||||
ParentRefs from a Route to a Service in the same namespace are "producer"
|
||||
routes, which apply default routing rules to inbound connections from
|
||||
any namespace to the Service.
|
||||
|
||||
ParentRefs from a Route to a Service in a different namespace are
|
||||
"consumer" routes, and these routing rules are only applied to outbound
|
||||
connections originating from the same namespace as the Route, for which
|
||||
the intended destination of the connections are a Service targeted as a
|
||||
ParentRef of the Route.
|
||||
</gateway:experimental:description>
|
||||
|
||||
Support: Core<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>port</b></td>
|
||||
<td>integer</td>
|
||||
<td>
|
||||
Port is the network port this Route targets. It can be interpreted
|
||||
differently based on the type of parent resource.
|
||||
|
||||
When the parent resource is a Gateway, this targets all listeners
|
||||
listening on the specified port that also support this kind of Route(and
|
||||
select this Route). It's not recommended to set `Port` unless the
|
||||
networking behaviors specified in a Route must apply to a specific port
|
||||
as opposed to a listener(s) whose port(s) may be changed. When both Port
|
||||
and SectionName are specified, the name and port of the selected listener
|
||||
must match both specified values.
|
||||
|
||||
<gateway:experimental:description>
|
||||
When the parent resource is a Service, this targets a specific port in the
|
||||
Service spec. When both Port (experimental) and SectionName are specified,
|
||||
the name and port of the selected port must match both specified values.
|
||||
</gateway:experimental:description>
|
||||
|
||||
Implementations MAY choose to support other parent resources.
|
||||
Implementations supporting other types of parent resources MUST clearly
|
||||
document how/if Port is interpreted.
|
||||
|
||||
For the purpose of status, an attachment is considered successful as
|
||||
long as the parent resource accepts it partially. For example, Gateway
|
||||
listeners can restrict which Routes can attach to them by Route kind,
|
||||
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
|
||||
from the referencing Route, the Route MUST be considered successfully
|
||||
attached. If no Gateway listeners accept attachment from this Route,
|
||||
the Route MUST be considered detached from the Gateway.
|
||||
|
||||
Support: Extended<br/>
|
||||
<br/>
|
||||
<i>Format</i>: int32<br/>
|
||||
<i>Minimum</i>: 1<br/>
|
||||
<i>Maximum</i>: 65535<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>sectionName</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
SectionName is the name of a section within the target resource. In the
|
||||
following resources, SectionName is interpreted as the following:
|
||||
|
||||
* Gateway: Listener name. When both Port (experimental) and SectionName
|
||||
are specified, the name and port of the selected listener must match
|
||||
both specified values.
|
||||
* Service: Port name. When both Port (experimental) and SectionName
|
||||
are specified, the name and port of the selected listener must match
|
||||
both specified values.
|
||||
|
||||
Implementations MAY choose to support attaching Routes to other resources.
|
||||
If that is the case, they MUST clearly document how SectionName is
|
||||
interpreted.
|
||||
|
||||
When unspecified (empty string), this will reference the entire resource.
|
||||
For the purpose of status, an attachment is considered successful if at
|
||||
least one section in the parent resource accepts it. For example, Gateway
|
||||
listeners can restrict which Routes can attach to them by Route kind,
|
||||
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
|
||||
the referencing Route, the Route MUST be considered successfully
|
||||
attached. If no Gateway listeners accept attachment from this Route, the
|
||||
Route MUST be considered detached from the Gateway.
|
||||
|
||||
Support: Core<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanestatusaddonskonnectivitygatewayaccesspointsindex">`TenantControlPlane.status.addons.konnectivity.gateway.accessPoints[index]`</span>
|
||||
|
||||
|
||||
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>port</b></td>
|
||||
<td>integer</td>
|
||||
<td>
|
||||
<br/>
|
||||
<br/>
|
||||
<i>Format</i>: int32<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>type</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
AddressType defines how a network address is represented as a text string.
|
||||
This may take two possible forms:
|
||||
|
||||
* A predefined CamelCase string identifier (currently limited to `IPAddress` or `Hostname`)
|
||||
* A domain-prefixed string identifier (like `acme.io/CustomAddressType`)
|
||||
|
||||
Values `IPAddress` and `Hostname` have Extended support.
|
||||
|
||||
The `NamedAddress` value has been deprecated in favor of implementation
|
||||
specific domain-prefixed strings.
|
||||
|
||||
All other values, including domain-prefixed values have Implementation-specific support,
|
||||
which are used in implementation-specific behaviors. Support for additional
|
||||
predefined CamelCase identifiers may be added in future releases.<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>value</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>urls</b></td>
|
||||
<td>[]string</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanestatusaddonskonnectivitygatewayrouteref">`TenantControlPlane.status.addons.konnectivity.gateway.routeRef`</span>
|
||||
|
||||
|
||||
Reference to the route created for this tenant.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>name</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names<br/>
|
||||
<br/>
|
||||
<i>Default</i>: <br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanestatusaddonskonnectivitykubeconfig">`TenantControlPlane.status.addons.konnectivity.kubeconfig`</span>
|
||||
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ nav:
|
||||
- guides/gitops.md
|
||||
- guides/console.md
|
||||
- guides/kubeconfig-generator.md
|
||||
- guides/gateway-api.md
|
||||
- guides/upgrade.md
|
||||
- guides/monitoring.md
|
||||
- guides/terraform.md
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
pointer "k8s.io/utils/ptr"
|
||||
@@ -68,9 +70,39 @@ var _ = BeforeSuite(func() {
|
||||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(k8sClient).NotTo(BeNil())
|
||||
|
||||
By("creating GatewayClass for Gateway API tests")
|
||||
gatewayClass := &gatewayv1.GatewayClass{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "envoy-gw-class",
|
||||
},
|
||||
Spec: gatewayv1.GatewayClassSpec{
|
||||
ControllerName: "gateway.envoyproxy.io/gatewayclass-controller",
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(context.Background(), gatewayClass)).NotTo(HaveOccurred())
|
||||
|
||||
By("creating Gateway with kube-apiserver and konnectivity-server listeners")
|
||||
CreateGatewayWithListeners("test-gateway", "default", "envoy-gw-class", "*.example.com")
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
By("deleting Gateway resources")
|
||||
gateway := &gatewayv1.Gateway{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-gateway",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
_ = k8sClient.Delete(context.Background(), gateway)
|
||||
|
||||
gatewayClass := &gatewayv1.GatewayClass{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "envoy-gw-class",
|
||||
},
|
||||
}
|
||||
_ = k8sClient.Delete(context.Background(), gatewayClass)
|
||||
|
||||
By("tearing down the test environment")
|
||||
err := testEnv.Stop()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
@@ -18,7 +18,7 @@ var _ = Describe("Deploy a TenantControlPlane resource with DataStoreOverrides",
|
||||
// Fill TenantControlPlane object
|
||||
tcp := &kamajiv1alpha1.TenantControlPlane{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tcp-clusterip",
|
||||
Name: "tcp-datastore-overrides",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: kamajiv1alpha1.TenantControlPlaneSpec{
|
||||
|
||||
151
e2e/tcp_gateway_konnectivity_ready_test.go
Normal file
151
e2e/tcp_gateway_konnectivity_ready_test.go
Normal file
@@ -0,0 +1,151 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
pointer "k8s.io/utils/ptr"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
)
|
||||
|
||||
var _ = Describe("Deploy a TenantControlPlane with Gateway API and Konnectivity", func() {
|
||||
var tcp *kamajiv1alpha1.TenantControlPlane
|
||||
|
||||
JustBeforeEach(func() {
|
||||
tcp = &kamajiv1alpha1.TenantControlPlane{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tcp-konnectivity-gateway",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: kamajiv1alpha1.TenantControlPlaneSpec{
|
||||
ControlPlane: kamajiv1alpha1.ControlPlane{
|
||||
Deployment: kamajiv1alpha1.DeploymentSpec{
|
||||
Replicas: pointer.To(int32(1)),
|
||||
},
|
||||
Service: kamajiv1alpha1.ServiceSpec{
|
||||
ServiceType: "ClusterIP",
|
||||
},
|
||||
Gateway: &kamajiv1alpha1.GatewaySpec{
|
||||
Hostname: gatewayv1.Hostname("tcp-gateway-konnectivity.example.com"),
|
||||
AdditionalMetadata: kamajiv1alpha1.AdditionalMetadata{
|
||||
Labels: map[string]string{
|
||||
"test.kamaji.io/gateway": "true",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"test.kamaji.io/created-by": "e2e-test",
|
||||
},
|
||||
},
|
||||
GatewayParentRefs: []gatewayv1.ParentReference{
|
||||
{
|
||||
Name: "test-gateway",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
NetworkProfile: kamajiv1alpha1.NetworkProfileSpec{
|
||||
Address: "172.18.0.4",
|
||||
},
|
||||
Kubernetes: kamajiv1alpha1.KubernetesSpec{
|
||||
Version: "v1.28.0",
|
||||
Kubelet: kamajiv1alpha1.KubeletSpec{
|
||||
CGroupFS: "cgroupfs",
|
||||
},
|
||||
AdmissionControllers: kamajiv1alpha1.AdmissionControllers{
|
||||
"LimitRanger",
|
||||
"ResourceQuota",
|
||||
},
|
||||
},
|
||||
Addons: kamajiv1alpha1.AddonsSpec{
|
||||
Konnectivity: &kamajiv1alpha1.KonnectivitySpec{
|
||||
KonnectivityServerSpec: kamajiv1alpha1.KonnectivityServerSpec{
|
||||
Port: 8132,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(context.Background(), tcp)).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.Background(), tcp)).Should(Succeed())
|
||||
|
||||
// Wait for the object to be completely deleted
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(context.Background(), types.NamespacedName{
|
||||
Name: tcp.Name,
|
||||
Namespace: tcp.Namespace,
|
||||
}, &kamajiv1alpha1.TenantControlPlane{})
|
||||
|
||||
return err != nil // Returns true when object is not found (deleted)
|
||||
}).WithTimeout(time.Minute).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("Should be Ready", func() {
|
||||
StatusMustEqualTo(tcp, kamajiv1alpha1.VersionReady)
|
||||
})
|
||||
|
||||
It("Should create Konnectivity TLSRoute with correct sectionName", func() {
|
||||
Eventually(func() error {
|
||||
route := &gatewayv1alpha2.TLSRoute{}
|
||||
if err := k8sClient.Get(context.Background(), types.NamespacedName{
|
||||
Name: tcp.Name + "-konnectivity",
|
||||
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("konnectivity-server") {
|
||||
return fmt.Errorf("expected sectionName 'konnectivity-server', got '%s'", *route.Spec.ParentRefs[0].SectionName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}).WithTimeout(time.Minute).Should(Succeed())
|
||||
})
|
||||
|
||||
It("Should use same hostname for both TLSRoutes", func() {
|
||||
Eventually(func() error {
|
||||
controlPlaneRoute := &gatewayv1alpha2.TLSRoute{}
|
||||
if err := k8sClient.Get(context.Background(), types.NamespacedName{
|
||||
Name: tcp.Name,
|
||||
Namespace: tcp.Namespace,
|
||||
}, controlPlaneRoute); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
konnectivityRoute := &gatewayv1alpha2.TLSRoute{}
|
||||
if err := k8sClient.Get(context.Background(), types.NamespacedName{
|
||||
Name: tcp.Name + "-konnectivity",
|
||||
Namespace: tcp.Namespace,
|
||||
}, konnectivityRoute); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(controlPlaneRoute.Spec.Hostnames) == 0 || len(konnectivityRoute.Spec.Hostnames) == 0 {
|
||||
return fmt.Errorf("hostnames are empty")
|
||||
}
|
||||
if controlPlaneRoute.Spec.Hostnames[0] != konnectivityRoute.Spec.Hostnames[0] {
|
||||
return fmt.Errorf("hostnames do not match: control plane '%s', konnectivity '%s'",
|
||||
controlPlaneRoute.Spec.Hostnames[0], konnectivityRoute.Spec.Hostnames[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
}).WithTimeout(time.Minute).Should(Succeed())
|
||||
})
|
||||
})
|
||||
@@ -5,6 +5,7 @@ package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
@@ -89,14 +90,39 @@ var _ = Describe("Deploy a TenantControlPlane with Gateway API", func() {
|
||||
StatusMustEqualTo(tcp, kamajiv1alpha1.VersionReady)
|
||||
})
|
||||
|
||||
It("Should create TLSRoute resource", func() {
|
||||
It("Should create control plane TLSRoute with correct sectionName", func() {
|
||||
Eventually(func() error {
|
||||
route := &gatewayv1alpha2.TLSRoute{}
|
||||
// TODO: Check ownership.
|
||||
return k8sClient.Get(context.Background(), types.NamespacedName{
|
||||
if err := k8sClient.Get(context.Background(), types.NamespacedName{
|
||||
Name: tcp.Name,
|
||||
Namespace: tcp.Namespace,
|
||||
}, route)
|
||||
}, 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("kube-apiserver") {
|
||||
return fmt.Errorf("expected sectionName 'kube-apiserver', got '%s'", *route.Spec.ParentRefs[0].SectionName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}).WithTimeout(time.Minute).Should(Succeed())
|
||||
})
|
||||
|
||||
It("Should not create Konnectivity TLSRoute", func() {
|
||||
// Verify Konnectivity route is not created
|
||||
Consistently(func() error {
|
||||
route := &gatewayv1alpha2.TLSRoute{}
|
||||
|
||||
return k8sClient.Get(context.Background(), types.NamespacedName{
|
||||
Name: tcp.Name + "-konnectivity",
|
||||
Namespace: tcp.Namespace,
|
||||
}, route)
|
||||
}, 10*time.Second, time.Second).Should(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,10 +5,12 @@ package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
pointer "k8s.io/utils/ptr"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
@@ -59,5 +61,15 @@ var _ = Describe("Deploy a TenantControlPlane resource", func() {
|
||||
// Check if TenantControlPlane resource has been created
|
||||
It("Should be Ready", func() {
|
||||
StatusMustEqualTo(tcp, kamajiv1alpha1.VersionReady)
|
||||
// ObservedGeneration is set at the end of successful reconciliation,
|
||||
// after status becomes Ready, so we need to wait for it.
|
||||
Eventually(func() int64 {
|
||||
if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: tcp.GetName(), Namespace: tcp.GetNamespace()}, tcp); err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
return tcp.Status.ObservedGeneration
|
||||
}, 30*time.Second, time.Second).Should(Equal(tcp.Generation),
|
||||
"ObservedGeneration should equal Generation after successful reconciliation")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,7 +19,9 @@ import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/util/retry"
|
||||
pointer "k8s.io/utils/ptr"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
)
|
||||
@@ -209,3 +211,60 @@ func ScaleTenantControlPlane(tcp *kamajiv1alpha1.TenantControlPlane, replicas in
|
||||
})
|
||||
Expect(err).To(Succeed())
|
||||
}
|
||||
|
||||
// CreateGatewayWithListeners creates a Gateway with both kube-apiserver and konnectivity-server listeners.
|
||||
func CreateGatewayWithListeners(gatewayName, namespace, gatewayClassName, hostname string) {
|
||||
GinkgoHelper()
|
||||
gateway := &gatewayv1.Gateway{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: gatewayName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: gatewayv1.GatewaySpec{
|
||||
GatewayClassName: gatewayv1.ObjectName(gatewayClassName),
|
||||
Listeners: []gatewayv1.Listener{
|
||||
{
|
||||
Name: "kube-apiserver",
|
||||
Port: 6443,
|
||||
Protocol: gatewayv1.TLSProtocolType,
|
||||
Hostname: pointer.To(gatewayv1.Hostname(hostname)),
|
||||
TLS: &gatewayv1.ListenerTLSConfig{
|
||||
Mode: pointer.To(gatewayv1.TLSModeType("Passthrough")),
|
||||
},
|
||||
AllowedRoutes: &gatewayv1.AllowedRoutes{
|
||||
Namespaces: &gatewayv1.RouteNamespaces{
|
||||
From: pointer.To(gatewayv1.NamespacesFromAll),
|
||||
},
|
||||
Kinds: []gatewayv1.RouteGroupKind{
|
||||
{
|
||||
Group: pointer.To(gatewayv1.Group("gateway.networking.k8s.io")),
|
||||
Kind: "TLSRoute",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "konnectivity-server",
|
||||
Port: 8132,
|
||||
Protocol: gatewayv1.TLSProtocolType,
|
||||
Hostname: pointer.To(gatewayv1.Hostname(hostname)),
|
||||
TLS: &gatewayv1.ListenerTLSConfig{
|
||||
Mode: pointer.To(gatewayv1.TLSModeType("Passthrough")),
|
||||
},
|
||||
AllowedRoutes: &gatewayv1.AllowedRoutes{
|
||||
Namespaces: &gatewayv1.RouteNamespaces{
|
||||
From: pointer.To(gatewayv1.NamespacesFromAll),
|
||||
},
|
||||
Kinds: []gatewayv1.RouteGroupKind{
|
||||
{
|
||||
Group: pointer.To(gatewayv1.Group("gateway.networking.k8s.io")),
|
||||
Kind: "TLSRoute",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(context.Background(), gateway)).NotTo(HaveOccurred())
|
||||
}
|
||||
|
||||
32
go.mod
32
go.mod
@@ -7,6 +7,7 @@ require (
|
||||
github.com/blang/semver v3.5.1+incompatible
|
||||
github.com/clastix/kamaji-telemetry v1.0.0
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/evanphx/json-patch/v5 v5.9.11
|
||||
github.com/go-logr/logr v1.4.3
|
||||
github.com/go-pg/pg/v10 v10.15.0
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
@@ -15,10 +16,10 @@ require (
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/juju/mutex/v2 v2.0.0
|
||||
github.com/nats-io/nats.go v1.48.0
|
||||
github.com/onsi/ginkgo/v2 v2.27.3
|
||||
github.com/onsi/gomega v1.38.3
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/onsi/ginkgo/v2 v2.28.1
|
||||
github.com/onsi/gomega v1.39.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/prometheus/client_model v0.6.2
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/spf13/viper v1.21.0
|
||||
@@ -28,8 +29,8 @@ require (
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0
|
||||
k8s.io/api v0.35.0
|
||||
k8s.io/apiextensions-apiserver v0.34.1
|
||||
k8s.io/apimachinery v0.35.0
|
||||
k8s.io/apiserver v0.35.0
|
||||
k8s.io/client-go v0.35.0
|
||||
k8s.io/cluster-bootstrap v0.0.0
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
@@ -70,7 +71,6 @@ require (
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
@@ -90,7 +90,7 @@ require (
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/cel-go v0.26.0 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||
@@ -123,9 +123,9 @@ require (
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
@@ -163,17 +163,17 @@ require (
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/term v0.39.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect
|
||||
google.golang.org/grpc v1.75.1 // indirect
|
||||
@@ -183,7 +183,7 @@ require (
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.34.1 // indirect
|
||||
k8s.io/apiserver v0.35.0 // indirect
|
||||
k8s.io/cli-runtime v0.0.0 // indirect
|
||||
k8s.io/cloud-provider v0.0.0 // indirect
|
||||
k8s.io/component-base v0.35.0 // indirect
|
||||
|
||||
44
go.sum
44
go.sum
@@ -138,8 +138,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
|
||||
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@@ -253,10 +253,10 @@ github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
|
||||
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8=
|
||||
github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
|
||||
github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
|
||||
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
|
||||
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
|
||||
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
||||
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
@@ -420,30 +420,30 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -457,15 +457,15 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -473,8 +473,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -1022,7 +1021,7 @@ func (d Deployment) templateLabels(ctx context.Context, tenantControlPlane *kama
|
||||
func (d Deployment) secretHashValue(ctx context.Context, client client.Client, namespace, name string) (string, error) {
|
||||
secret := &corev1.Secret{}
|
||||
if err := client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, secret); err != nil {
|
||||
return "", errors.Wrap(err, "cannot retrieve *corev1.Secret for resource version retrieval")
|
||||
return "", fmt.Errorf("cannot retrieve *corev1.Secret for resource version retrieval: %w", err)
|
||||
}
|
||||
|
||||
return d.hashValue(*secret), nil
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
@@ -93,7 +92,7 @@ func GenerateCertificatePrivateKeyPair(template *x509.Certificate, caCertificate
|
||||
|
||||
caPrivKeyBytes, err := ParsePrivateKeyBytes(caPrivateKey)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "provided CA private key for certificate generation cannot be parsed")
|
||||
return nil, nil, fmt.Errorf("provided CA private key for certificate generation cannot be parsed: %w", err)
|
||||
}
|
||||
|
||||
return generateCertificateKeyPairBytes(template, caCertBytes, caPrivKeyBytes)
|
||||
@@ -108,7 +107,7 @@ func ParseCertificateBytes(content []byte) (*x509.Certificate, error) {
|
||||
|
||||
crt, err := x509.ParseCertificate(pemContent.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot parse x509 Certificate")
|
||||
return nil, fmt.Errorf("cannot parse x509 Certificate: %w", err)
|
||||
}
|
||||
|
||||
return crt, nil
|
||||
@@ -124,7 +123,7 @@ func ParsePrivateKeyBytes(content []byte) (crypto.Signer, error) {
|
||||
if pemContent.Type == "EC PRIVATE KEY" {
|
||||
privateKey, err := x509.ParseECPrivateKey(pemContent.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot parse EC Private Key")
|
||||
return nil, fmt.Errorf("cannot parse EC Private Key: %w", err)
|
||||
}
|
||||
|
||||
return privateKey, nil
|
||||
@@ -132,7 +131,7 @@ func ParsePrivateKeyBytes(content []byte) (crypto.Signer, error) {
|
||||
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(pemContent.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot parse PKCS1 Private Key")
|
||||
return nil, fmt.Errorf("cannot parse PKCS1 Private Key: %w", err)
|
||||
}
|
||||
|
||||
return privateKey, nil
|
||||
@@ -209,12 +208,12 @@ func VerifyCertificate(cert, ca []byte, usages ...x509.ExtKeyUsage) (bool, error
|
||||
func generateCertificateKeyPairBytes(template *x509.Certificate, caCert *x509.Certificate, caKey crypto.Signer) (*bytes.Buffer, *bytes.Buffer, error) {
|
||||
certPrivKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "cannot generate an RSA key")
|
||||
return nil, nil, fmt.Errorf("cannot generate an RSA key: %w", err)
|
||||
}
|
||||
|
||||
certBytes, err := x509.CreateCertificate(cryptorand.Reader, template, caCert, &certPrivKey.PublicKey, caKey)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "cannot create the certificate")
|
||||
return nil, nil, fmt.Errorf("cannot create the certificate: %w", err)
|
||||
}
|
||||
|
||||
certPEM := &bytes.Buffer{}
|
||||
@@ -223,7 +222,7 @@ func generateCertificateKeyPairBytes(template *x509.Certificate, caCert *x509.Ce
|
||||
Headers: nil,
|
||||
Bytes: certBytes,
|
||||
}); err != nil {
|
||||
return nil, nil, errors.Wrap(err, "cannot encode the generate certificate bytes")
|
||||
return nil, nil, fmt.Errorf("cannot encode the generate certificate bytes: %w", err)
|
||||
}
|
||||
|
||||
certPrivKeyPEM := &bytes.Buffer{}
|
||||
@@ -232,7 +231,7 @@ func generateCertificateKeyPairBytes(template *x509.Certificate, caCert *x509.Ce
|
||||
Headers: nil,
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
|
||||
}); err != nil {
|
||||
return nil, nil, errors.Wrap(err, "cannot encode private key")
|
||||
return nil, nil, fmt.Errorf("cannot encode private key: %w", err)
|
||||
}
|
||||
|
||||
return certPEM, certPrivKeyPEM, nil
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
@@ -16,7 +15,7 @@ import (
|
||||
func NewStorageConnection(ctx context.Context, client client.Client, ds kamajiv1alpha1.DataStore) (Connection, error) {
|
||||
cc, err := NewConnectionConfig(ctx, client, ds)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to create connection config object")
|
||||
return nil, fmt.Errorf("unable to create connection config object: %w", err)
|
||||
}
|
||||
|
||||
switch ds.Spec.Driver {
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
@@ -67,7 +66,7 @@ func NewConnectionConfig(ctx context.Context, client client.Client, ds kamajiv1a
|
||||
|
||||
certificate, err := tls.X509KeyPair(crt, key)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot retrieve x.509 key pair from the Kine Secret")
|
||||
return nil, fmt.Errorf("cannot retrieve x.509 key pair from the Kine Secret: %w", err)
|
||||
}
|
||||
|
||||
tlsConfig.Certificates = []tls.Certificate{certificate}
|
||||
@@ -93,12 +92,12 @@ func NewConnectionConfig(ctx context.Context, client client.Client, ds kamajiv1a
|
||||
for _, ep := range ds.Spec.Endpoints {
|
||||
host, stringPort, err := net.SplitHostPort(ep)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot retrieve host-port pair from DataStore endpoints")
|
||||
return nil, fmt.Errorf("cannot retrieve host-port pair from DataStore endpoints: %w", err)
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(stringPort)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot convert port from string for the given DataStore")
|
||||
return nil, fmt.Errorf("cannot convert port from string for the given DataStore: %w", err)
|
||||
}
|
||||
|
||||
eps = append(eps, ConnectionEndpoint{
|
||||
|
||||
@@ -3,48 +3,48 @@
|
||||
|
||||
package errors
|
||||
|
||||
import "github.com/pkg/errors"
|
||||
import "fmt"
|
||||
|
||||
func NewCreateUserError(err error) error {
|
||||
return errors.Wrap(err, "cannot create user")
|
||||
return fmt.Errorf("cannot create user: %w", err)
|
||||
}
|
||||
|
||||
func NewGrantPrivilegesError(err error) error {
|
||||
return errors.Wrap(err, "cannot grant privileges")
|
||||
return fmt.Errorf("cannot grant privileges: %w", err)
|
||||
}
|
||||
|
||||
func NewCheckUserExistsError(err error) error {
|
||||
return errors.Wrap(err, "cannot check if user exists")
|
||||
return fmt.Errorf("cannot check if user exists: %w", err)
|
||||
}
|
||||
|
||||
func NewCheckGrantExistsError(err error) error {
|
||||
return errors.Wrap(err, "cannot check if grant exists")
|
||||
return fmt.Errorf("cannot check if grant exists: %w", err)
|
||||
}
|
||||
|
||||
func NewDeleteUserError(err error) error {
|
||||
return errors.Wrap(err, "cannot delete user")
|
||||
return fmt.Errorf("cannot delete user: %w", err)
|
||||
}
|
||||
|
||||
func NewCannotDeleteDatabaseError(err error) error {
|
||||
return errors.Wrap(err, "cannot delete database")
|
||||
return fmt.Errorf("cannot delete database: %w", err)
|
||||
}
|
||||
|
||||
func NewCheckDatabaseExistError(err error) error {
|
||||
return errors.Wrap(err, "cannot check if database exists")
|
||||
return fmt.Errorf("cannot check if database exists: %w", err)
|
||||
}
|
||||
|
||||
func NewRevokePrivilegesError(err error) error {
|
||||
return errors.Wrap(err, "cannot revoke privileges")
|
||||
return fmt.Errorf("cannot revoke privileges: %w", err)
|
||||
}
|
||||
|
||||
func NewCloseConnectionError(err error) error {
|
||||
return errors.Wrap(err, "cannot close connection")
|
||||
return fmt.Errorf("cannot close connection: %w", err)
|
||||
}
|
||||
|
||||
func NewCheckConnectionError(err error) error {
|
||||
return errors.Wrap(err, "cannot check connection")
|
||||
return fmt.Errorf("cannot check connection: %w", err)
|
||||
}
|
||||
|
||||
func NewCreateDBError(err error) error {
|
||||
return errors.Wrap(err, "cannot create database")
|
||||
return fmt.Errorf("cannot create database: %w", err)
|
||||
}
|
||||
|
||||
@@ -7,13 +7,12 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
goerrors "github.com/pkg/errors"
|
||||
"go.etcd.io/etcd/api/v3/authpb"
|
||||
"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
|
||||
etcdclient "go.etcd.io/etcd/client/v3"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
"github.com/clastix/kamaji/internal/datastore/errors"
|
||||
dserrors "github.com/clastix/kamaji/internal/datastore/errors"
|
||||
)
|
||||
|
||||
func NewETCDConnection(config ConnectionConfig) (Connection, error) {
|
||||
@@ -44,7 +43,7 @@ type EtcdClient struct {
|
||||
|
||||
func (e *EtcdClient) CreateUser(ctx context.Context, user, password string) error {
|
||||
if _, err := e.Client.Auth.UserAddWithOptions(ctx, user, password, &etcdclient.UserAddOptions{NoPassword: true}); err != nil {
|
||||
return errors.NewCreateUserError(err)
|
||||
return dserrors.NewCreateUserError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -56,18 +55,18 @@ func (e *EtcdClient) CreateDB(context.Context, string) error {
|
||||
|
||||
func (e *EtcdClient) GrantPrivileges(ctx context.Context, user, dbName string) error {
|
||||
if _, err := e.Client.Auth.RoleAdd(ctx, dbName); err != nil {
|
||||
return errors.NewGrantPrivilegesError(err)
|
||||
return dserrors.NewGrantPrivilegesError(err)
|
||||
}
|
||||
|
||||
permission := etcdclient.PermissionType(authpb.READWRITE)
|
||||
key := e.buildKey(dbName)
|
||||
|
||||
if _, err := e.Client.RoleGrantPermission(ctx, dbName, key, etcdclient.GetPrefixRangeEnd(key), permission); err != nil {
|
||||
return errors.NewGrantPrivilegesError(err)
|
||||
return dserrors.NewGrantPrivilegesError(err)
|
||||
}
|
||||
|
||||
if _, err := e.Client.UserGrantRole(ctx, user, dbName); err != nil {
|
||||
return errors.NewGrantPrivilegesError(err)
|
||||
return dserrors.NewGrantPrivilegesError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -75,11 +74,15 @@ func (e *EtcdClient) GrantPrivileges(ctx context.Context, user, dbName string) e
|
||||
|
||||
func (e *EtcdClient) UserExists(ctx context.Context, user string) (bool, error) {
|
||||
if _, err := e.Client.UserGet(ctx, user); err != nil {
|
||||
if goerrors.As(err, &rpctypes.ErrGRPCUserNotFound) {
|
||||
// Convert gRPC error to comparable EtcdError using rpctypes.Error(),
|
||||
// then compare against the client-side error constant.
|
||||
// The == comparison is correct here as rpctypes.Error() normalizes
|
||||
// gRPC status errors to comparable EtcdError struct values.
|
||||
if rpctypes.Error(err) == rpctypes.ErrUserNotFound { //nolint:errorlint
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, errors.NewCheckUserExistsError(err)
|
||||
return false, dserrors.NewCheckUserExistsError(err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
@@ -92,16 +95,20 @@ func (e *EtcdClient) DBExists(context.Context, string) (bool, error) {
|
||||
func (e *EtcdClient) GrantPrivilegesExists(ctx context.Context, username, dbName string) (bool, error) {
|
||||
_, err := e.Client.RoleGet(ctx, dbName)
|
||||
if err != nil {
|
||||
if goerrors.As(err, &rpctypes.ErrGRPCRoleNotFound) {
|
||||
// Convert gRPC error to comparable EtcdError using rpctypes.Error(),
|
||||
// then compare against the client-side error constant.
|
||||
// The == comparison is correct here as rpctypes.Error() normalizes
|
||||
// gRPC status errors to comparable EtcdError struct values.
|
||||
if rpctypes.Error(err) == rpctypes.ErrRoleNotFound { //nolint:errorlint
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, errors.NewCheckGrantExistsError(err)
|
||||
return false, dserrors.NewCheckGrantExistsError(err)
|
||||
}
|
||||
|
||||
user, err := e.Client.UserGet(ctx, username)
|
||||
if err != nil {
|
||||
return false, errors.NewCheckGrantExistsError(err)
|
||||
return false, dserrors.NewCheckGrantExistsError(err)
|
||||
}
|
||||
|
||||
for _, i := range user.Roles {
|
||||
@@ -115,7 +122,7 @@ func (e *EtcdClient) GrantPrivilegesExists(ctx context.Context, username, dbName
|
||||
|
||||
func (e *EtcdClient) DeleteUser(ctx context.Context, user string) error {
|
||||
if _, err := e.Client.Auth.UserDelete(ctx, user); err != nil {
|
||||
return errors.NewDeleteUserError(err)
|
||||
return dserrors.NewDeleteUserError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -124,7 +131,7 @@ func (e *EtcdClient) DeleteUser(ctx context.Context, user string) error {
|
||||
func (e *EtcdClient) DeleteDB(ctx context.Context, dbName string) error {
|
||||
prefix := e.buildKey(dbName)
|
||||
if _, err := e.Client.Delete(ctx, prefix, etcdclient.WithPrefix()); err != nil {
|
||||
return errors.NewCannotDeleteDatabaseError(err)
|
||||
return dserrors.NewCannotDeleteDatabaseError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -132,7 +139,7 @@ func (e *EtcdClient) DeleteDB(ctx context.Context, dbName string) error {
|
||||
|
||||
func (e *EtcdClient) RevokePrivileges(ctx context.Context, _, dbName string) error {
|
||||
if _, err := e.Client.Auth.RoleDelete(ctx, dbName); err != nil {
|
||||
return errors.NewRevokePrivilegesError(err)
|
||||
return dserrors.NewRevokePrivilegesError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -146,7 +153,7 @@ func (e *EtcdClient) GetConnectionString() string {
|
||||
|
||||
func (e *EtcdClient) Close() error {
|
||||
if err := e.Client.Close(); err != nil {
|
||||
return errors.NewCloseConnectionError(err)
|
||||
return dserrors.NewCloseConnectionError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -154,7 +161,7 @@ func (e *EtcdClient) Close() error {
|
||||
|
||||
func (e *EtcdClient) Check(ctx context.Context) error {
|
||||
if _, err := e.Client.AuthStatus(ctx); err != nil {
|
||||
return errors.NewCheckConnectionError(err)
|
||||
return dserrors.NewCheckConnectionError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -5,11 +5,11 @@ package datastore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
)
|
||||
@@ -73,7 +73,7 @@ func (nc *NATSConnection) CreateUser(_ context.Context, _, _ string) error {
|
||||
func (nc *NATSConnection) CreateDB(_ context.Context, dbName string) error {
|
||||
_, err := nc.js.CreateKeyValue(&nats.KeyValueConfig{Bucket: dbName})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to create the datastore")
|
||||
return fmt.Errorf("unable to create the datastore: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-pg/pg/v10"
|
||||
goerrors "github.com/pkg/errors"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
"github.com/clastix/kamaji/internal/datastore/errors"
|
||||
@@ -18,18 +17,18 @@ import (
|
||||
|
||||
const (
|
||||
postgresqlFetchDBStatement = "SELECT FROM pg_database WHERE datname = ?"
|
||||
postgresqlCreateDBStatement = "CREATE DATABASE %s"
|
||||
postgresqlCreateDBStatement = `CREATE DATABASE "%s"`
|
||||
postgresqlUserExists = "SELECT 1 FROM pg_roles WHERE rolname = ?"
|
||||
postgresqlCreateUserStatement = "CREATE ROLE %s LOGIN PASSWORD ?"
|
||||
postgresqlCreateUserStatement = `CREATE ROLE "%s" LOGIN PASSWORD ?`
|
||||
postgresqlShowGrantsStatement = "SELECT has_database_privilege(rolname, ?, 'create') from pg_roles where rolcanlogin and rolname = ?"
|
||||
postgresqlShowOwnershipStatement = "SELECT 't' FROM pg_catalog.pg_database AS d WHERE d.datname = ? AND pg_catalog.pg_get_userbyid(d.datdba) = ?"
|
||||
postgresqlShowTableOwnershipStatement = "SELECT 't' from pg_tables where tableowner = ? AND tablename = ?"
|
||||
postgresqlKineTableExistsStatement = "SELECT 't' FROM pg_tables WHERE schemaname = ? AND tablename = ?"
|
||||
postgresqlGrantPrivilegesStatement = "GRANT CONNECT, CREATE ON DATABASE %s TO %s"
|
||||
postgresqlChangeOwnerStatement = "ALTER DATABASE %s OWNER TO %s"
|
||||
postgresqlRevokePrivilegesStatement = "REVOKE ALL PRIVILEGES ON DATABASE %s FROM %s"
|
||||
postgresqlDropRoleStatement = "DROP ROLE %s"
|
||||
postgresqlDropDBStatement = "DROP DATABASE %s WITH (FORCE)"
|
||||
postgresqlGrantPrivilegesStatement = `GRANT CONNECT, CREATE ON DATABASE "%s" TO "%s"`
|
||||
postgresqlChangeOwnerStatement = `ALTER DATABASE "%s" OWNER TO "%s"`
|
||||
postgresqlRevokePrivilegesStatement = `REVOKE ALL PRIVILEGES ON DATABASE "%s" FROM "%s"`
|
||||
postgresqlDropRoleStatement = `DROP ROLE "%s"`
|
||||
postgresqlDropDBStatement = `DROP DATABASE "%s" WITH (FORCE)`
|
||||
)
|
||||
|
||||
type PostgreSQLConnection struct {
|
||||
@@ -236,7 +235,7 @@ func (r *PostgreSQLConnection) DeleteUser(ctx context.Context, user string) erro
|
||||
|
||||
func (r *PostgreSQLConnection) DeleteDB(ctx context.Context, dbName string) error {
|
||||
if err := r.GrantPrivileges(ctx, r.rootUser, dbName); err != nil {
|
||||
return errors.NewCannotDeleteDatabaseError(goerrors.Wrap(err, "cannot grant privileges to root user"))
|
||||
return errors.NewCannotDeleteDatabaseError(fmt.Errorf("cannot grant privileges to root user: %w", err))
|
||||
}
|
||||
|
||||
if _, err := r.db.ExecContext(ctx, fmt.Sprintf(postgresqlDropDBStatement, dbName)); err != nil {
|
||||
|
||||
@@ -3,15 +3,21 @@
|
||||
|
||||
package errors
|
||||
|
||||
import "github.com/pkg/errors"
|
||||
import "errors"
|
||||
|
||||
func ShouldReconcileErrorBeIgnored(err error) bool {
|
||||
var (
|
||||
nonExposedLBErr NonExposedLoadBalancerError
|
||||
missingValidIPErr MissingValidIPError
|
||||
migrationErr MigrationInProcessError
|
||||
)
|
||||
|
||||
switch {
|
||||
case errors.As(err, &NonExposedLoadBalancerError{}):
|
||||
case errors.As(err, &nonExposedLBErr):
|
||||
return true
|
||||
case errors.As(err, &MissingValidIPError{}):
|
||||
case errors.As(err, &missingValidIPErr):
|
||||
return true
|
||||
case errors.As(err, &MigrationInProcessError{}):
|
||||
case errors.As(err, &migrationErr):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
package kubeadm
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"fmt"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@@ -20,19 +21,19 @@ 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")
|
||||
return fmt.Errorf("error updating or creating token: %w", err)
|
||||
}
|
||||
|
||||
if err := node.AllowBootstrapTokensToGetNodes(client); err != nil {
|
||||
return errors.Wrap(err, "error allowing bootstrap tokens to get Nodes")
|
||||
return fmt.Errorf("error allowing bootstrap tokens to get Nodes: %w", err)
|
||||
}
|
||||
|
||||
if err := node.AllowBootstrapTokensToPostCSRs(client); err != nil {
|
||||
return errors.Wrap(err, "error allowing bootstrap tokens to post CSRs")
|
||||
return fmt.Errorf("error allowing bootstrap tokens to post CSRs: %w", err)
|
||||
}
|
||||
|
||||
if err := node.AutoApproveNodeBootstrapTokens(client); err != nil {
|
||||
return errors.Wrap(err, "error auto-approving node bootstrap tokens")
|
||||
return fmt.Errorf("error auto-approving node bootstrap tokens: %w", err)
|
||||
}
|
||||
|
||||
if err := node.AutoApproveNodeCertificateRotation(client); err != nil {
|
||||
@@ -66,7 +67,7 @@ func BootstrapToken(client kubernetes.Interface, config *Configuration) error {
|
||||
}
|
||||
|
||||
if err := clusterinfo.CreateClusterInfoRBACRules(client); err != nil {
|
||||
return errors.Wrap(err, "error creating clusterinfo RBAC rules")
|
||||
return fmt.Errorf("error creating clusterinfo RBAC rules: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/pkg/errors"
|
||||
jsonpatchv5 "github.com/evanphx/json-patch/v5"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -34,13 +34,14 @@ func UploadKubeadmConfig(client kubernetes.Interface, config *Configuration) ([]
|
||||
return nil, uploadconfig.UploadConfiguration(&config.InitConfiguration, client)
|
||||
}
|
||||
|
||||
func UploadKubeletConfig(client kubernetes.Interface, config *Configuration) ([]byte, error) {
|
||||
func UploadKubeletConfig(client kubernetes.Interface, config *Configuration, patches jsonpatchv5.Patch) ([]byte, error) {
|
||||
kubeletConfiguration := KubeletConfiguration{
|
||||
TenantControlPlaneDomain: config.InitConfiguration.Networking.DNSDomain,
|
||||
TenantControlPlaneDNSServiceIPs: config.Parameters.TenantDNSServiceIPs,
|
||||
TenantControlPlaneCgroupDriver: config.Parameters.TenantControlPlaneCGroupDriver,
|
||||
}
|
||||
content, err := getKubeletConfigmapContent(kubeletConfiguration)
|
||||
|
||||
content, err := getKubeletConfigmapContent(kubeletConfiguration, patches)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -65,13 +66,13 @@ func UploadKubeletConfig(client kubernetes.Interface, config *Configuration) ([]
|
||||
}
|
||||
|
||||
if err = createConfigMapRBACRules(client, configMapName); err != nil {
|
||||
return nil, errors.Wrap(err, "error creating kubelet configuration configmap RBAC rules")
|
||||
return nil, fmt.Errorf("error creating kubelet configuration configmap RBAC rules: %w", err)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func getKubeletConfigmapContent(kubeletConfiguration KubeletConfiguration) ([]byte, error) {
|
||||
func getKubeletConfigmapContent(kubeletConfiguration KubeletConfiguration, patch jsonpatchv5.Patch) ([]byte, error) {
|
||||
var kc kubelettypes.KubeletConfiguration
|
||||
|
||||
kubeletv1beta1.SetDefaults_KubeletConfiguration(&kc)
|
||||
@@ -93,6 +94,21 @@ func getKubeletConfigmapContent(kubeletConfiguration KubeletConfiguration) ([]by
|
||||
// determine the resolvConf location, as reported in clastix/kamaji#581.
|
||||
kc.ResolverConfig = nil
|
||||
|
||||
if len(patch) > 0 {
|
||||
kubeletConfig, patchErr := utilities.EncodeToJSON(&kc)
|
||||
if patchErr != nil {
|
||||
return nil, fmt.Errorf("unable to encode KubeletConfiguration to JSON for JSON patching: %w", patchErr)
|
||||
}
|
||||
|
||||
if kubeletConfig, patchErr = patch.Apply(kubeletConfig); patchErr != nil {
|
||||
return nil, fmt.Errorf("unable to apply JSON patching to KubeletConfiguration: %w", patchErr)
|
||||
}
|
||||
|
||||
if patchErr = utilities.DecodeFromJSON(string(kubeletConfig), &kc); patchErr != nil {
|
||||
return nil, fmt.Errorf("unable to decode JSON to KubeletConfiguration: %w", patchErr)
|
||||
}
|
||||
}
|
||||
|
||||
return utilities.EncodeToYaml(&kc)
|
||||
}
|
||||
|
||||
@@ -142,7 +158,7 @@ func createConfigMapRBACRules(client kubernetes.Interface, configMapName string)
|
||||
func generateKubeletConfigMapName(version string) (string, error) {
|
||||
parsedVersion, err := semver.ParseTolerant(version)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to parse kubernetes version %q", version)
|
||||
return "", fmt.Errorf("failed to parse kubernetes version %q: %w", version, err)
|
||||
}
|
||||
|
||||
majorMinor := semver.Version{Major: parsedVersion.Major, Minor: parsedVersion.Minor}
|
||||
|
||||
@@ -6,8 +6,8 @@ package addons
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@@ -219,7 +219,7 @@ func (c *CoreDNS) UpdateTenantControlPlaneStatus(_ context.Context, tcp *kamajiv
|
||||
func (c *CoreDNS) decodeManifests(ctx context.Context, tcp *kamajiv1alpha1.TenantControlPlane) error {
|
||||
tcpClient, config, err := resources.GetKubeadmManifestDeps(ctx, c.Client, tcp)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to create manifests dependencies")
|
||||
return fmt.Errorf("unable to create manifests dependencies: %w", err)
|
||||
}
|
||||
|
||||
// If CoreDNS addon is enabled and with an override, adding these to the kubeadm init configuration
|
||||
@@ -235,38 +235,38 @@ func (c *CoreDNS) decodeManifests(ctx context.Context, tcp *kamajiv1alpha1.Tenan
|
||||
|
||||
manifests, err := kubeadm.AddCoreDNS(tcpClient, config)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to generate manifests")
|
||||
return fmt.Errorf("unable to generate manifests: %w", err)
|
||||
}
|
||||
|
||||
parts := bytes.Split(manifests, []byte("---"))
|
||||
|
||||
if err = utilities.DecodeFromYAML(string(parts[1]), c.deployment); err != nil {
|
||||
return errors.Wrap(err, "unable to decode Deployment manifest")
|
||||
return fmt.Errorf("unable to decode Deployment manifest: %w", err)
|
||||
}
|
||||
addons_utils.SetKamajiManagedLabels(c.deployment)
|
||||
|
||||
if err = utilities.DecodeFromYAML(string(parts[2]), c.configMap); err != nil {
|
||||
return errors.Wrap(err, "unable to decode ConfigMap manifest")
|
||||
return fmt.Errorf("unable to decode ConfigMap manifest: %w", err)
|
||||
}
|
||||
addons_utils.SetKamajiManagedLabels(c.configMap)
|
||||
|
||||
if err = utilities.DecodeFromYAML(string(parts[3]), c.service); err != nil {
|
||||
return errors.Wrap(err, "unable to decode Service manifest")
|
||||
return fmt.Errorf("unable to decode Service manifest: %w", err)
|
||||
}
|
||||
addons_utils.SetKamajiManagedLabels(c.service)
|
||||
|
||||
if err = utilities.DecodeFromYAML(string(parts[4]), c.clusterRole); err != nil {
|
||||
return errors.Wrap(err, "unable to decode ClusterRole manifest")
|
||||
return fmt.Errorf("unable to decode ClusterRole manifest: %w", err)
|
||||
}
|
||||
addons_utils.SetKamajiManagedLabels(c.clusterRole)
|
||||
|
||||
if err = utilities.DecodeFromYAML(string(parts[5]), c.clusterRoleBinding); err != nil {
|
||||
return errors.Wrap(err, "unable to decode ClusterRoleBinding manifest")
|
||||
return fmt.Errorf("unable to decode ClusterRoleBinding manifest: %w", err)
|
||||
}
|
||||
addons_utils.SetKamajiManagedLabels(c.clusterRoleBinding)
|
||||
|
||||
if err = utilities.DecodeFromYAML(string(parts[6]), c.serviceAccount); err != nil {
|
||||
return errors.Wrap(err, "unable to decode ServiceAccount manifest")
|
||||
return fmt.Errorf("unable to decode ServiceAccount manifest: %w", err)
|
||||
}
|
||||
addons_utils.SetKamajiManagedLabels(c.serviceAccount)
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ package addons
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@@ -321,7 +321,7 @@ func (k *KubeProxy) mutateDaemonSet(ctx context.Context, tenantClient client.Cli
|
||||
func (k *KubeProxy) decodeManifests(ctx context.Context, tcp *kamajiv1alpha1.TenantControlPlane) error {
|
||||
tcpClient, config, err := resources.GetKubeadmManifestDeps(ctx, k.Client, tcp)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to create manifests dependencies")
|
||||
return fmt.Errorf("unable to create manifests dependencies: %w", err)
|
||||
}
|
||||
// If the kube-proxy addon has overrides, adding it to the kubeadm parameters
|
||||
config.Parameters.KubeProxyOptions = &kubeadm.AddonOptions{}
|
||||
@@ -340,38 +340,38 @@ func (k *KubeProxy) decodeManifests(ctx context.Context, tcp *kamajiv1alpha1.Ten
|
||||
|
||||
manifests, err := kubeadm.AddKubeProxy(tcpClient, config)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to generate manifests")
|
||||
return fmt.Errorf("unable to generate manifests: %w", err)
|
||||
}
|
||||
|
||||
parts := bytes.Split(manifests, []byte("---"))
|
||||
|
||||
if err = utilities.DecodeFromYAML(string(parts[1]), k.serviceAccount); err != nil {
|
||||
return errors.Wrap(err, "unable to decode ServiceAccount manifest")
|
||||
return fmt.Errorf("unable to decode ServiceAccount manifest: %w", err)
|
||||
}
|
||||
addon_utils.SetKamajiManagedLabels(k.serviceAccount)
|
||||
|
||||
if err = utilities.DecodeFromYAML(string(parts[2]), k.clusterRoleBinding); err != nil {
|
||||
return errors.Wrap(err, "unable to decode ClusterRoleBinding manifest")
|
||||
return fmt.Errorf("unable to decode ClusterRoleBinding manifest: %w", err)
|
||||
}
|
||||
addon_utils.SetKamajiManagedLabels(k.clusterRoleBinding)
|
||||
|
||||
if err = utilities.DecodeFromYAML(string(parts[3]), k.role); err != nil {
|
||||
return errors.Wrap(err, "unable to decode Role manifest")
|
||||
return fmt.Errorf("unable to decode Role manifest: %w", err)
|
||||
}
|
||||
addon_utils.SetKamajiManagedLabels(k.role)
|
||||
|
||||
if err = utilities.DecodeFromYAML(string(parts[4]), k.roleBinding); err != nil {
|
||||
return errors.Wrap(err, "unable to decode RoleBinding manifest")
|
||||
return fmt.Errorf("unable to decode RoleBinding manifest: %w", err)
|
||||
}
|
||||
addon_utils.SetKamajiManagedLabels(k.roleBinding)
|
||||
|
||||
if err = utilities.DecodeFromYAML(string(parts[5]), k.configMap); err != nil {
|
||||
return errors.Wrap(err, "unable to decode ConfigMap manifest")
|
||||
return fmt.Errorf("unable to decode ConfigMap manifest: %w", err)
|
||||
}
|
||||
addon_utils.SetKamajiManagedLabels(k.configMap)
|
||||
|
||||
if err = utilities.DecodeFromYAML(string(parts[6]), k.daemonSet); err != nil {
|
||||
return errors.Wrap(err, "unable to decode DaemonSet manifest")
|
||||
return fmt.Errorf("unable to decode DaemonSet manifest: %w", err)
|
||||
}
|
||||
addon_utils.SetKamajiManagedLabels(k.daemonSet)
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ package datastore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
|
||||
@@ -5,8 +5,8 @@ package datastore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -192,7 +192,7 @@ func (r *Setup) UpdateTenantControlPlaneStatus(_ context.Context, tenantControlP
|
||||
func (r *Setup) createDB(ctx context.Context, _ *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
|
||||
exists, err := r.Connection.DBExists(ctx, r.resource.schema)
|
||||
if err != nil {
|
||||
return controllerutil.OperationResultNone, errors.Wrap(err, "unable to check if datastore exists")
|
||||
return controllerutil.OperationResultNone, fmt.Errorf("unable to check if datastore exists: %w", err)
|
||||
}
|
||||
|
||||
if exists {
|
||||
@@ -200,7 +200,7 @@ func (r *Setup) createDB(ctx context.Context, _ *kamajiv1alpha1.TenantControlPla
|
||||
}
|
||||
|
||||
if err := r.Connection.CreateDB(ctx, r.resource.schema); err != nil {
|
||||
return controllerutil.OperationResultNone, errors.Wrap(err, "unable to create the datastore")
|
||||
return controllerutil.OperationResultNone, fmt.Errorf("unable to create the datastore: %w", err)
|
||||
}
|
||||
|
||||
return controllerutil.OperationResultCreated, nil
|
||||
@@ -209,7 +209,7 @@ func (r *Setup) createDB(ctx context.Context, _ *kamajiv1alpha1.TenantControlPla
|
||||
func (r *Setup) deleteDB(ctx context.Context, _ *kamajiv1alpha1.TenantControlPlane) error {
|
||||
exists, err := r.Connection.DBExists(ctx, r.resource.schema)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to check if datastore exists")
|
||||
return fmt.Errorf("unable to check if datastore exists: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
@@ -217,7 +217,7 @@ func (r *Setup) deleteDB(ctx context.Context, _ *kamajiv1alpha1.TenantControlPla
|
||||
}
|
||||
|
||||
if err := r.Connection.DeleteDB(ctx, r.resource.schema); err != nil {
|
||||
return errors.Wrap(err, "unable to delete the datastore")
|
||||
return fmt.Errorf("unable to delete the datastore: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -226,7 +226,7 @@ func (r *Setup) deleteDB(ctx context.Context, _ *kamajiv1alpha1.TenantControlPla
|
||||
func (r *Setup) createUser(ctx context.Context, _ *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
|
||||
exists, err := r.Connection.UserExists(ctx, r.resource.user)
|
||||
if err != nil {
|
||||
return controllerutil.OperationResultNone, errors.Wrap(err, "unable to check if user exists")
|
||||
return controllerutil.OperationResultNone, fmt.Errorf("unable to check if user exists: %w", err)
|
||||
}
|
||||
|
||||
if exists {
|
||||
@@ -234,7 +234,7 @@ func (r *Setup) createUser(ctx context.Context, _ *kamajiv1alpha1.TenantControlP
|
||||
}
|
||||
|
||||
if err := r.Connection.CreateUser(ctx, r.resource.user, r.resource.password); err != nil {
|
||||
return controllerutil.OperationResultNone, errors.Wrap(err, "unable to create the user")
|
||||
return controllerutil.OperationResultNone, fmt.Errorf("unable to create the user: %w", err)
|
||||
}
|
||||
|
||||
return controllerutil.OperationResultCreated, nil
|
||||
@@ -243,7 +243,7 @@ func (r *Setup) createUser(ctx context.Context, _ *kamajiv1alpha1.TenantControlP
|
||||
func (r *Setup) deleteUser(ctx context.Context, _ *kamajiv1alpha1.TenantControlPlane) error {
|
||||
exists, err := r.Connection.UserExists(ctx, r.resource.user)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to check if user exists")
|
||||
return fmt.Errorf("unable to check if user exists: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
@@ -251,7 +251,7 @@ func (r *Setup) deleteUser(ctx context.Context, _ *kamajiv1alpha1.TenantControlP
|
||||
}
|
||||
|
||||
if err := r.Connection.DeleteUser(ctx, r.resource.user); err != nil {
|
||||
return errors.Wrap(err, "unable to remove the user")
|
||||
return fmt.Errorf("unable to remove the user: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -260,7 +260,7 @@ func (r *Setup) deleteUser(ctx context.Context, _ *kamajiv1alpha1.TenantControlP
|
||||
func (r *Setup) createGrantPrivileges(ctx context.Context, _ *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
|
||||
exists, err := r.Connection.GrantPrivilegesExists(ctx, r.resource.user, r.resource.schema)
|
||||
if err != nil {
|
||||
return controllerutil.OperationResultNone, errors.Wrap(err, "unable to check if privileges exist")
|
||||
return controllerutil.OperationResultNone, fmt.Errorf("unable to check if privileges exist: %w", err)
|
||||
}
|
||||
|
||||
if exists {
|
||||
@@ -268,7 +268,7 @@ func (r *Setup) createGrantPrivileges(ctx context.Context, _ *kamajiv1alpha1.Ten
|
||||
}
|
||||
|
||||
if err := r.Connection.GrantPrivileges(ctx, r.resource.user, r.resource.schema); err != nil {
|
||||
return controllerutil.OperationResultNone, errors.Wrap(err, "unable to grant privileges")
|
||||
return controllerutil.OperationResultNone, fmt.Errorf("unable to grant privileges: %w", err)
|
||||
}
|
||||
|
||||
return controllerutil.OperationResultCreated, nil
|
||||
@@ -277,7 +277,7 @@ func (r *Setup) createGrantPrivileges(ctx context.Context, _ *kamajiv1alpha1.Ten
|
||||
func (r *Setup) revokeGrantPrivileges(ctx context.Context, _ *kamajiv1alpha1.TenantControlPlane) error {
|
||||
exists, err := r.Connection.GrantPrivilegesExists(ctx, r.resource.user, r.resource.schema)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to check if privileges exist")
|
||||
return fmt.Errorf("unable to check if privileges exist: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
@@ -285,7 +285,7 @@ func (r *Setup) revokeGrantPrivileges(ctx context.Context, _ *kamajiv1alpha1.Ten
|
||||
}
|
||||
|
||||
if err := r.Connection.RevokePrivileges(ctx, r.resource.user, r.resource.schema); err != nil {
|
||||
return errors.Wrap(err, "unable to revoke privileges")
|
||||
return fmt.Errorf("unable to revoke privileges: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -5,9 +5,9 @@ package datastore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kubeerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -82,7 +82,7 @@ func (r *Config) Delete(ctx context.Context, _ *kamajiv1alpha1.TenantControlPlan
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.Wrap(err, "cannot retrieve the DataStore Secret for removal")
|
||||
return fmt.Errorf("cannot retrieve the DataStore Secret for removal: %w", err)
|
||||
}
|
||||
|
||||
secret.SetFinalizers(nil)
|
||||
@@ -92,7 +92,7 @@ func (r *Config) Delete(ctx context.Context, _ *kamajiv1alpha1.TenantControlPlan
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.Wrap(err, "cannot remove DataStore Secret finalizers")
|
||||
return fmt.Errorf("cannot remove DataStore Secret finalizers: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -138,12 +138,12 @@ func (r *Config) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.
|
||||
// set username and password to the basicAuth values of the NATS datastore
|
||||
u, err := r.DataStore.Spec.BasicAuth.Username.GetContent(ctx, r.Client)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to retrieve the username for the NATS datastore")
|
||||
return fmt.Errorf("failed to retrieve the username for the NATS datastore: %w", err)
|
||||
}
|
||||
|
||||
p, err := r.DataStore.Spec.BasicAuth.Password.GetContent(ctx, r.Client)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to retrieve the password for the NATS datastore")
|
||||
return fmt.Errorf("failed to retrieve the password for the NATS datastore: %w", err)
|
||||
}
|
||||
|
||||
username = u
|
||||
|
||||
@@ -6,14 +6,10 @@ package resources
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
v1 "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"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
@@ -56,66 +52,12 @@ func (r *KubernetesGatewayResource) gatewayStatusNeedsUpdate(tcp *kamajiv1alpha1
|
||||
currentStatus := tcp.Status.Kubernetes.Gateway
|
||||
|
||||
// Check if route reference has changed
|
||||
if currentStatus.RouteRef.Name != r.resource.Name {
|
||||
if currentStatus != nil && currentStatus.RouteRef.Name != r.resource.Name {
|
||||
return true
|
||||
}
|
||||
|
||||
// Compare RouteStatus - check if number of parents changed
|
||||
if len(currentStatus.RouteStatus.Parents) != len(r.resource.Status.RouteStatus.Parents) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Compare individual parent statuses
|
||||
// NOTE: Multiple Parent References are assumed.
|
||||
for i, currentParent := range currentStatus.RouteStatus.Parents {
|
||||
if i >= len(r.resource.Status.RouteStatus.Parents) {
|
||||
return true
|
||||
}
|
||||
|
||||
resourceParent := r.resource.Status.RouteStatus.Parents[i]
|
||||
|
||||
// Compare parent references
|
||||
if currentParent.ParentRef.Name != resourceParent.ParentRef.Name ||
|
||||
(currentParent.ParentRef.Namespace == nil) != (resourceParent.ParentRef.Namespace == nil) ||
|
||||
(currentParent.ParentRef.Namespace != nil && resourceParent.ParentRef.Namespace != nil &&
|
||||
*currentParent.ParentRef.Namespace != *resourceParent.ParentRef.Namespace) ||
|
||||
(currentParent.ParentRef.SectionName == nil) != (resourceParent.ParentRef.SectionName == nil) ||
|
||||
(currentParent.ParentRef.SectionName != nil && resourceParent.ParentRef.SectionName != nil &&
|
||||
*currentParent.ParentRef.SectionName != *resourceParent.ParentRef.SectionName) {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(currentParent.Conditions) != len(resourceParent.Conditions) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Compare each condition
|
||||
for j, currentCondition := range currentParent.Conditions {
|
||||
if j >= len(resourceParent.Conditions) {
|
||||
return true
|
||||
}
|
||||
|
||||
resourceCondition := resourceParent.Conditions[j]
|
||||
|
||||
if currentCondition.Type != resourceCondition.Type ||
|
||||
currentCondition.Status != resourceCondition.Status ||
|
||||
currentCondition.Reason != resourceCondition.Reason ||
|
||||
currentCondition.Message != resourceCondition.Message ||
|
||||
!currentCondition.LastTransitionTime.Equal(&resourceCondition.LastTransitionTime) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Since access points are derived from route status and gateway conditions,
|
||||
// and we've already compared the route status above, we can assume that
|
||||
// if the route status hasn't changed, the access points calculation
|
||||
// will produce the same result. This avoids the need for complex
|
||||
// gateway fetching in the status comparison.
|
||||
//
|
||||
// If there are edge cases where gateway state changes but route status doesn't,
|
||||
// those will be caught in the next reconciliation cycle anyway.
|
||||
return false
|
||||
return IsGatewayRouteStatusChanged(currentStatus, r.resource.Status.RouteStatus)
|
||||
}
|
||||
|
||||
func (r *KubernetesGatewayResource) ShouldCleanup(tcp *kamajiv1alpha1.TenantControlPlane) bool {
|
||||
@@ -125,95 +67,18 @@ func (r *KubernetesGatewayResource) ShouldCleanup(tcp *kamajiv1alpha1.TenantCont
|
||||
func (r *KubernetesGatewayResource) CleanUp(ctx context.Context, tcp *kamajiv1alpha1.TenantControlPlane) (bool, error) {
|
||||
logger := log.FromContext(ctx, "resource", r.GetName())
|
||||
|
||||
route := gatewayv1alpha2.TLSRoute{}
|
||||
if err := r.Client.Get(ctx, client.ObjectKey{
|
||||
Namespace: r.resource.GetNamespace(),
|
||||
Name: r.resource.GetName(),
|
||||
}, &route); err != nil {
|
||||
if !k8serrors.IsNotFound(err) {
|
||||
logger.Error(err, "failed to get TLSRoute before cleanup")
|
||||
cleaned, err := CleanupTLSRoute(ctx, r.Client, r.resource.GetName(), r.resource.GetNamespace(), tcp)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to cleanup tcp route")
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !metav1.IsControlledBy(&route, tcp) {
|
||||
logger.Info("skipping cleanup: route is not managed by Kamaji", "name", route.Name, "namespace", route.Namespace)
|
||||
|
||||
return false, nil
|
||||
if cleaned {
|
||||
logger.V(1).Info("tcp route cleaned up successfully")
|
||||
}
|
||||
|
||||
if err := r.Client.Delete(ctx, &route); err != nil {
|
||||
if !k8serrors.IsNotFound(err) {
|
||||
// TODO: Is that an error? Wanted to delete the resource anyways.
|
||||
logger.Error(err, "cannot cleanup tcp route")
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
logger.V(1).Info("tcp route cleaned up successfully")
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// fetchGatewayByListener uses the indexer to efficiently find a gateway with a specific listener.
|
||||
// This avoids the need to iterate through all listeners in a gateway.
|
||||
func (r *KubernetesGatewayResource) fetchGatewayByListener(ctx context.Context, ref gatewayv1.ParentReference) (*gatewayv1.Gateway, error) {
|
||||
if ref.Namespace == nil {
|
||||
return nil, fmt.Errorf("missing namespace")
|
||||
}
|
||||
if ref.SectionName == nil {
|
||||
return nil, fmt.Errorf("missing sectionName")
|
||||
}
|
||||
|
||||
// Build the composite key that matches our indexer format: namespace/gatewayName/listenerName
|
||||
listenerKey := fmt.Sprintf("%s/%s/%s", *ref.Namespace, ref.Name, *ref.SectionName)
|
||||
|
||||
// Query gateways using the indexer
|
||||
gatewayList := &gatewayv1.GatewayList{}
|
||||
if err := r.Client.List(ctx, gatewayList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(kamajiv1alpha1.GatewayListenerNameKey, listenerKey),
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to list gateways by listener: %w", err)
|
||||
}
|
||||
|
||||
if len(gatewayList.Items) == 0 {
|
||||
return nil, fmt.Errorf("no gateway found with listener '%s'", *ref.SectionName)
|
||||
}
|
||||
|
||||
// Since we're using a composite key with namespace/name/listener, we should get exactly one result
|
||||
if len(gatewayList.Items) > 1 {
|
||||
return nil, fmt.Errorf("found multiple gateways with listener '%s', expected exactly one", *ref.SectionName)
|
||||
}
|
||||
|
||||
return &gatewayList.Items[0], nil
|
||||
}
|
||||
|
||||
func FindMatchingListener(listeners []gatewayv1.Listener, ref gatewayv1.ParentReference) (gatewayv1.Listener, error) {
|
||||
if ref.SectionName == nil {
|
||||
return gatewayv1.Listener{}, fmt.Errorf("missing sectionName")
|
||||
}
|
||||
name := *ref.SectionName
|
||||
for _, listener := range listeners {
|
||||
if listener.Name == name {
|
||||
return listener, nil
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle the cases according to the spec:
|
||||
// - When both Port (experimental) and SectionName are
|
||||
// specified, the name and port of the selected listener
|
||||
// must match both specified values.
|
||||
// - When unspecified (empty string) this will reference
|
||||
// the entire resource [...] an attachment is considered
|
||||
// successful if at least one section in the parent resource accepts it
|
||||
|
||||
return gatewayv1.Listener{}, fmt.Errorf("could not find listener '%s'", name)
|
||||
return cleaned, nil
|
||||
}
|
||||
|
||||
func (r *KubernetesGatewayResource) UpdateTenantControlPlaneStatus(ctx context.Context, tcp *kamajiv1alpha1.TenantControlPlane) error {
|
||||
@@ -251,53 +116,9 @@ func (r *KubernetesGatewayResource) UpdateTenantControlPlaneStatus(ctx context.C
|
||||
}
|
||||
|
||||
logger.V(1).Info("updating TenantControlPlane status for Gateway routes")
|
||||
accessPoints := []kamajiv1alpha1.GatewayAccessPoint{}
|
||||
for _, routeStatus := range routeStatuses.Parents {
|
||||
routeAccepted := meta.IsStatusConditionTrue(
|
||||
routeStatus.Conditions,
|
||||
string(gatewayv1.RouteConditionAccepted),
|
||||
)
|
||||
if !routeAccepted {
|
||||
continue
|
||||
}
|
||||
|
||||
// Use the indexer to efficiently find the gateway with the specific listener
|
||||
gateway, err := r.fetchGatewayByListener(ctx, routeStatus.ParentRef)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not fetch gateway with listener '%v': %w",
|
||||
routeStatus.ParentRef.SectionName, err)
|
||||
}
|
||||
gatewayProgrammed := meta.IsStatusConditionTrue(
|
||||
gateway.Status.Conditions,
|
||||
string(gatewayv1.GatewayConditionProgrammed),
|
||||
)
|
||||
if !gatewayProgrammed {
|
||||
continue
|
||||
}
|
||||
|
||||
// Since we fetched the gateway using the indexer, we know the listener exists
|
||||
// but we still need to get its details from the gateway spec
|
||||
listener, err := FindMatchingListener(
|
||||
gateway.Spec.Listeners, routeStatus.ParentRef,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to match listener: %w", err)
|
||||
}
|
||||
|
||||
for _, hostname := range r.resource.Spec.Hostnames {
|
||||
rawURL := fmt.Sprintf("https://%s:%d", hostname, listener.Port)
|
||||
url, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid url: %w", err)
|
||||
}
|
||||
|
||||
hostnameAddressType := gatewayv1.HostnameAddressType
|
||||
accessPoints = append(accessPoints, kamajiv1alpha1.GatewayAccessPoint{
|
||||
Type: &hostnameAddressType,
|
||||
Value: url.String(),
|
||||
Port: listener.Port,
|
||||
})
|
||||
}
|
||||
accessPoints, err := BuildGatewayAccessPointsStatus(ctx, r.Client, r.resource, routeStatuses)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tcp.Status.Kubernetes.Gateway.AccessPoints = accessPoints
|
||||
|
||||
@@ -329,10 +150,6 @@ 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
|
||||
|
||||
@@ -340,6 +157,11 @@ 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{
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
@@ -100,6 +101,62 @@ var _ = Describe("KubernetesGatewayResource", func() {
|
||||
shouldUpdate := resource.ShouldStatusBeUpdated(ctx, tcp)
|
||||
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{
|
||||
{
|
||||
Name: "test-gateway-1",
|
||||
Namespace: &namespace,
|
||||
},
|
||||
{
|
||||
Name: "test-gateway-2",
|
||||
Namespace: &namespace,
|
||||
},
|
||||
}
|
||||
|
||||
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(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")))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Context("When GatewayRoutes is not configured", func() {
|
||||
@@ -235,4 +292,81 @@ 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())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
241
internal/resources/k8s_gateway_utils.go
Normal file
241
internal/resources/k8s_gateway_utils.go
Normal file
@@ -0,0 +1,241 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
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"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
)
|
||||
|
||||
// fetchGatewayByListener uses the indexer to efficiently find a gateway with a specific listener.
|
||||
// This avoids the need to iterate through all listeners in a gateway.
|
||||
func fetchGatewayByListener(ctx context.Context, c client.Client, ref gatewayv1.ParentReference) (*gatewayv1.Gateway, error) {
|
||||
if ref.SectionName == nil {
|
||||
return nil, fmt.Errorf("missing sectionName")
|
||||
}
|
||||
|
||||
// Build the composite key that matches our indexer format: namespace/gatewayName/listenerName
|
||||
listenerKey := fmt.Sprintf("%s/%s/%s", *ref.Namespace, ref.Name, *ref.SectionName)
|
||||
|
||||
// Query gateways using the indexer
|
||||
gatewayList := &gatewayv1.GatewayList{}
|
||||
if err := c.List(ctx, gatewayList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(kamajiv1alpha1.GatewayListenerNameKey, listenerKey),
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to list gateways by listener: %w", err)
|
||||
}
|
||||
|
||||
if len(gatewayList.Items) == 0 {
|
||||
return nil, fmt.Errorf("no gateway found with listener '%s'", *ref.SectionName)
|
||||
}
|
||||
|
||||
// Since we're using a composite key with namespace/name/listener, we should get exactly one result
|
||||
if len(gatewayList.Items) > 1 {
|
||||
return nil, fmt.Errorf("found multiple gateways with listener '%s', expected exactly one", *ref.SectionName)
|
||||
}
|
||||
|
||||
return &gatewayList.Items[0], nil
|
||||
}
|
||||
|
||||
// FindMatchingListener finds a listener in the given list that matches the parent reference.
|
||||
func FindMatchingListener(listeners []gatewayv1.Listener, ref gatewayv1.ParentReference) (gatewayv1.Listener, error) {
|
||||
if ref.SectionName == nil {
|
||||
return gatewayv1.Listener{}, fmt.Errorf("missing sectionName")
|
||||
}
|
||||
name := *ref.SectionName
|
||||
for _, listener := range listeners {
|
||||
if listener.Name == name {
|
||||
return listener, nil
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle the cases according to the spec:
|
||||
// - When both Port (experimental) and SectionName are
|
||||
// specified, the name and port of the selected listener
|
||||
// must match both specified values.
|
||||
// - When unspecified (empty string) this will reference
|
||||
// the entire resource [...] an attachment is considered
|
||||
// successful if at least one section in the parent resource accepts it
|
||||
|
||||
return gatewayv1.Listener{}, fmt.Errorf("could not find listener '%s'", name)
|
||||
}
|
||||
|
||||
// IsGatewayRouteStatusChanged checks if the gateway route status has changed compared to the stored status.
|
||||
// Returns true if the status has changed (update needed), false if it's the same.
|
||||
func IsGatewayRouteStatusChanged(currentStatus *kamajiv1alpha1.KubernetesGatewayStatus, resourceStatus gatewayv1alpha2.RouteStatus) bool {
|
||||
if currentStatus == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Compare RouteStatus - check if number of parents changed
|
||||
if len(currentStatus.RouteStatus.Parents) != len(resourceStatus.Parents) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Compare individual parent statuses
|
||||
// NOTE: Multiple Parent References are assumed.
|
||||
for i, currentParent := range currentStatus.RouteStatus.Parents {
|
||||
if i >= len(resourceStatus.Parents) {
|
||||
return true
|
||||
}
|
||||
|
||||
resourceParent := resourceStatus.Parents[i]
|
||||
|
||||
// Compare parent references
|
||||
if currentParent.ParentRef.Name != resourceParent.ParentRef.Name ||
|
||||
(currentParent.ParentRef.Namespace == nil) != (resourceParent.ParentRef.Namespace == nil) ||
|
||||
(currentParent.ParentRef.Namespace != nil && resourceParent.ParentRef.Namespace != nil &&
|
||||
*currentParent.ParentRef.Namespace != *resourceParent.ParentRef.Namespace) ||
|
||||
(currentParent.ParentRef.SectionName == nil) != (resourceParent.ParentRef.SectionName == nil) ||
|
||||
(currentParent.ParentRef.SectionName != nil && resourceParent.ParentRef.SectionName != nil &&
|
||||
*currentParent.ParentRef.SectionName != *resourceParent.ParentRef.SectionName) {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(currentParent.Conditions) != len(resourceParent.Conditions) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Compare each condition
|
||||
for j, currentCondition := range currentParent.Conditions {
|
||||
if j >= len(resourceParent.Conditions) {
|
||||
return true
|
||||
}
|
||||
|
||||
resourceCondition := resourceParent.Conditions[j]
|
||||
|
||||
if currentCondition.Type != resourceCondition.Type ||
|
||||
currentCondition.Status != resourceCondition.Status ||
|
||||
currentCondition.Reason != resourceCondition.Reason ||
|
||||
currentCondition.Message != resourceCondition.Message ||
|
||||
!currentCondition.LastTransitionTime.Equal(&resourceCondition.LastTransitionTime) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Since access points are derived from route status and gateway conditions,
|
||||
// and we've already compared the route status above, we can assume that
|
||||
// if the route status hasn't changed, the access points calculation
|
||||
// will produce the same result. This avoids the need for complex
|
||||
// gateway fetching in the status comparison.
|
||||
//
|
||||
// If there are edge cases where gateway state changes but route status doesn't,
|
||||
// those will be caught in the next reconciliation cycle anyway.
|
||||
return false
|
||||
}
|
||||
|
||||
// CleanupTLSRoute cleans up a TLSRoute resource if it's managed by the given TenantControlPlane.
|
||||
func CleanupTLSRoute(ctx context.Context, c client.Client, routeName, routeNamespace string, tcp metav1.Object) (bool, error) {
|
||||
route := gatewayv1alpha2.TLSRoute{}
|
||||
if err := c.Get(ctx, client.ObjectKey{
|
||||
Namespace: routeNamespace,
|
||||
Name: routeName,
|
||||
}, &route); err != nil {
|
||||
if !k8serrors.IsNotFound(err) {
|
||||
return false, fmt.Errorf("failed to get TLSRoute before cleanup: %w", err)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if !metav1.IsControlledBy(&route, tcp) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := c.Delete(ctx, &route); err != nil {
|
||||
if !k8serrors.IsNotFound(err) {
|
||||
return false, fmt.Errorf("cannot delete TLSRoute route: %w", err)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// BuildGatewayAccessPointsStatus builds access points from route statuses.
|
||||
func BuildGatewayAccessPointsStatus(ctx context.Context, c client.Client, route *gatewayv1alpha2.TLSRoute, routeStatuses gatewayv1alpha2.RouteStatus) ([]kamajiv1alpha1.GatewayAccessPoint, error) {
|
||||
accessPoints := []kamajiv1alpha1.GatewayAccessPoint{}
|
||||
routeNamespace := gatewayv1.Namespace(route.Namespace)
|
||||
|
||||
for _, routeStatus := range routeStatuses.Parents {
|
||||
routeAccepted := meta.IsStatusConditionTrue(
|
||||
routeStatus.Conditions,
|
||||
string(gatewayv1.RouteConditionAccepted),
|
||||
)
|
||||
if !routeAccepted {
|
||||
continue
|
||||
}
|
||||
|
||||
if routeStatus.ParentRef.Namespace == nil {
|
||||
// Set the namespace to the route namespace if not set
|
||||
routeStatus.ParentRef.Namespace = &routeNamespace
|
||||
}
|
||||
|
||||
// Use the indexer to efficiently find the gateway with the specific listener
|
||||
gateway, err := fetchGatewayByListener(ctx, c, routeStatus.ParentRef)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not fetch gateway with listener '%v': %w",
|
||||
routeStatus.ParentRef.SectionName, err)
|
||||
}
|
||||
gatewayProgrammed := meta.IsStatusConditionTrue(
|
||||
gateway.Status.Conditions,
|
||||
string(gatewayv1.GatewayConditionProgrammed),
|
||||
)
|
||||
if !gatewayProgrammed {
|
||||
continue
|
||||
}
|
||||
|
||||
// Since we fetched the gateway using the indexer, we know the listener exists
|
||||
// but we still need to get its details from the gateway spec
|
||||
listener, err := FindMatchingListener(
|
||||
gateway.Spec.Listeners, routeStatus.ParentRef,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to match listener: %w", err)
|
||||
}
|
||||
|
||||
for _, hostname := range route.Spec.Hostnames {
|
||||
rawURL := fmt.Sprintf("https://%s:%d", hostname, listener.Port)
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid url: %w", err)
|
||||
}
|
||||
|
||||
hostnameAddressType := gatewayv1.HostnameAddressType
|
||||
accessPoints = append(accessPoints, kamajiv1alpha1.GatewayAccessPoint{
|
||||
Type: &hostnameAddressType,
|
||||
Value: parsedURL.String(),
|
||||
Port: listener.Port,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -182,6 +182,21 @@ func (r *Agent) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.T
|
||||
return err
|
||||
}
|
||||
|
||||
// Override address with control plane gateway hostname if configured
|
||||
// Konnectivity TLSRoute uses the same hostname as control plane gateway
|
||||
if tenantControlPlane.Spec.ControlPlane.Gateway != nil &&
|
||||
len(tenantControlPlane.Spec.ControlPlane.Gateway.Hostname) > 0 {
|
||||
hostname := tenantControlPlane.Spec.ControlPlane.Gateway.Hostname
|
||||
|
||||
// Extract hostname
|
||||
if len(hostname) > 0 {
|
||||
konnectivityHostname, _ := utilities.GetControlPlaneAddressAndPortFromHostname(
|
||||
string(hostname),
|
||||
tenantControlPlane.Spec.Addons.Konnectivity.KonnectivityServerSpec.Port)
|
||||
address = konnectivityHostname
|
||||
}
|
||||
}
|
||||
|
||||
r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
|
||||
|
||||
specSelector := &metav1.LabelSelector{
|
||||
@@ -281,8 +296,10 @@ func (r *Agent) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.T
|
||||
case kamajiv1alpha1.KonnectivityAgentModeDeployment:
|
||||
//nolint:forcetypeassert
|
||||
r.resource.(*appsv1.Deployment).Spec.Template = *podTemplateSpec
|
||||
//nolint:forcetypeassert
|
||||
r.resource.(*appsv1.Deployment).Spec.Replicas = pointer.To(tenantControlPlane.Spec.Addons.Konnectivity.KonnectivityAgentSpec.Replicas)
|
||||
if tenantControlPlane.Spec.Addons.Konnectivity.KonnectivityAgentSpec.Replicas != nil {
|
||||
//nolint:forcetypeassert
|
||||
r.resource.(*appsv1.Deployment).Spec.Replicas = tenantControlPlane.Spec.Addons.Konnectivity.KonnectivityAgentSpec.Replicas
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
apiserverv1alpha1 "k8s.io/apiserver/pkg/apis/apiserver/v1alpha1"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
@@ -94,18 +93,18 @@ func (r *EgressSelectorConfigurationResource) mutate(_ context.Context, tenantCo
|
||||
return func() error {
|
||||
r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
|
||||
|
||||
configuration := &apiserverv1alpha1.EgressSelectorConfiguration{
|
||||
configuration := &EgressSelectorConfiguration{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: egressSelectorConfigurationKind,
|
||||
APIVersion: apiServerAPIVersion,
|
||||
},
|
||||
EgressSelections: []apiserverv1alpha1.EgressSelection{
|
||||
EgressSelections: []EgressSelection{
|
||||
{
|
||||
Name: egressSelectorConfigurationName,
|
||||
Connection: apiserverv1alpha1.Connection{
|
||||
ProxyProtocol: apiserverv1alpha1.ProtocolGRPC,
|
||||
Transport: &apiserverv1alpha1.Transport{
|
||||
UDS: &apiserverv1alpha1.UDSTransport{
|
||||
Connection: Connection{
|
||||
ProxyProtocol: ProtocolGRPC,
|
||||
Transport: &Transport{
|
||||
UDS: &UDSTransport{
|
||||
UDSName: defaultUDSName,
|
||||
},
|
||||
},
|
||||
|
||||
71
internal/resources/konnectivity/egress_types.go
Normal file
71
internal/resources/konnectivity/egress_types.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package konnectivity
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// Local type definitions for EgressSelectorConfiguration to avoid importing
|
||||
// k8s.io/apiserver which conflicts with controller-runtime workqueue metrics.
|
||||
// These types are based on k8s.io/apiserver/pkg/apis/apiserver/v1alpha1.
|
||||
|
||||
// ProtocolType is the type of the proxy protocol used for egress selection.
|
||||
type ProtocolType string
|
||||
|
||||
const (
|
||||
// ProtocolHTTPConnect uses HTTP CONNECT as the proxy protocol.
|
||||
ProtocolHTTPConnect ProtocolType = "HTTPConnect"
|
||||
// ProtocolGRPC uses GRPC as the proxy protocol.
|
||||
ProtocolGRPC ProtocolType = "GRPC"
|
||||
// ProtocolDirect establishes a direct connection without proxy.
|
||||
ProtocolDirect ProtocolType = "Direct"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:object:generate=true
|
||||
|
||||
// EgressSelectorConfiguration provides versioned configuration for egress selector clients.
|
||||
type EgressSelectorConfiguration struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
EgressSelections []EgressSelection `json:"egressSelections"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
|
||||
// EgressSelection provides the configuration for a single egress selection client.
|
||||
type EgressSelection struct {
|
||||
Name string `json:"name"`
|
||||
Connection Connection `json:"connection"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
|
||||
// Connection provides the configuration for a single egress selection client connection.
|
||||
type Connection struct {
|
||||
ProxyProtocol ProtocolType `json:"proxyProtocol,omitempty"`
|
||||
Transport *Transport `json:"transport,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
|
||||
// Transport defines the transport configurations we support for egress selector.
|
||||
type Transport struct {
|
||||
TCP *TCPTransport `json:"tcp,omitempty"`
|
||||
UDS *UDSTransport `json:"uds,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
|
||||
// TCPTransport provides the information to connect to a TCP endpoint.
|
||||
type TCPTransport struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
|
||||
// UDSTransport provides the information to connect to a Unix Domain Socket endpoint.
|
||||
type UDSTransport struct {
|
||||
UDSName string `json:"udsName,omitempty"`
|
||||
}
|
||||
232
internal/resources/konnectivity/gateway_resource.go
Normal file
232
internal/resources/konnectivity/gateway_resource.go
Normal file
@@ -0,0 +1,232 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package konnectivity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
"github.com/clastix/kamaji/internal/resources"
|
||||
"github.com/clastix/kamaji/internal/utilities"
|
||||
)
|
||||
|
||||
type KubernetesKonnectivityGatewayResource struct {
|
||||
resource *gatewayv1alpha2.TLSRoute
|
||||
Client client.Client
|
||||
}
|
||||
|
||||
func (r *KubernetesKonnectivityGatewayResource) GetHistogram() prometheus.Histogram {
|
||||
gatewayCollector = resources.LazyLoadHistogramFromResource(gatewayCollector, r)
|
||||
|
||||
return gatewayCollector
|
||||
}
|
||||
|
||||
func (r *KubernetesKonnectivityGatewayResource) ShouldStatusBeUpdated(_ context.Context, tcp *kamajiv1alpha1.TenantControlPlane) bool {
|
||||
switch {
|
||||
case !r.shouldHaveGateway(tcp) && (tcp.Status.Addons.Konnectivity.Gateway == nil):
|
||||
return false
|
||||
case r.shouldHaveGateway(tcp) && (tcp.Status.Addons.Konnectivity.Gateway == nil):
|
||||
return true
|
||||
case !r.shouldHaveGateway(tcp) && (tcp.Status.Addons.Konnectivity.Gateway != nil):
|
||||
return true
|
||||
case r.shouldHaveGateway(tcp) && (tcp.Status.Addons.Konnectivity.Gateway != nil):
|
||||
return r.gatewayStatusNeedsUpdate(tcp)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// shouldHaveGateway checks if Konnectivity gateway should be configured.
|
||||
// Create when Konnectivity addon is enabled and control plane gateway is configured.
|
||||
func (r *KubernetesKonnectivityGatewayResource) shouldHaveGateway(tcp *kamajiv1alpha1.TenantControlPlane) bool {
|
||||
if tcp.Spec.Addons.Konnectivity == nil { // konnectivity addon is disabled
|
||||
return false
|
||||
}
|
||||
// Create when control plane gateway is configured
|
||||
return tcp.Spec.ControlPlane.Gateway != nil
|
||||
}
|
||||
|
||||
// gatewayStatusNeedsUpdate compares the current gateway resource status with the stored status.
|
||||
func (r *KubernetesKonnectivityGatewayResource) gatewayStatusNeedsUpdate(tcp *kamajiv1alpha1.TenantControlPlane) bool {
|
||||
currentStatus := tcp.Status.Addons.Konnectivity.Gateway
|
||||
|
||||
// Check if route reference has changed
|
||||
if currentStatus != nil && currentStatus.RouteRef.Name != r.resource.Name {
|
||||
return true
|
||||
}
|
||||
|
||||
// Compare RouteStatus - check if number of parents changed
|
||||
return resources.IsGatewayRouteStatusChanged(currentStatus, r.resource.Status.RouteStatus)
|
||||
}
|
||||
|
||||
func (r *KubernetesKonnectivityGatewayResource) ShouldCleanup(tcp *kamajiv1alpha1.TenantControlPlane) bool {
|
||||
return !r.shouldHaveGateway(tcp) && tcp.Status.Addons.Konnectivity.Gateway != nil
|
||||
}
|
||||
|
||||
func (r *KubernetesKonnectivityGatewayResource) CleanUp(ctx context.Context, tcp *kamajiv1alpha1.TenantControlPlane) (bool, error) {
|
||||
logger := log.FromContext(ctx, "resource", r.GetName())
|
||||
|
||||
cleaned, err := resources.CleanupTLSRoute(ctx, r.Client, r.resource.GetName(), r.resource.GetNamespace(), tcp)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to cleanup konnectivity route")
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
if cleaned {
|
||||
logger.V(1).Info("konnectivity route cleaned up successfully")
|
||||
}
|
||||
|
||||
return cleaned, nil
|
||||
}
|
||||
|
||||
func (r *KubernetesKonnectivityGatewayResource) UpdateTenantControlPlaneStatus(ctx context.Context, tcp *kamajiv1alpha1.TenantControlPlane) error {
|
||||
logger := log.FromContext(ctx, "resource", r.GetName())
|
||||
|
||||
// Clean up status if Gateway routes are no longer configured
|
||||
if !r.shouldHaveGateway(tcp) {
|
||||
tcp.Status.Addons.Konnectivity.Gateway = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
tcp.Status.Addons.Konnectivity.Gateway = &kamajiv1alpha1.KubernetesGatewayStatus{
|
||||
RouteStatus: r.resource.Status.RouteStatus,
|
||||
RouteRef: v1.LocalObjectReference{
|
||||
Name: r.resource.Name,
|
||||
},
|
||||
}
|
||||
|
||||
routeStatuses := tcp.Status.Addons.Konnectivity.Gateway.RouteStatus
|
||||
|
||||
// TODO: Investigate the implications of having multiple parents / hostnames
|
||||
// TODO: Use condition to report?
|
||||
if len(routeStatuses.Parents) == 0 {
|
||||
return fmt.Errorf("no gateway attached to the konnectivity route")
|
||||
}
|
||||
if len(routeStatuses.Parents) > 1 {
|
||||
return fmt.Errorf("too many gateways attached to the konnectivity route")
|
||||
}
|
||||
if len(r.resource.Spec.Hostnames) == 0 {
|
||||
return fmt.Errorf("no hostname in the konnectivity route")
|
||||
}
|
||||
if len(r.resource.Spec.Hostnames) > 1 {
|
||||
return fmt.Errorf("too many hostnames in the konnectivity route")
|
||||
}
|
||||
|
||||
logger.V(1).Info("updating TenantControlPlane status for Konnectivity Gateway routes")
|
||||
accessPoints, err := resources.BuildGatewayAccessPointsStatus(ctx, r.Client, r.resource, routeStatuses)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tcp.Status.Addons.Konnectivity.Gateway.AccessPoints = accessPoints
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *KubernetesKonnectivityGatewayResource) Define(_ context.Context, tcp *kamajiv1alpha1.TenantControlPlane) error {
|
||||
r.resource = &gatewayv1alpha2.TLSRoute{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("%s-konnectivity", tcp.GetName()),
|
||||
Namespace: tcp.GetNamespace(),
|
||||
},
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *KubernetesKonnectivityGatewayResource) mutate(tcp *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn {
|
||||
return func() error {
|
||||
// Use control plane gateway configuration
|
||||
if tcp.Spec.ControlPlane.Gateway == nil {
|
||||
return fmt.Errorf("control plane gateway is not configured")
|
||||
}
|
||||
|
||||
labels := utilities.MergeMaps(
|
||||
r.resource.GetLabels(),
|
||||
utilities.KamajiLabels(tcp.GetName(), r.GetName()),
|
||||
tcp.Spec.ControlPlane.Gateway.AdditionalMetadata.Labels,
|
||||
)
|
||||
r.resource.SetLabels(labels)
|
||||
|
||||
annotations := utilities.MergeMaps(
|
||||
r.resource.GetAnnotations(),
|
||||
tcp.Spec.ControlPlane.Gateway.AdditionalMetadata.Annotations,
|
||||
)
|
||||
r.resource.SetAnnotations(annotations)
|
||||
|
||||
// Use hostname from control plane gateway
|
||||
if len(tcp.Spec.ControlPlane.Gateway.Hostname) == 0 {
|
||||
return fmt.Errorf("control plane gateway hostname is not set")
|
||||
}
|
||||
|
||||
serviceName := gatewayv1alpha2.ObjectName(tcp.Status.Addons.Konnectivity.Service.Name)
|
||||
servicePort := tcp.Status.Addons.Konnectivity.Service.Port
|
||||
|
||||
if serviceName == "" || servicePort == 0 {
|
||||
return fmt.Errorf("konnectivity service not ready, cannot create TLSRoute")
|
||||
}
|
||||
|
||||
// Copy parentRefs from control plane gateway and explicitly set port and sectionName fields
|
||||
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")
|
||||
|
||||
rule := gatewayv1alpha2.TLSRouteRule{
|
||||
BackendRefs: []gatewayv1alpha2.BackendRef{
|
||||
{
|
||||
BackendObjectReference: gatewayv1alpha2.BackendObjectReference{
|
||||
Name: serviceName,
|
||||
Port: &servicePort,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r.resource.Spec.Hostnames = []gatewayv1.Hostname{tcp.Spec.ControlPlane.Gateway.Hostname}
|
||||
r.resource.Spec.Rules = []gatewayv1alpha2.TLSRouteRule{rule}
|
||||
|
||||
return controllerutil.SetControllerReference(tcp, r.resource, r.Client.Scheme())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *KubernetesKonnectivityGatewayResource) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
|
||||
logger := log.FromContext(ctx, "resource", r.GetName())
|
||||
|
||||
if !r.shouldHaveGateway(tenantControlPlane) {
|
||||
return controllerutil.OperationResultNone, nil
|
||||
}
|
||||
|
||||
if tenantControlPlane.Spec.ControlPlane.Gateway == nil {
|
||||
return controllerutil.OperationResultNone, nil
|
||||
}
|
||||
|
||||
if len(tenantControlPlane.Spec.ControlPlane.Gateway.Hostname) == 0 {
|
||||
return controllerutil.OperationResultNone, fmt.Errorf("missing hostname to expose Konnectivity using a Gateway resource")
|
||||
}
|
||||
|
||||
logger.V(1).Info("creating or updating resource konnectivity gateway routes")
|
||||
|
||||
result, err := utilities.CreateOrUpdateWithConflict(ctx, r.Client, r.resource, r.mutate(tenantControlPlane))
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *KubernetesKonnectivityGatewayResource) GetName() string {
|
||||
return "konnectivity_gateway_routes"
|
||||
}
|
||||
218
internal/resources/konnectivity/gateway_resource_test.go
Normal file
218
internal/resources/konnectivity/gateway_resource_test.go
Normal file
@@ -0,0 +1,218 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package konnectivity_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
"github.com/clastix/kamaji/internal/resources/konnectivity"
|
||||
)
|
||||
|
||||
func TestKonnectivityGatewayResource(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Konnectivity Gateway Resource Suite")
|
||||
}
|
||||
|
||||
var runtimeScheme *runtime.Scheme
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
runtimeScheme = runtime.NewScheme()
|
||||
Expect(scheme.AddToScheme(runtimeScheme)).To(Succeed())
|
||||
Expect(kamajiv1alpha1.AddToScheme(runtimeScheme)).To(Succeed())
|
||||
Expect(gatewayv1alpha2.Install(runtimeScheme)).To(Succeed())
|
||||
})
|
||||
|
||||
var _ = Describe("KubernetesKonnectivityGatewayResource", func() {
|
||||
var (
|
||||
tcp *kamajiv1alpha1.TenantControlPlane
|
||||
resource *konnectivity.KubernetesKonnectivityGatewayResource
|
||||
ctx context.Context
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(runtimeScheme).
|
||||
Build()
|
||||
|
||||
resource = &konnectivity.KubernetesKonnectivityGatewayResource{
|
||||
Client: fakeClient,
|
||||
}
|
||||
|
||||
namespace := gatewayv1.Namespace("default")
|
||||
tcp = &kamajiv1alpha1.TenantControlPlane{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-tcp",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: kamajiv1alpha1.TenantControlPlaneSpec{
|
||||
ControlPlane: kamajiv1alpha1.ControlPlane{
|
||||
Gateway: &kamajiv1alpha1.GatewaySpec{
|
||||
Hostname: gatewayv1alpha2.Hostname("test.example.com"),
|
||||
GatewayParentRefs: []gatewayv1alpha2.ParentReference{
|
||||
{
|
||||
Name: "test-gateway",
|
||||
Namespace: &namespace,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Addons: kamajiv1alpha1.AddonsSpec{
|
||||
Konnectivity: &kamajiv1alpha1.KonnectivitySpec{
|
||||
KonnectivityServerSpec: kamajiv1alpha1.KonnectivityServerSpec{
|
||||
Port: 8132,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: kamajiv1alpha1.TenantControlPlaneStatus{
|
||||
Addons: kamajiv1alpha1.AddonsStatus{
|
||||
Konnectivity: kamajiv1alpha1.KonnectivityStatus{
|
||||
Service: kamajiv1alpha1.KubernetesServiceStatus{
|
||||
Name: "test-konnectivity-service",
|
||||
Port: 8132,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
Describe("shouldHaveGateway logic", func() {
|
||||
It("should return false when Konnectivity addon is disabled", func() {
|
||||
tcp.Spec.Addons.Konnectivity = nil
|
||||
shouldUpdate := resource.ShouldStatusBeUpdated(ctx, tcp)
|
||||
Expect(shouldUpdate).To(BeFalse())
|
||||
Expect(resource.ShouldCleanup(tcp)).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should return false when control plane gateway is not configured", func() {
|
||||
tcp.Spec.ControlPlane.Gateway = nil
|
||||
shouldUpdate := resource.ShouldStatusBeUpdated(ctx, tcp)
|
||||
Expect(shouldUpdate).To(BeFalse())
|
||||
Expect(resource.ShouldCleanup(tcp)).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should return true when both Konnectivity and gateway are configured", func() {
|
||||
shouldUpdate := resource.ShouldStatusBeUpdated(ctx, tcp)
|
||||
Expect(shouldUpdate).To(BeTrue())
|
||||
Expect(resource.ShouldCleanup(tcp)).To(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
Context("When Konnectivity gateway should be configured", func() {
|
||||
It("should set correct TLSRoute name with -konnectivity suffix", func() {
|
||||
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: "test-tcp-konnectivity", Namespace: tcp.Namespace}, route)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(route.Name).To(Equal("test-tcp-konnectivity"))
|
||||
})
|
||||
|
||||
It("should set sectionName to \"konnectivity-server\" and port from Konnectivity service status", func() {
|
||||
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: "test-tcp-konnectivity", Namespace: tcp.Namespace}, route)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(route.Spec.ParentRefs).To(HaveLen(1))
|
||||
Expect(route.Spec.ParentRefs[0].SectionName).NotTo(BeNil())
|
||||
Expect(*route.Spec.ParentRefs[0].SectionName).To(Equal(gatewayv1.SectionName("konnectivity-server")))
|
||||
Expect(route.Spec.ParentRefs[0].Port).NotTo(BeNil())
|
||||
Expect(*route.Spec.ParentRefs[0].Port).To(Equal(tcp.Status.Addons.Konnectivity.Service.Port))
|
||||
})
|
||||
|
||||
It("should use control plane gateway hostname", func() {
|
||||
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: "test-tcp-konnectivity", Namespace: tcp.Namespace}, route)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(route.Spec.Hostnames).To(HaveLen(1))
|
||||
Expect(route.Spec.Hostnames[0]).To(Equal(tcp.Spec.ControlPlane.Gateway.Hostname))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Konnectivity-specific error cases", func() {
|
||||
It("should return early without error when control plane gateway is not configured", func() {
|
||||
tcp.Spec.ControlPlane.Gateway = nil
|
||||
|
||||
err := resource.Define(ctx, tcp)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
result, err := resource.CreateOrUpdate(ctx, tcp)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result).To(Equal(controllerutil.OperationResultNone))
|
||||
})
|
||||
|
||||
It("should fail when Konnectivity service is not ready", func() {
|
||||
tcp.Status.Addons.Konnectivity.Service.Name = ""
|
||||
tcp.Status.Addons.Konnectivity.Service.Port = 0
|
||||
|
||||
err := resource.Define(ctx, tcp)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err = resource.CreateOrUpdate(ctx, tcp)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("konnectivity service not ready"))
|
||||
})
|
||||
|
||||
It("should fail when control plane gateway parentRefs are not specified", func() {
|
||||
tcp.Spec.ControlPlane.Gateway.GatewayParentRefs = nil
|
||||
|
||||
err := resource.Define(ctx, tcp)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err = resource.CreateOrUpdate(ctx, tcp)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("control plane gateway parentRefs are not specified"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("When Konnectivity gateway should not be configured", func() {
|
||||
BeforeEach(func() {
|
||||
tcp.Spec.Addons.Konnectivity = nil
|
||||
tcp.Status.Addons.Konnectivity = kamajiv1alpha1.KonnectivityStatus{
|
||||
Gateway: &kamajiv1alpha1.KubernetesGatewayStatus{
|
||||
AccessPoints: nil,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
It("should cleanup when gateway is removed", func() {
|
||||
Expect(resource.ShouldCleanup(tcp)).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
It("should return correct resource name", func() {
|
||||
Expect(resource.GetName()).To(Equal("konnectivity_gateway_routes"))
|
||||
})
|
||||
})
|
||||
@@ -13,6 +13,7 @@ var (
|
||||
clusterrolebindingCollector prometheus.Histogram
|
||||
deploymentCollector prometheus.Histogram
|
||||
egressCollector prometheus.Histogram
|
||||
gatewayCollector prometheus.Histogram
|
||||
kubeconfigCollector prometheus.Histogram
|
||||
serviceaccountCollector prometheus.Histogram
|
||||
serviceCollector prometheus.Histogram
|
||||
|
||||
134
internal/resources/konnectivity/zz_generated.deepcopy.go
Normal file
134
internal/resources/konnectivity/zz_generated.deepcopy.go
Normal file
@@ -0,0 +1,134 @@
|
||||
//go:build !ignore_autogenerated
|
||||
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
|
||||
package konnectivity
|
||||
|
||||
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 *Connection) DeepCopyInto(out *Connection) {
|
||||
*out = *in
|
||||
if in.Transport != nil {
|
||||
in, out := &in.Transport, &out.Transport
|
||||
*out = new(Transport)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Connection.
|
||||
func (in *Connection) DeepCopy() *Connection {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Connection)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *EgressSelection) DeepCopyInto(out *EgressSelection) {
|
||||
*out = *in
|
||||
in.Connection.DeepCopyInto(&out.Connection)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EgressSelection.
|
||||
func (in *EgressSelection) DeepCopy() *EgressSelection {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(EgressSelection)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *EgressSelectorConfiguration) DeepCopyInto(out *EgressSelectorConfiguration) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
if in.EgressSelections != nil {
|
||||
in, out := &in.EgressSelections, &out.EgressSelections
|
||||
*out = make([]EgressSelection, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EgressSelectorConfiguration.
|
||||
func (in *EgressSelectorConfiguration) DeepCopy() *EgressSelectorConfiguration {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(EgressSelectorConfiguration)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *EgressSelectorConfiguration) 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 *TCPTransport) DeepCopyInto(out *TCPTransport) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPTransport.
|
||||
func (in *TCPTransport) DeepCopy() *TCPTransport {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(TCPTransport)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Transport) DeepCopyInto(out *Transport) {
|
||||
*out = *in
|
||||
if in.TCP != nil {
|
||||
in, out := &in.TCP, &out.TCP
|
||||
*out = new(TCPTransport)
|
||||
**out = **in
|
||||
}
|
||||
if in.UDS != nil {
|
||||
in, out := &in.UDS, &out.UDS
|
||||
*out = new(UDSTransport)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Transport.
|
||||
func (in *Transport) DeepCopy() *Transport {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Transport)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *UDSTransport) DeepCopyInto(out *UDSTransport) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UDSTransport.
|
||||
func (in *UDSTransport) DeepCopy() *UDSTransport {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(UDSTransport)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jsonpatchv5 "github.com/evanphx/json-patch/v5"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
@@ -150,7 +151,21 @@ func (r *KubeadmPhase) GetKubeadmFunction(ctx context.Context, tcp *kamajiv1alph
|
||||
case PhaseUploadConfigKubeadm:
|
||||
return kubeadm.UploadKubeadmConfig, nil
|
||||
case PhaseUploadConfigKubelet:
|
||||
return kubeadm.UploadKubeletConfig, nil
|
||||
return func(c clientset.Interface, configuration *kubeadm.Configuration) ([]byte, error) {
|
||||
var patch jsonpatchv5.Patch
|
||||
|
||||
if len(tcp.Spec.Kubernetes.Kubelet.ConfigurationJSONPatches) > 0 {
|
||||
jsonP, patchErr := tcp.Spec.Kubernetes.Kubelet.ConfigurationJSONPatches.ToJSON()
|
||||
if patchErr != nil {
|
||||
return nil, fmt.Errorf("cannot encode JSON Patches to JSON: %w", patchErr)
|
||||
}
|
||||
if patch, patchErr = jsonpatchv5.DecodePatch(jsonP); patchErr != nil {
|
||||
return nil, fmt.Errorf("cannot decode JSON Patches: %w", patchErr)
|
||||
}
|
||||
}
|
||||
|
||||
return kubeadm.UploadKubeletConfig(c, configuration, patch)
|
||||
}, nil
|
||||
case PhaseBootstrapToken:
|
||||
return func(client clientset.Interface, config *kubeadm.Configuration) ([]byte, error) {
|
||||
config.InitConfiguration.BootstrapTokens = nil
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
"k8s.io/kubernetes/cmd/kubeadm/app/phases/upgrade"
|
||||
@@ -75,7 +74,7 @@ func (k *KubernetesUpgrade) CreateOrUpdate(ctx context.Context, tenantControlPla
|
||||
// Checking if the upgrade is allowed, or not
|
||||
clientSet, err := utilities.GetTenantClientSet(ctx, k.Client, tenantControlPlane)
|
||||
if err != nil {
|
||||
return controllerutil.OperationResultNone, errors.Wrap(err, "cannot create REST client required for Kubernetes upgrade plan")
|
||||
return controllerutil.OperationResultNone, fmt.Errorf("cannot create REST client required for Kubernetes upgrade plan: %w", err)
|
||||
}
|
||||
|
||||
var coreDNSVersion string
|
||||
@@ -86,7 +85,7 @@ func (k *KubernetesUpgrade) CreateOrUpdate(ctx context.Context, tenantControlPla
|
||||
versionGetter := kamajiupgrade.NewKamajiKubeVersionGetter(clientSet, tenantControlPlane.Status.Kubernetes.Version.Version, coreDNSVersion, tenantControlPlane.Status.Kubernetes.Version.Status)
|
||||
|
||||
if _, err = upgrade.GetAvailableUpgrades(versionGetter, false, false, &printers.Discard{}); err != nil {
|
||||
return controllerutil.OperationResultNone, errors.Wrap(err, "cannot retrieve available Upgrades for Kubernetes upgrade plan")
|
||||
return controllerutil.OperationResultNone, fmt.Errorf("cannot retrieve available Upgrades for Kubernetes upgrade plan: %w", err)
|
||||
}
|
||||
|
||||
if err = k.isUpgradable(); err != nil {
|
||||
@@ -123,12 +122,12 @@ func (k *KubernetesUpgrade) UpdateTenantControlPlaneStatus(_ context.Context, te
|
||||
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))
|
||||
return fmt.Errorf("unable to parse normalized version %q as a semantic version: %w", k.upgrade.After.KubeVersion, err)
|
||||
}
|
||||
|
||||
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))
|
||||
return fmt.Errorf("unable to parse normalized version %q as a semantic version: %w", k.upgrade.After.KubeVersion, err)
|
||||
}
|
||||
|
||||
if newK8sVersion.Minor() < oldK8sVersion.Minor() {
|
||||
|
||||
@@ -5,9 +5,9 @@ package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -25,17 +25,17 @@ import (
|
||||
func GetKubeadmManifestDeps(ctx context.Context, client client.Client, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (*clientset.Clientset, *kubeadm.Configuration, error) {
|
||||
config, err := getStoredKubeadmConfiguration(ctx, client, "", tenantControlPlane)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "cannot retrieve kubeadm configuration")
|
||||
return nil, nil, fmt.Errorf("cannot retrieve kubeadm configuration: %w", err)
|
||||
}
|
||||
|
||||
kubeconfig, err := utilities.GetTenantKubeconfig(ctx, client, tenantControlPlane)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "cannot retrieve kubeconfig configuration")
|
||||
return nil, nil, fmt.Errorf("cannot retrieve kubeconfig configuration: %w", err)
|
||||
}
|
||||
|
||||
address, _, err := tenantControlPlane.AssignedControlPlaneAddress()
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "cannot retrieve Tenant Control Plane address")
|
||||
return nil, nil, fmt.Errorf("cannot retrieve Tenant Control Plane address: %w", err)
|
||||
}
|
||||
|
||||
config.Kubeconfig = *kubeconfig
|
||||
@@ -47,7 +47,7 @@ func GetKubeadmManifestDeps(ctx context.Context, client client.Client, tenantCon
|
||||
TenantControlPlaneAddress: address,
|
||||
TenantControlPlaneCertSANs: tenantControlPlane.Spec.NetworkProfile.CertSANs,
|
||||
TenantControlPlanePort: tenantControlPlane.Spec.NetworkProfile.Port,
|
||||
TenantControlPlaneCGroupDriver: tenantControlPlane.Spec.Kubernetes.Kubelet.CGroupFS.String(),
|
||||
TenantControlPlaneCGroupDriver: tenantControlPlane.Spec.Kubernetes.Kubelet.CGroupFS.String(), //nolint:staticcheck
|
||||
}
|
||||
// If CoreDNS addon is enabled and with an override, adding these to the kubeadm init configuration
|
||||
if coreDNS := tenantControlPlane.Spec.Addons.CoreDNS; coreDNS != nil {
|
||||
@@ -80,7 +80,7 @@ func GetKubeadmManifestDeps(ctx context.Context, client client.Client, tenantCon
|
||||
|
||||
tenantClient, err := utilities.GetTenantClientSet(ctx, client, tenantControlPlane)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "cannot generate tenant client")
|
||||
return nil, nil, fmt.Errorf("cannot generate tenant client: %w", err)
|
||||
}
|
||||
|
||||
return tenantClient, config, nil
|
||||
@@ -200,7 +200,7 @@ func KubeadmPhaseCreate(ctx context.Context, r KubeadmPhaseResource, logger logr
|
||||
TenantControlPlaneAddress: address,
|
||||
TenantControlPlaneCertSANs: tenantControlPlane.Spec.NetworkProfile.CertSANs,
|
||||
TenantControlPlanePort: tenantControlPlane.Spec.NetworkProfile.Port,
|
||||
TenantControlPlaneCGroupDriver: tenantControlPlane.Spec.Kubernetes.Kubelet.CGroupFS.String(),
|
||||
TenantControlPlaneCGroupDriver: tenantControlPlane.Spec.Kubernetes.Kubelet.CGroupFS.String(), //nolint:staticcheck
|
||||
}
|
||||
|
||||
var checksum string
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
versionutil "k8s.io/apimachinery/pkg/util/version"
|
||||
apimachineryversion "k8s.io/apimachinery/pkg/version"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@@ -63,7 +62,7 @@ func (k kamajiKubeVersionGetter) KubeadmVersion() (string, *versionutil.Version,
|
||||
|
||||
kubeadmVersion, err := versionutil.ParseSemantic(kubeadmVersionInfo.String())
|
||||
if err != nil {
|
||||
return "", nil, errors.Wrap(err, "Couldn't parse kubeadm version")
|
||||
return "", nil, fmt.Errorf("couldn't parse kubeadm version: %w", err)
|
||||
}
|
||||
|
||||
return kubeadmVersionInfo.String(), kubeadmVersion, nil
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gomodules.xyz/jsonpatch/v2"
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
@@ -34,11 +33,11 @@ func (h handlersChainer) Handler(object runtime.Object, routeHandlers ...handler
|
||||
// When deleting the OldObject struct field contains the object being deleted:
|
||||
// https://github.com/kubernetes/kubernetes/pull/76346
|
||||
if err := h.decoder.DecodeRaw(req.OldObject, decodedObj); err != nil {
|
||||
return admission.Errored(http.StatusInternalServerError, errors.Wrap(err, fmt.Sprintf("unable to decode deleted object into %T", object)))
|
||||
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("unable to decode deleted object into %T: %w", object, err))
|
||||
}
|
||||
default:
|
||||
if err := h.decoder.Decode(req, decodedObj); err != nil {
|
||||
return admission.Errored(http.StatusInternalServerError, errors.Wrap(err, fmt.Sprintf("unable to decode into %T", object)))
|
||||
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("unable to decode into %T: %w", object, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +69,7 @@ func (h handlersChainer) Handler(object runtime.Object, routeHandlers ...handler
|
||||
}
|
||||
case admissionv1.Update:
|
||||
if err := h.decoder.DecodeRaw(req.OldObject, oldDecodedObj); err != nil {
|
||||
return admission.Errored(http.StatusInternalServerError, errors.Wrap(err, fmt.Sprintf("unable to decode old object into %T", object)))
|
||||
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("unable to decode old object into %T: %w", object, err))
|
||||
}
|
||||
|
||||
for _, routeHandler := range routeHandlers {
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gomodules.xyz/jsonpatch/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
@@ -39,7 +38,7 @@ func (d DataStoreSecretValidation) OnUpdate(object runtime.Object, _ runtime.Obj
|
||||
dsList := &kamajiv1alpha1.DataStoreList{}
|
||||
|
||||
if err := d.Client.List(ctx, dsList, client.MatchingFieldsSelector{Selector: fields.OneTermEqualSelector(kamajiv1alpha1.DatastoreUsedSecretNamespacedNameKey, fmt.Sprintf("%s/%s", secret.GetNamespace(), secret.GetName()))}); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot list Tenant Control Plane using the provided Secret")
|
||||
return nil, fmt.Errorf("cannot list Tenant Control Plane using the provided Secret: %w", err)
|
||||
}
|
||||
|
||||
if len(dsList.Items) > 0 {
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gomodules.xyz/jsonpatch/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -38,7 +37,7 @@ func (d DataStoreValidation) OnDelete(object runtime.Object) AdmissionResponse {
|
||||
|
||||
tcpList := &kamajiv1alpha1.TenantControlPlaneList{}
|
||||
if err := d.Client.List(ctx, tcpList, client.MatchingFieldsSelector{Selector: fields.OneTermEqualSelector(kamajiv1alpha1.TenantControlPlaneUsedDataStoreKey, ds.GetName())}); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot retrieve TenantControlPlane list used by the DataStore")
|
||||
return nil, fmt.Errorf("cannot retrieve TenantControlPlane list used by the DataStore: %w", err)
|
||||
}
|
||||
|
||||
if len(tcpList.Items) > 0 {
|
||||
|
||||
@@ -5,9 +5,9 @@ package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gomodules.xyz/jsonpatch/v2"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
pointer "k8s.io/utils/ptr"
|
||||
@@ -31,7 +31,7 @@ func (t TenantControlPlaneDefaults) OnCreate(object runtime.Object) AdmissionRes
|
||||
if len(defaulted.Spec.NetworkProfile.DNSServiceIPs) == 0 {
|
||||
ip, _, err := net.ParseCIDR(defaulted.Spec.NetworkProfile.ServiceCIDR)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot define resulting DNS Service IP")
|
||||
return nil, fmt.Errorf("cannot define resulting DNS Service IP: %w", err)
|
||||
}
|
||||
switch {
|
||||
case ip.To4() != nil:
|
||||
@@ -45,7 +45,7 @@ func (t TenantControlPlaneDefaults) OnCreate(object runtime.Object) AdmissionRes
|
||||
|
||||
operations, err := utils.JSONPatch(original, defaulted)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot create patch responses upon Tenant Control Plane creation")
|
||||
return nil, fmt.Errorf("cannot create patch responses upon Tenant Control Plane creation: %w", err)
|
||||
}
|
||||
|
||||
return operations, nil
|
||||
|
||||
@@ -5,9 +5,9 @@ package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/pkg/errors"
|
||||
"gomodules.xyz/jsonpatch/v2"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -103,7 +103,7 @@ func (t TenantControlPlaneDeployment) OnUpdate(newObject runtime.Object, oldObje
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "the resulting Deployment will generate a configuration error, cannot proceed")
|
||||
return nil, fmt.Errorf("the resulting Deployment will generate a configuration error, cannot proceed: %w", err)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/pkg/errors"
|
||||
"gomodules.xyz/jsonpatch/v2"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
@@ -27,14 +26,14 @@ func (t TenantControlPlaneVersion) OnCreate(object runtime.Object) AdmissionResp
|
||||
|
||||
ver, err := semver.New(t.normalizeKubernetesVersion(tcp.Spec.Kubernetes.Version))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to parse the desired Kubernetes version")
|
||||
return nil, fmt.Errorf("unable to parse the desired Kubernetes version: %w", err)
|
||||
}
|
||||
// No need to check if the patch version
|
||||
ver.Patch = 0
|
||||
|
||||
supportedVer, supportedErr := semver.Make(t.normalizeKubernetesVersion(upgrade.KubeadmVersion))
|
||||
if supportedErr != nil {
|
||||
return nil, errors.Wrap(supportedErr, "unable to parse the Kamaji supported Kubernetes version")
|
||||
return nil, fmt.Errorf("unable to parse the Kamaji supported Kubernetes version: %w", supportedErr)
|
||||
}
|
||||
|
||||
if ver.GT(supportedVer) {
|
||||
@@ -67,21 +66,21 @@ func (t TenantControlPlaneVersion) OnUpdate(object runtime.Object, oldObject run
|
||||
|
||||
oldVer, oldErr := semver.Make(t.normalizeKubernetesVersion(oldTCP.Spec.Kubernetes.Version))
|
||||
if oldErr != nil {
|
||||
return nil, errors.Wrap(oldErr, "unable to parse the previous Kubernetes version")
|
||||
return nil, fmt.Errorf("unable to parse the previous Kubernetes version: %w", oldErr)
|
||||
}
|
||||
// No need to check if the patch version
|
||||
oldVer.Patch = 0
|
||||
|
||||
newVer, newErr := semver.New(t.normalizeKubernetesVersion(newTCP.Spec.Kubernetes.Version))
|
||||
if newErr != nil {
|
||||
return nil, errors.Wrap(newErr, "unable to parse the desired Kubernetes version")
|
||||
return nil, fmt.Errorf("unable to parse the desired Kubernetes version: %w", newErr)
|
||||
}
|
||||
// No need to check if the patch version
|
||||
newVer.Patch = 0
|
||||
|
||||
supportedVer, supportedErr := semver.Make(t.normalizeKubernetesVersion(upgrade.KubeadmVersion))
|
||||
if supportedErr != nil {
|
||||
return nil, errors.Wrap(supportedErr, "unable to parse the Kamaji supported Kubernetes version")
|
||||
return nil, fmt.Errorf("unable to parse the Kamaji supported Kubernetes version: %w", supportedErr)
|
||||
}
|
||||
|
||||
switch {
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
json "github.com/json-iterator/go"
|
||||
"github.com/pkg/errors"
|
||||
"gomodules.xyz/jsonpatch/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
@@ -13,12 +14,12 @@ import (
|
||||
func JSONPatch(original, modified client.Object) ([]jsonpatch.Operation, error) {
|
||||
originalJSON, err := json.Marshal(original)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot marshal original object")
|
||||
return nil, fmt.Errorf("cannot marshal original object: %w", err)
|
||||
}
|
||||
|
||||
modifiedJSON, err := json.Marshal(modified)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot marshal modified object")
|
||||
return nil, fmt.Errorf("cannot marshal modified object: %w", err)
|
||||
}
|
||||
|
||||
return jsonpatch.CreatePatch(originalJSON, modifiedJSON)
|
||||
|
||||
156
internal/workqueue_metrics_test.go
Normal file
156
internal/workqueue_metrics_test.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/metrics"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
// dummyReconciler is a minimal reconciler for testing.
|
||||
type dummyReconciler struct{}
|
||||
|
||||
func (r *dummyReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
// TestWorkqueueMetricsRegistration verifies that controller-runtime workqueue
|
||||
// metrics are properly registered after removing the k8s.io/apiserver import.
|
||||
// This test proves that issue #1026 is fixed.
|
||||
func TestWorkqueueMetricsRegistration(t *testing.T) {
|
||||
// Create a minimal scheme and manager
|
||||
scheme := runtime.NewScheme()
|
||||
|
||||
// Create a manager with a fake config - this will trigger controller-runtime initialization
|
||||
mgr, err := manager.New(&rest.Config{
|
||||
Host: "https://localhost:6443",
|
||||
}, manager.Options{
|
||||
Scheme: scheme,
|
||||
Metrics: metricsserver.Options{
|
||||
BindAddress: "0", // Disable metrics server binding
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
// If we can't create a manager (e.g., no cluster), skip the full test
|
||||
// but still verify basic metrics registration
|
||||
t.Logf("Could not create manager (expected in unit test): %v", err)
|
||||
t.Log("Falling back to basic metrics check...")
|
||||
checkBasicMetrics(t)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Create a controller with the manager - this triggers workqueue creation
|
||||
_, err = controller.New("test-controller", mgr, controller.Options{
|
||||
Reconciler: &dummyReconciler{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create controller: %v", err)
|
||||
}
|
||||
|
||||
// Start the manager in background
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
_ = mgr.Start(ctx)
|
||||
}()
|
||||
|
||||
// Give it a moment to initialize
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Gather all registered metrics from controller-runtime's registry
|
||||
metricFamilies, err := metrics.Registry.Gather()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to gather metrics: %v", err)
|
||||
}
|
||||
|
||||
// Collect all workqueue metrics
|
||||
workqueueMetrics := make(map[string]*dto.MetricFamily)
|
||||
for _, mf := range metricFamilies {
|
||||
name := mf.GetName()
|
||||
if len(name) > 10 && name[:10] == "workqueue_" {
|
||||
workqueueMetrics[name] = mf
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Total metrics registered: %d", len(metricFamilies))
|
||||
t.Logf("Workqueue metrics found: %d", len(workqueueMetrics))
|
||||
|
||||
// Verify we have workqueue metrics
|
||||
if len(workqueueMetrics) == 0 {
|
||||
t.Fatal("FAILED: No workqueue metrics found! The initialization conflict is still present.")
|
||||
}
|
||||
|
||||
// List all found workqueue metrics
|
||||
t.Log("Found workqueue metrics:")
|
||||
for name := range workqueueMetrics {
|
||||
t.Logf(" - %s", name)
|
||||
}
|
||||
|
||||
// Check for specific expected metrics from controller-runtime
|
||||
expectedMetrics := []string{
|
||||
"workqueue_depth",
|
||||
"workqueue_adds_total",
|
||||
"workqueue_queue_duration_seconds",
|
||||
"workqueue_work_duration_seconds",
|
||||
"workqueue_retries_total",
|
||||
"workqueue_unfinished_work_seconds",
|
||||
"workqueue_longest_running_processor_seconds",
|
||||
}
|
||||
|
||||
missingMetrics := []string{}
|
||||
for _, expected := range expectedMetrics {
|
||||
if _, found := workqueueMetrics[expected]; !found {
|
||||
missingMetrics = append(missingMetrics, expected)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingMetrics) > 0 {
|
||||
t.Errorf("Missing expected workqueue metrics: %v", missingMetrics)
|
||||
} else {
|
||||
t.Log("✅ SUCCESS: All expected workqueue metrics are present!")
|
||||
t.Log("The fix successfully resolved issue #1026 - workqueue metrics are now registered.")
|
||||
}
|
||||
}
|
||||
|
||||
// checkBasicMetrics is a fallback check when we can't create a full manager.
|
||||
func checkBasicMetrics(t *testing.T) {
|
||||
t.Helper()
|
||||
// Gather metrics
|
||||
metricFamilies, err := metrics.Registry.Gather()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to gather metrics: %v", err)
|
||||
}
|
||||
|
||||
// Count workqueue metrics
|
||||
workqueueCount := 0
|
||||
for _, mf := range metricFamilies {
|
||||
name := mf.GetName()
|
||||
if len(name) > 10 && name[:10] == "workqueue_" {
|
||||
workqueueCount++
|
||||
t.Logf("Found: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Total metrics: %d", len(metricFamilies))
|
||||
t.Logf("Workqueue metrics: %d", workqueueCount)
|
||||
|
||||
if workqueueCount > 0 {
|
||||
t.Log("✅ Workqueue metrics are being registered!")
|
||||
} else {
|
||||
t.Log("ℹ️ No workqueue metrics yet (this is expected without an actual controller)")
|
||||
t.Log("The fix removed the import conflict - metrics will appear when controllers run")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user