diff --git a/Makefile b/Makefile index e248382..66affff 100644 --- a/Makefile +++ b/Makefile @@ -18,8 +18,11 @@ CONTAINER_REGISTRY ?= ghcr.io/kubestellar/kubeflex # latest tag LATEST_TAG ?= $(shell git describe --tags $(git rev-list --tags --max-count=1)) -# Image URL to use all building/pushing image targets -IMG ?= ghcr.io/kubestellar/kubeflex/manager:latest +KO_DOCKER_REPO ?= ko.local +IMAGE_TAG ?= $(shell git rev-parse --short HEAD) +CMD_NAME ?= manager +IMG ?= ${KO_DOCKER_REPO}/${CMD_NAME}:${IMAGE_TAG} + # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. ENVTEST_K8S_VERSION = 1.26.1 @@ -195,6 +198,19 @@ chart: manifests kustomize @mkdir -p chart/crds $(KUSTOMIZE) build config/crd > chart/crds/crds.yaml +.PHONY: ko-local-build +ko-local-build: + KO_DOCKER_REPO=${KO_DOCKER_REPO} ko build -B ./cmd/${CMD_NAME} -t ${IMAGE_TAG} --platform linux/${ARCH} + +# this is used for local testing +.PHONY: kind-load-image +kind-load-image: + kind load docker-image ${IMG} --name kubeflex + +.PHONY: install-local-chart +install-local-chart: chart kind-load-image + helm upgrade --install --create-namespace -n kubeflex-system kubeflex-operator ./chart + ##@ Build Dependencies ## Location to install dependencies to diff --git a/api/v1alpha1/conditions_test.go b/api/v1alpha1/conditions_test.go index e7930c2..083a94c 100644 --- a/api/v1alpha1/conditions_test.go +++ b/api/v1alpha1/conditions_test.go @@ -99,5 +99,5 @@ func generateCondition(ctype ConditionType, reason ConditionReason, message stri } func addTime(t time.Duration) metav1.Time { - return metav1.NewTime(time.Now().Add(2 * time.Hour)) + return metav1.NewTime(time.Now().Add(t)) } diff --git a/api/v1alpha1/controlplane_types.go b/api/v1alpha1/controlplane_types.go index 1503e96..0635f42 100644 --- a/api/v1alpha1/controlplane_types.go +++ b/api/v1alpha1/controlplane_types.go @@ -22,10 +22,17 @@ import ( // ControlPlaneSpec defines the desired state of ControlPlane type ControlPlaneSpec struct { - Type ControlPlaneType `json:"type,omitempty"` - Backend BackendDBType `json:"backend,omitempty"` - PostCreateHook *string `json:"postCreateHook,omitempty"` - PostCreateHookVars map[string]string `json:"postCreateHookVars,omitempty"` + Type ControlPlaneType `json:"type,omitempty"` + Backend BackendDBType `json:"backend,omitempty"` + // BootstrapSecretRef contains a reference to the kubeconfig used to bootstrap adoption of + // an external cluster + // +optional + BootstrapSecretRef *SecretReference `json:"bootstrapSecretRef,omitempty"` + // expiration time for token of adopted cluster + // +optional + AdoptedTokenExpirationSeconds *int64 `json:"adoptedTokenExpirationSeconds,omitempty"` + PostCreateHook *string `json:"postCreateHook,omitempty"` + PostCreateHookVars map[string]string `json:"postCreateHookVars,omitempty"` } // ControlPlaneStatus defines the observed state of ControlPlane @@ -71,7 +78,7 @@ const ( BackendDBTypeDedicated BackendDBType = "dedicated" ) -// +kubebuilder:validation:Enum=k8s;ocm;vcluster;host +// +kubebuilder:validation:Enum=k8s;ocm;vcluster;host;external type ControlPlaneType string const ( @@ -79,6 +86,7 @@ const ( ControlPlaneTypeOCM ControlPlaneType = "ocm" ControlPlaneTypeVCluster ControlPlaneType = "vcluster" ControlPlaneTypeHost ControlPlaneType = "host" + ControlPlaneTypeExternal ControlPlaneType = "external" ) // We do not use ObjectReference as its use is discouraged in favor of a locally defined type. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index fddaad0..0d64d11 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -103,6 +103,16 @@ func (in *ControlPlaneList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ControlPlaneSpec) DeepCopyInto(out *ControlPlaneSpec) { *out = *in + if in.BootstrapSecretRef != nil { + in, out := &in.BootstrapSecretRef, &out.BootstrapSecretRef + *out = new(SecretReference) + **out = **in + } + if in.AdoptedTokenExpirationSeconds != nil { + in, out := &in.AdoptedTokenExpirationSeconds, &out.AdoptedTokenExpirationSeconds + *out = new(int64) + **out = **in + } if in.PostCreateHook != nil { in, out := &in.PostCreateHook, &out.PostCreateHook *out = new(string) diff --git a/chart/crds/crds.yaml b/chart/crds/crds.yaml index 31dfefc..3746113 100644 --- a/chart/crds/crds.yaml +++ b/chart/crds/crds.yaml @@ -54,11 +54,42 @@ spec: spec: description: ControlPlaneSpec defines the desired state of ControlPlane properties: + adoptedTokenExpirationSeconds: + description: expiration time for token of adopted cluster + format: int64 + type: integer backend: enum: - shared - dedicated type: string + bootstrapSecretRef: + description: |- + BootstrapSecretRef contains a reference to the kubeconfig used to bootstrap adoption of + an external cluster + properties: + inClusterKey: + description: Required + type: string + key: + description: Required + type: string + name: + description: |- + `name` is the name of the secret. + Required + type: string + namespace: + description: |- + `namespace` is the namespace of the secret. + Required + type: string + required: + - inClusterKey + - key + - name + - namespace + type: object postCreateHook: type: string postCreateHookVars: @@ -71,6 +102,7 @@ spec: - ocm - vcluster - host + - external type: string type: object status: diff --git a/chart/templates/operator.yaml b/chart/templates/operator.yaml index bac33d9..1df4830 100644 --- a/chart/templates/operator.yaml +++ b/chart/templates/operator.yaml @@ -591,7 +591,7 @@ spec: - --secure-listen-address=0.0.0.0:8443 - --upstream=http://127.0.0.1:8080/ - --logtostderr=true - - --v={{.Values.verbosity}} + - --v=0 image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1 name: kube-rbac-proxy ports: @@ -614,12 +614,13 @@ spec: - --health-probe-bind-address=:8081 - --metrics-bind-address=127.0.0.1:8080 - --leader-elect + - --zap-log-level={{max (.Values.verbosity | default 2 | int) 1}} env: - name: HELM_CONFIG_HOME value: /tmp - name: HELM_CACHE_HOME value: /tmp - image: ghcr.io/kubestellar/kubeflex/manager:latest + image: ko.local/manager:3c7e658 imagePullPolicy: IfNotPresent livenessProbe: httpGet: diff --git a/cmd/kflex/adopt/adopt.go b/cmd/kflex/adopt/adopt.go new file mode 100644 index 0000000..3f24a49 --- /dev/null +++ b/cmd/kflex/adopt/adopt.go @@ -0,0 +1,224 @@ +/* +Copyright 2024 The KubeStellar Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package adopt + +import ( + "context" + "fmt" + "os" + "strings" + "sync" + + "path/filepath" + + homedir "github.com/mitchellh/go-homedir" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd/api" + "sigs.k8s.io/controller-runtime/pkg/client" + + tenancyv1alpha1 "github.com/kubestellar/kubeflex/api/v1alpha1" + "github.com/kubestellar/kubeflex/cmd/kflex/common" + cont "github.com/kubestellar/kubeflex/cmd/kflex/ctx" + kfclient "github.com/kubestellar/kubeflex/pkg/client" + "github.com/kubestellar/kubeflex/pkg/util" +) + +type CPAdopt struct { + common.CP + AdoptedKubeconfig string + AdoptedContext string + AdoptedURLOverride string + AdoptedTokenExpirationSeconds int + SkipURLOverride bool +} + +// Adopt a control plane from another cluster +func (c *CPAdopt) Adopt(hook string, hookVars []string, chattyStatus bool) { + done := make(chan bool) + var wg sync.WaitGroup + cx := cont.CPCtx{} + cx.Context(chattyStatus, false, false, false) + + controlPlaneType := tenancyv1alpha1.ControlPlaneTypeExternal + util.PrintStatus(fmt.Sprintf("Adopting control plane %s of type %s ...", c.Name, controlPlaneType), done, &wg, chattyStatus) + + adoptedKubeconfig := getAdoptedKubeconfig(c) + + clp, err := kfclient.GetClient(c.Kubeconfig) + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting kubeflex client: %v\n", err) + os.Exit(1) + } + cl := *clp + + clientsetp, err := kfclient.GetClientSet(c.Kubeconfig) + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting clientset: %v\n", err) + os.Exit(1) + } + + if err := applyAdoptedBootstrapSecret(clientsetp, c.Name, adoptedKubeconfig, c.AdoptedContext, c.AdoptedURLOverride, c.SkipURLOverride); err != nil { + fmt.Fprintf(os.Stderr, "error creating adopted cluster kubeconfig: %v\n", err) + os.Exit(1) + } + + cp := common.GenerateControlPlane(c.Name, string(controlPlaneType), "", hook, hookVars) + + if err := cl.Create(context.TODO(), cp, &client.CreateOptions{}); err != nil { + fmt.Fprintf(os.Stderr, "Error creating ControlPlane object: %v\n", err) + os.Exit(1) + } + + done <- true + wg.Wait() +} + +func applyAdoptedBootstrapSecret(clientset *kubernetes.Clientset, cpName, adoptedKubeconfig, contextName, adoptedURLOverride string, skipURLOverride bool) error { + // Load the kubeconfig from file + config, err := clientcmd.LoadFromFile(adoptedKubeconfig) + if err != nil { + return fmt.Errorf("failed to load kubeconfig file %s: %v", adoptedKubeconfig, err) + } + + // Retrieve the specified context + context, exists := config.Contexts[contextName] + if !exists { + return fmt.Errorf("context %s not found in the kubeconfig", contextName) + } + + // Retrieve the associated cluster + cluster, exists := config.Clusters[context.Cluster] + if !exists { + return fmt.Errorf("cluster %s not found for context %s", context.Cluster, contextName) + } + + // Construct a new kubeConfig object + kubeConfig := api.NewConfig() + + kubeConfig.Clusters[context.Cluster] = cluster + + if !skipURLOverride { + // Determine the server endpoint + endpoint := adoptedURLOverride + if endpoint == "" { + endpoint = cluster.Server + if !isValidServerURL(endpoint) { + return fmt.Errorf("invalid server endpoint %s. Please provide a valid value with the `url-override` option", endpoint) + } + } + kubeConfig.Clusters[context.Cluster].Server = endpoint + } + + if authInfo, exists := config.AuthInfos[context.AuthInfo]; exists { + kubeConfig.AuthInfos[contextName] = authInfo + } else { + return fmt.Errorf("authInfo %s not found for context %s", context.AuthInfo, contextName) + } + + kubeConfig.Contexts[contextName] = &api.Context{ + Cluster: context.Cluster, + AuthInfo: contextName, + } + kubeConfig.CurrentContext = contextName + + newKubeConfig, err := clientcmd.Write(*kubeConfig) + if err != nil { + return fmt.Errorf("failed to serialize the new kubeconfig: %v", err) + } + + createOrUpdateSecret(clientset, cpName, newKubeConfig) + + return nil +} + +func createOrUpdateSecret(clientset *kubernetes.Clientset, cpName string, kubeconfig []byte) error { + + // Define the kubeconfig secret + kubeConfigSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: util.GenerateBoostrapSecretName(cpName), + Namespace: util.SystemNamespace, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{util.KubeconfigSecretKeyInCluster: kubeconfig}, + } + + // Try to create the secret + if _, err := clientset.CoreV1().Secrets(util.SystemNamespace).Create(context.TODO(), kubeConfigSecret, metav1.CreateOptions{}); err != nil { + // Check if the error is because the secret already exists + if apierrors.IsAlreadyExists(err) { + // Retrieve the existing secret + existingSecret, getErr := clientset.CoreV1().Secrets(util.SystemNamespace).Get(context.TODO(), util.AdminConfSecret, metav1.GetOptions{}) + if getErr != nil { + return fmt.Errorf("failed to fetch existing secret %s in namespace %s: %v", util.AdminConfSecret, util.SystemNamespace, getErr) + } + + // Update the data of the existing secret + existingSecret.Data = kubeConfigSecret.Data + + // Update the secret with new data + if _, updateErr := clientset.CoreV1().Secrets(util.SystemNamespace).Update(context.TODO(), existingSecret, metav1.UpdateOptions{}); updateErr != nil { + return fmt.Errorf("failed to update existing secret %s in namespace %s: %v", util.AdminConfSecret, util.SystemNamespace, updateErr) + } + } else { + return fmt.Errorf("failed to create secret %s in namespace %s: %v", util.AdminConfSecret, util.SystemNamespace, err) + } + } + + return nil +} + +// check if the current server URL in the adopted cluster kubeconfig is using +// a local address, which would not work in a container +func isValidServerURL(serverURL string) bool { + localAddresses := []string{"127.0.0.1", "localhost", "::1"} + for _, addr := range localAddresses { + if strings.Contains(serverURL, addr) { + return false + } + } + return true +} + +func getAdoptedKubeconfig(c *CPAdopt) string { + if c.AdoptedKubeconfig != "" { + return c.AdoptedKubeconfig + } + if c.Kubeconfig != "" { + return c.Kubeconfig + } + return getKubeConfigFromEnv(c.Kubeconfig) +} + +func getKubeConfigFromEnv(kubeconfig string) string { + if kubeconfig == "" { + kubeconfig = os.Getenv("KUBECONFIG") + if kubeconfig == "" { + home, err := homedir.Dir() + if err != nil { + fmt.Fprintf(os.Stderr, "Error finding home directory: %v\n", err) + os.Exit(1) + } + kubeconfig = filepath.Join(home, ".kube", "config") + } + } + return kubeconfig +} diff --git a/cmd/kflex/common/cp.go b/cmd/kflex/common/cp.go index c3097db..5185a5c 100644 --- a/cmd/kflex/common/cp.go +++ b/cmd/kflex/common/cp.go @@ -18,6 +18,12 @@ package common import ( "context" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + tenancyv1alpha1 "github.com/kubestellar/kubeflex/api/v1alpha1" + "github.com/kubestellar/kubeflex/pkg/util" ) type CP struct { @@ -25,3 +31,40 @@ type CP struct { Kubeconfig string Name string } + +func GenerateControlPlane(name, controlPlaneType, backendType, hook string, hookVars []string) *tenancyv1alpha1.ControlPlane { + cp := &tenancyv1alpha1.ControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: tenancyv1alpha1.ControlPlaneSpec{ + Type: tenancyv1alpha1.ControlPlaneType(controlPlaneType), + Backend: tenancyv1alpha1.BackendDBType(backendType), + }, + } + if hook != "" { + cp.Spec.PostCreateHook = &hook + cp.Spec.PostCreateHookVars = convertToMap(hookVars) + } + if controlPlaneType == string(tenancyv1alpha1.ControlPlaneTypeExternal) { + cp.Spec.BootstrapSecretRef = &tenancyv1alpha1.SecretReference{ + Name: util.GenerateBoostrapSecretName(name), + Namespace: util.SystemNamespace, + InClusterKey: util.KubeconfigSecretKeyInCluster, + } + } + return cp +} + +func convertToMap(pairs []string) map[string]string { + params := make(map[string]string) + + for _, pair := range pairs { + split := strings.SplitN(pair, "=", 2) + if len(split) == 2 { + params[split[0]] = split[1] + } + } + + return params +} diff --git a/cmd/kflex/create/create.go b/cmd/kflex/create/create.go index 3866cc7..329ec6d 100644 --- a/cmd/kflex/create/create.go +++ b/cmd/kflex/create/create.go @@ -20,11 +20,9 @@ import ( "context" "fmt" "os" - "strings" "sync" tenancyv1alpha1 "github.com/kubestellar/kubeflex/api/v1alpha1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kubestellar/kubeflex/cmd/kflex/common" @@ -52,7 +50,7 @@ func (c *CPCreate) Create(controlPlaneType, backendType, hook string, hookVars [ } cl := *clp - cp := c.generateControlPlane(controlPlaneType, backendType, hook, hookVars) + cp := common.GenerateControlPlane(c.Name, controlPlaneType, backendType, hook, hookVars) util.PrintStatus(fmt.Sprintf("Creating new control plane %s of type %s ...", c.Name, controlPlaneType), done, &wg, chattyStatus) if err := cl.Create(context.TODO(), cp, &client.CreateOptions{}); err != nil { @@ -104,33 +102,3 @@ func (c *CPCreate) Create(controlPlaneType, backendType, hook string, hookVars [ wg.Wait() } - -func (c *CPCreate) generateControlPlane(controlPlaneType, backendType, hook string, hookVars []string) *tenancyv1alpha1.ControlPlane { - cp := &tenancyv1alpha1.ControlPlane{ - ObjectMeta: v1.ObjectMeta{ - Name: c.Name, - }, - Spec: tenancyv1alpha1.ControlPlaneSpec{ - Type: tenancyv1alpha1.ControlPlaneType(controlPlaneType), - Backend: tenancyv1alpha1.BackendDBType(backendType), - }, - } - if hook != "" { - cp.Spec.PostCreateHook = &hook - cp.Spec.PostCreateHookVars = convertToMap(hookVars) - } - return cp -} - -func convertToMap(pairs []string) map[string]string { - params := make(map[string]string) - - for _, pair := range pairs { - split := strings.SplitN(pair, "=", 2) - if len(split) == 2 { - params[split[0]] = split[1] - } - } - - return params -} diff --git a/cmd/kflex/ctx/ctx.go b/cmd/kflex/ctx/ctx.go index 138696c..83f0c52 100644 --- a/cmd/kflex/ctx/ctx.go +++ b/cmd/kflex/ctx/ctx.go @@ -29,7 +29,7 @@ import ( kfclient "github.com/kubestellar/kubeflex/pkg/client" "github.com/kubestellar/kubeflex/pkg/kubeconfig" "github.com/kubestellar/kubeflex/pkg/util" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/clientcmd/api" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -132,7 +132,7 @@ func (c *CPCtx) loadAndMergeFromServer(kconfig *api.Config) error { kfcClient := *kfcClientp cp := &tenancyv1alpha1.ControlPlane{ - ObjectMeta: v1.ObjectMeta{ + ObjectMeta: metav1.ObjectMeta{ Name: c.CP.Name, }, } diff --git a/cmd/kflex/delete/delete.go b/cmd/kflex/delete/delete.go index fa4188c..29eebc7 100644 --- a/cmd/kflex/delete/delete.go +++ b/cmd/kflex/delete/delete.go @@ -23,7 +23,7 @@ import ( "sync" tenancyv1alpha1 "github.com/kubestellar/kubeflex/api/v1alpha1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -39,7 +39,11 @@ type CPDelete struct { func (c *CPDelete) Delete(chattyStatus bool) { done := make(chan bool) - cp := c.generateControlPlane() + cp := &tenancyv1alpha1.ControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: c.Name, + }, + } var wg sync.WaitGroup util.PrintStatus(fmt.Sprintf("Deleting control plane %s...", c.Name), done, &wg, chattyStatus) @@ -88,11 +92,3 @@ func (c *CPDelete) Delete(chattyStatus bool) { done <- true wg.Wait() } - -func (c *CPDelete) generateControlPlane() *tenancyv1alpha1.ControlPlane { - return &tenancyv1alpha1.ControlPlane{ - ObjectMeta: v1.ObjectMeta{ - Name: c.Name, - }, - } -} diff --git a/cmd/kflex/main.go b/cmd/kflex/main.go index af54f87..aef9c0b 100644 --- a/cmd/kflex/main.go +++ b/cmd/kflex/main.go @@ -26,6 +26,7 @@ import ( "github.com/go-logr/logr" "github.com/go-logr/zapr" tenancyv1alpha1 "github.com/kubestellar/kubeflex/api/v1alpha1" + "github.com/kubestellar/kubeflex/cmd/kflex/adopt" "github.com/kubestellar/kubeflex/cmd/kflex/common" cr "github.com/kubestellar/kubeflex/cmd/kflex/create" cont "github.com/kubestellar/kubeflex/cmd/kflex/ctx" @@ -53,6 +54,11 @@ var hookVars []string var hostContainer string var overwriteExistingCtx bool var setCurrentCtxAsHosting bool +var adoptedKubeconfig string +var adoptedContext string +var adoptedURLOverride string +var adoptedTokenExpirationSeconds int +var skipURLOverride bool // defaults const BKTypeDefault = string(tenancyv1alpha1.BackendDBTypeShared) @@ -116,7 +122,7 @@ var initCmd = &cobra.Command{ } var createCmd = &cobra.Command{ - Use: "create", + Use: "create ", Short: "Create a control plane instance", Long: `Create a control plane instance and switches the Kubeconfig context to the current instance`, @@ -140,8 +146,32 @@ var createCmd = &cobra.Command{ }, } +var adoptCmd = &cobra.Command{ + Use: "adopt ", + Short: "Adopt a control plane from an external cluster", + Long: `Adopt a control plane from an external cluster and switches the Kubeconfig context to + the current instance`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + cp := adopt.CPAdopt{ + CP: common.CP{ + Ctx: createContext(), + Name: args[0], + Kubeconfig: kubeconfig, + }, + AdoptedKubeconfig: adoptedKubeconfig, + AdoptedContext: adoptedContext, + AdoptedURLOverride: adoptedURLOverride, + AdoptedTokenExpirationSeconds: adoptedTokenExpirationSeconds, + SkipURLOverride: skipURLOverride, + } + // create passing the control plane type and backend type + cp.Adopt(Hook, hookVars, chattyStatus) + }, +} + var deleteCmd = &cobra.Command{ - Use: "delete", + Use: "delete ", Short: "Delete a control plane instance", Long: `Delete a control plane instance and switches the context back to the hosting cluster context`, @@ -201,6 +231,17 @@ func init() { createCmd.Flags().BoolVarP(&chattyStatus, "chatty-status", "s", true, "chatty status indicator") createCmd.Flags().StringArrayVarP(&hookVars, "set", "e", []string{}, "set post create hook variables, in the form name=value ") + adoptCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "path to kubeconfig file") + adoptCmd.Flags().IntVarP(&verbosity, "verbosity", "v", 0, "log level") // TODO - figure out how to inject verbosity + adoptCmd.Flags().StringVarP(&Hook, "postcreate-hook", "p", "", "name of post create hook to run") + adoptCmd.Flags().BoolVarP(&chattyStatus, "chatty-status", "s", true, "chatty status indicator") + adoptCmd.Flags().BoolVarP(&skipURLOverride, "skip-url-override", "d", false, "skip URL override, used for local debugging only") + adoptCmd.Flags().StringArrayVarP(&hookVars, "set", "e", []string{}, "set post create hook variables, in the form name=value ") + adoptCmd.Flags().StringVarP(&adoptedKubeconfig, "adopted-kubeconfig", "a", "", "path to adopted cluster kubeconfig file. If unspecified, it uses the default Kubeconfig") + adoptCmd.Flags().StringVarP(&adoptedContext, "adopted-context", "c", "", "path to adopted cluster context in adopted kubeconfig") + adoptCmd.Flags().StringVarP(&adoptedURLOverride, "url-override", "u", "", "URL overrride for adopted cluster. Required when cluster address uses local host address, e.g. `https://127.0.0.1` ") + adoptCmd.Flags().IntVarP(&adoptedTokenExpirationSeconds, "expiration-seconds", "x", 86400*365, "adopted token expiration in seconds. Default is one year.") + deleteCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "path to kubeconfig file") deleteCmd.Flags().IntVarP(&verbosity, "verbosity", "v", 0, "log level") // TODO - figure out how to inject verbosity deleteCmd.Flags().BoolVarP(&chattyStatus, "chatty-status", "s", true, "chatty status indicator") @@ -214,6 +255,7 @@ func init() { rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(initCmd) rootCmd.AddCommand(createCmd) + rootCmd.AddCommand(adoptCmd) rootCmd.AddCommand(deleteCmd) rootCmd.AddCommand(ctxCmd) } diff --git a/config/crd/bases/tenancy.kflex.kubestellar.org_controlplanes.yaml b/config/crd/bases/tenancy.kflex.kubestellar.org_controlplanes.yaml index a7b30a4..cc74ac8 100644 --- a/config/crd/bases/tenancy.kflex.kubestellar.org_controlplanes.yaml +++ b/config/crd/bases/tenancy.kflex.kubestellar.org_controlplanes.yaml @@ -55,11 +55,42 @@ spec: spec: description: ControlPlaneSpec defines the desired state of ControlPlane properties: + adoptedTokenExpirationSeconds: + description: expiration time for token of adopted cluster + format: int64 + type: integer backend: enum: - shared - dedicated type: string + bootstrapSecretRef: + description: |- + BootstrapSecretRef contains a reference to the kubeconfig used to bootstrap adoption of + an external cluster + properties: + inClusterKey: + description: Required + type: string + key: + description: Required + type: string + name: + description: |- + `name` is the name of the secret. + Required + type: string + namespace: + description: |- + `namespace` is the namespace of the secret. + Required + type: string + required: + - inClusterKey + - key + - name + - namespace + type: object postCreateHook: type: string postCreateHookVars: @@ -72,6 +103,7 @@ spec: - ocm - vcluster - host + - external type: string type: object status: diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 9fc6767..2d50887 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -5,5 +5,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: controller - newName: ghcr.io/kubestellar/kubeflex/manager - newTag: latest + newName: ko.local/manager + newTag: 3c7e658 diff --git a/docs/users.md b/docs/users.md index 40ff969..382b504 100644 --- a/docs/users.md +++ b/docs/users.md @@ -280,6 +280,8 @@ clusters registration and support for the [`ManifestWork` API](https://open-clus - vcluster: this is based on the [vcluster project](https://www.vcluster.com) and provides the ability to create pods in the hosting namespace of the hosting cluster. - host: this control plane type exposes the underlying hosting cluster with the same control plane abstraction used by the other control plane types. +- external: this control plane type exposes an external cluster with the same control plane abstraction +used by the other control plane types. ## Control Plane Backends @@ -314,6 +316,12 @@ To create a control plane of type `host` run the command: kflex create cp4 --type host ``` +To create a control plane of type `external` with the required options, run the command: + +```shell +kflex adopt --adopted-context cp5 +``` + ## Working with an OCM control plane Let's create an OCM control plane: @@ -522,6 +530,63 @@ vcluster-0 2/2 Running 0 The nginx pod is the one with the name `nginx-x-default-x-vcluster`. +## Working with an external control plane + +In this section, we will show an example of creating an external control plane to integrate +a kind cluster named `ext1` into the same Docker network as your Kubeflex hosting cluster. This setup +requires that both clusters are accessible from each other within the shared Docker network. + +### Prerequisites +Ensure that both your hosting and adopted clusters are on the same Docker network. For +clusters using the default `kind` docker network, execute the following command to +verify connectivity: + +```shell +docker network inspect kind | jq '.[].Containers | to_entries[] | .value.Name' +``` + +The output will list the container names for your clusters. For example: + +```shell +"ext1-control-plane" represents the external cluster you wish to adopt. +"kubeflex-control-plane" represents the Kubeflex hosting cluster. +``` + +Once verified, the endpoint for the adopted cluster should be set as follows: + +```shell +https://ext1-control-plane:6443 +``` + +If you're not utilizing the default `kind` network, you'll need to identify which Docker networks are available. List all networks with this command: + +```shell +docker network ls +``` + +Inspect any specific network to confirm its configuration using docker network inspect . + +### Adopting the external cluster + +To set up the external cluster ext1 as a control plane named cpe, use the following command: + +```shell +$ kflex adopt -c kind-ext1 -u https://ext1-control-plane:6443 +✔ Checking for saved hosting cluster context... +✔ Switching to hosting cluster context... +✔ Adopting control plane cpe of type external ... +``` + +where `kind-ext1` is the context name for the `ext1` cluster found in your current kubeconfig. +and `https://ext1-control-plane:6443` is the endpoint you previously determined for the +external control plane. + +### External clusters on a different host + +If the external cluster operates on a different host and your kubeconfig context +contains an endpoint for that cluster which is not using a loopback interface, +specifying an override URL is unnecessary. + ## Post-create hooks With post-create hooks you can automate applying kubernetes templates on the hosting cluster or on diff --git a/internal/controller/controlplane_controller.go b/internal/controller/controlplane_controller.go index 59648a0..631b5ab 100644 --- a/internal/controller/controlplane_controller.go +++ b/internal/controller/controlplane_controller.go @@ -33,6 +33,7 @@ import ( clog "sigs.k8s.io/controller-runtime/pkg/log" tenancyv1alpha1 "github.com/kubestellar/kubeflex/api/v1alpha1" + "github.com/kubestellar/kubeflex/pkg/reconcilers/external" "github.com/kubestellar/kubeflex/pkg/reconcilers/host" "github.com/kubestellar/kubeflex/pkg/reconcilers/k8s" "github.com/kubestellar/kubeflex/pkg/reconcilers/ocm" @@ -154,6 +155,9 @@ func (r *ControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.Request case tenancyv1alpha1.ControlPlaneTypeHost: reconciler := host.New(r.Client, r.Scheme, r.Version, r.ClientSet, r.DynamicClient) return reconciler.Reconcile(ctx, hcp) + case tenancyv1alpha1.ControlPlaneTypeExternal: + reconciler := external.New(r.Client, r.Scheme, r.Version, r.ClientSet, r.DynamicClient) + return reconciler.Reconcile(ctx, hcp) default: return ctrl.Result{}, fmt.Errorf("unsupported control plane type: %s", hcp.Spec.Type) } diff --git a/pkg/kubeconfig/kubeconfig.go b/pkg/kubeconfig/kubeconfig.go index 3039834..3504af7 100644 --- a/pkg/kubeconfig/kubeconfig.go +++ b/pkg/kubeconfig/kubeconfig.go @@ -22,6 +22,7 @@ import ( "time" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/util/wait" @@ -141,6 +142,36 @@ func WatchForSecretCreation(clientset kubernetes.Clientset, controlPlaneName, se return nil } +func WaitForNamespaceReady(ctx context.Context, clientset kubernetes.Interface, controlPlaneName string) error { + namespace := util.GenerateNamespaceFromControlPlaneName(controlPlaneName) + + err := wait.PollUntilContextTimeout( + ctx, + 2*time.Second, + 2*time.Minute, + true, + func(context.Context) (bool, error) { + ns, err := clientset.CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{}) + if errors.IsNotFound(err) { + return false, nil // Retry if namespace is not found + } else if err != nil { + return false, fmt.Errorf("error checking namespace status: %v", err) + } + + if ns.Status.Phase == v1.NamespaceActive { + return true, nil // Namespace is ready + } + + return false, nil // Continue waiting + }, + ) + + if err != nil { + return fmt.Errorf("timed out waiting for namespace %s to be ready: %v", namespace, err) + } + return nil +} + func adjustConfigKeys(config *clientcmdapi.Config, cpName, controlPlaneType string) { switch controlPlaneType { case string(tenancyv1alpha1.ControlPlaneTypeOCM): diff --git a/pkg/reconcilers/external/kubeconfig.go b/pkg/reconcilers/external/kubeconfig.go new file mode 100644 index 0000000..42181fc --- /dev/null +++ b/pkg/reconcilers/external/kubeconfig.go @@ -0,0 +1,304 @@ +/* +Copyright 2024 The KubeStellar Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package external + +import ( + "context" + "fmt" + + authenticationv1 "k8s.io/api/authentication/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd/api" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + clog "sigs.k8s.io/controller-runtime/pkg/log" + + tenancyv1alpha1 "github.com/kubestellar/kubeflex/api/v1alpha1" + "github.com/kubestellar/kubeflex/pkg/util" +) + +const ( + adoptedClusterSAName = "kubeflex" + adoptedClusterSANamespace = "kube-system" +) + +var ( + defaultAdoptedTokenExpirationSeconds int64 = 365 * 86400 +) + +func (r *ExternalReconciler) ReconcileKubeconfigFromBoostrapSecret(ctx context.Context, hcp *tenancyv1alpha1.ControlPlane) error { + _ = clog.FromContext(ctx) + + // do not reconcile if kubeconfig secret is already present + if r.IsKubeconfigSecretPresent(ctx, *hcp) { + return nil + } + + bootstrapApiConfig, err := getKubeconfigFromBoostrapSecret(r.Client, ctx, hcp) + if err != nil { + return err + } + + bootstrapRestConfig, err := clientcmd.NewDefaultClientConfig(*bootstrapApiConfig, &clientcmd.ConfigOverrides{}).ClientConfig() + if err != nil { + return err + } + + aClientset, err := kubernetes.NewForConfig(bootstrapRestConfig) + if err != nil { + return err + } + + if err = reconcileAdoptedClusterRoleBinding(aClientset, adoptedClusterSAName, adoptedClusterSANamespace); err != nil { + return fmt.Errorf("error creating ClusterRoleBinding on the adopted cluster: %v", err) + } + + if err = reconcileAdoptedServiceAccount(aClientset.CoreV1().ServiceAccounts(adoptedClusterSANamespace), adoptedClusterSAName); err != nil { + return fmt.Errorf("error creating ServiceAccount on the adopted cluster: %v", err) + } + + bearerToken, err := requestTokenWithExpiration(aClientset.CoreV1().ServiceAccounts(adoptedClusterSANamespace), adoptedClusterSAName, hcp.Spec.AdoptedTokenExpirationSeconds) + if err != nil { + return fmt.Errorf("error requesting token from the adopted cluster: %v", err) + } + + newKubeConfig, err := createNewKubeConfig(bootstrapApiConfig, bearerToken, hcp) + if err != nil { + return fmt.Errorf("error creating adopted cluster kubeconfig: %v", err) + } + + if err := r.ReconcileKubeconfigSecret(ctx, *hcp, newKubeConfig); err != nil { + return fmt.Errorf("error creating kubeconfig secret: %v", err) + } + + return deleteBoostrapSecret(r.Client, ctx, hcp) +} + +func getKubeconfigFromBoostrapSecret(crClient client.Client, ctx context.Context, hcp *tenancyv1alpha1.ControlPlane) (*api.Config, error) { + _ = clog.FromContext(ctx) + + if hcp.Spec.BootstrapSecretRef == nil { + return nil, fmt.Errorf("bootstrapSecretRef must be present in the control plane") + } + + bootstrapSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: hcp.Spec.BootstrapSecretRef.Name, + Namespace: hcp.Spec.BootstrapSecretRef.Namespace, + }, + } + + err := crClient.Get(context.TODO(), client.ObjectKeyFromObject(bootstrapSecret), bootstrapSecret, &client.GetOptions{}) + if err != nil { + return nil, err + } + + key := util.DefaultString(hcp.Spec.BootstrapSecretRef.Key, util.KubeconfigSecretKeyInCluster) + + kconfigBytes := bootstrapSecret.Data[key] + if kconfigBytes == nil { + return nil, fmt.Errorf("kubeconfig not found in bootstrap secret for key %s", key) + } + + return clientcmd.Load(kconfigBytes) +} + +func reconcileAdoptedClusterRoleBinding(clientset *kubernetes.Clientset, saName, saNamespace string) error { + name := saName + "-clusterrolebinding" + + // Check if the ClusterRoleBinding already exists + _, err := clientset.RbacV1().ClusterRoleBindings().Get(context.TODO(), name, metav1.GetOptions{}) + if err == nil { + return nil + } + if !apierrors.IsNotFound(err) { + return err + } + + // Define new ClusterRoleBinding + clusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "cluster-admin", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: saName, + Namespace: saNamespace, + }, + }, + } + + // Create the ClusterRoleBinding + _, err = clientset.RbacV1().ClusterRoleBindings().Create(context.TODO(), clusterRoleBinding, metav1.CreateOptions{}) + if err != nil { + return err + } + + return nil +} + +func reconcileAdoptedServiceAccount(saClient v1.ServiceAccountInterface, saName string) error { + + // Check if the ServiceAccount already exists + _, err := saClient.Get(context.TODO(), saName, metav1.GetOptions{}) + if err == nil { + return nil + } + if !apierrors.IsNotFound(err) { + return err + } + + // Define a new ServiceAccount + sa := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: saName, + }, + } + + // Create the ServiceAccount + _, err = saClient.Create(context.TODO(), sa, metav1.CreateOptions{}) + if err != nil { + return err + } + + return nil +} + +func requestTokenWithExpiration(saClient v1.ServiceAccountInterface, saName string, expirationSeconds *int64) (string, error) { + if expirationSeconds == nil { + expirationSeconds = &defaultAdoptedTokenExpirationSeconds + } + + tokenRequest := &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + Audiences: []string{}, + ExpirationSeconds: expirationSeconds, + }, + } + + tokenResponse, err := saClient.CreateToken(context.TODO(), saName, tokenRequest, metav1.CreateOptions{}) + if err != nil { + return "", err + } + return tokenResponse.Status.Token, nil +} + +func createNewKubeConfig(bootstrapConfig *api.Config, token string, hcp *tenancyv1alpha1.ControlPlane) ([]byte, error) { + context := bootstrapConfig.Contexts[bootstrapConfig.CurrentContext] + + cluster := bootstrapConfig.Clusters[context.Cluster] + if cluster == nil { + return nil, fmt.Errorf("invalid cluster name %s for adoped cluster", context.Cluster) + } + + kubeConfig := api.NewConfig() + + kubeConfig.Clusters[context.Cluster] = &api.Cluster{ + Server: cluster.Server, + CertificateAuthorityData: cluster.CertificateAuthorityData, + } + + kubeConfig.AuthInfos[hcp.Name] = &api.AuthInfo{ + Token: token, + } + + kubeConfig.Contexts[hcp.Name] = &api.Context{ + Cluster: context.Cluster, + AuthInfo: hcp.Name, + } + kubeConfig.CurrentContext = hcp.Name + + newKubeConfig, err := clientcmd.Write(*kubeConfig) + if err != nil { + return nil, err + } + + return newKubeConfig, nil +} + +func (r *ExternalReconciler) ReconcileKubeconfigSecret(ctx context.Context, cp tenancyv1alpha1.ControlPlane, kubeconfig []byte) error { + namespace := util.GenerateNamespaceFromControlPlaneName(cp.Name) + + // create kubeconfig secret + kubeConfigSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: util.AdminConfSecret, + Namespace: namespace, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{util.KubeconfigSecretKeyInCluster: kubeconfig}, + } + + // Attempt to get the existing kubeconfig secret + err := r.Client.Get(ctx, client.ObjectKeyFromObject(kubeConfigSecret), kubeConfigSecret) + if err != nil { + if apierrors.IsNotFound(err) { + // Set controller reference on new kubeconfig secret + if setErr := controllerutil.SetControllerReference(&cp, kubeConfigSecret, r.Scheme); setErr != nil { + return setErr + } + // Create the kubeconfig secret as it does not exist + return r.Client.Create(ctx, kubeConfigSecret) + } + return err + } + + // Update the existing kubeconfig secret + return r.Client.Update(ctx, kubeConfigSecret) +} + +func deleteBoostrapSecret(crClient client.Client, ctx context.Context, hcp *tenancyv1alpha1.ControlPlane) error { + _ = clog.FromContext(ctx) + + bootstrapSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: hcp.Spec.BootstrapSecretRef.Name, + Namespace: hcp.Spec.BootstrapSecretRef.Namespace, + }, + } + return crClient.Delete(context.TODO(), bootstrapSecret) +} + +func (r *ExternalReconciler) IsKubeconfigSecretPresent(ctx context.Context, cp tenancyv1alpha1.ControlPlane) bool { + namespace := util.GenerateNamespaceFromControlPlaneName(cp.Name) + + kubeConfigSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: util.AdminConfSecret, + Namespace: namespace, + }, + } + + // Attempt to get the existing kubeconfig secret + if err := r.Client.Get(ctx, client.ObjectKeyFromObject(kubeConfigSecret), kubeConfigSecret); err == nil { + return true + } + + return false +} diff --git a/pkg/reconcilers/external/reconciler.go b/pkg/reconcilers/external/reconciler.go new file mode 100644 index 0000000..71a3e69 --- /dev/null +++ b/pkg/reconcilers/external/reconciler.go @@ -0,0 +1,71 @@ +/* +Copyright 2024 The KubeStellar Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package external + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + clog "sigs.k8s.io/controller-runtime/pkg/log" + + tenancyv1alpha1 "github.com/kubestellar/kubeflex/api/v1alpha1" + "github.com/kubestellar/kubeflex/pkg/reconcilers/shared" + "github.com/kubestellar/kubeflex/pkg/util" +) + +// ExternalReconciler reconciles an "external" ControlPlane to adopt an external cluster with the ControlPlane abstraction +type ExternalReconciler struct { + *shared.BaseReconciler +} + +func New(cl client.Client, scheme *runtime.Scheme, version string, clientSet *kubernetes.Clientset, dynamicClient *dynamic.DynamicClient) *ExternalReconciler { + return &ExternalReconciler{ + BaseReconciler: &shared.BaseReconciler{ + Client: cl, + Scheme: scheme, + ClientSet: clientSet, + DynamicClient: dynamicClient, + }, + } +} + +func (r *ExternalReconciler) Reconcile(ctx context.Context, hcp *tenancyv1alpha1.ControlPlane) (ctrl.Result, error) { + _ = clog.FromContext(ctx) + + if err := r.BaseReconciler.ReconcileNamespace(ctx, hcp); err != nil { + return r.UpdateStatusForSyncingError(hcp, err) + } + + if err := r.ReconcileKubeconfigFromBoostrapSecret(ctx, hcp); err != nil { + return r.UpdateStatusForSyncingError(hcp, err) + } + + r.UpdateStatusWithSecretRef(hcp, util.AdminConfSecret, util.KubeconfigSecretKeyDefault, util.KubeconfigSecretKeyInCluster) + + if hcp.Spec.PostCreateHook != nil && + tenancyv1alpha1.HasConditionAvailable(hcp.Status.Conditions) { + if err := r.ReconcileUpdatePostCreateHook(ctx, hcp); err != nil { + return r.UpdateStatusForSyncingError(hcp, err) + } + } + + return r.UpdateStatusForSyncingSuccess(ctx, hcp) +} diff --git a/pkg/reconcilers/host/secret.go b/pkg/reconcilers/host/secret.go index f940e84..0488094 100644 --- a/pkg/reconcilers/host/secret.go +++ b/pkg/reconcilers/host/secret.go @@ -120,7 +120,7 @@ func (r *HostReconciler) getServiceAccountToken(ctx context.Context, hcp *tenanc }, } - if err := r.Client.Get(context.TODO(), client.ObjectKeyFromObject(saSecret), saSecret, &client.GetOptions{}); err != nil { + if err := r.Client.Get(ctx, client.ObjectKeyFromObject(saSecret), saSecret, &client.GetOptions{}); err != nil { return nil, nil, err } diff --git a/pkg/util/status_check.go b/pkg/util/status_check.go index 01f3b02..d6e394f 100644 --- a/pkg/util/status_check.go +++ b/pkg/util/status_check.go @@ -137,7 +137,8 @@ func IsAPIServerDeploymentReady(log logr.Logger, c client.Client, hcp tenancyv1a switch hcp.Spec.Type { case tenancyv1alpha1.ControlPlaneTypeHost: - // host is always available + case tenancyv1alpha1.ControlPlaneTypeExternal: + // host or external is always available return true, nil case tenancyv1alpha1.ControlPlaneTypeVCluster: s := &v1.StatefulSet{ @@ -176,7 +177,7 @@ func IsAPIServerDeploymentReady(log logr.Logger, c client.Client, hcp tenancyv1a return true, nil } default: - log.Info("control plane type not supported", "type", hcp.Spec.Type) + log.Error(fmt.Errorf("control plane type not supported"), "type", hcp.Spec.Type) return false, nil } diff --git a/pkg/util/util.go b/pkg/util/util.go index 6c99068..eb06f23 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -146,3 +146,14 @@ func ZeroFields(obj runtime.Object) runtime.Object { return zeroed } + +func DefaultString(value, defaultValue string) string { + if value == "" { + return defaultValue + } + return value +} + +func GenerateBoostrapSecretName(cpName string) string { + return fmt.Sprintf("%s-bootstrap", cpName) +}