From 8567ddf749ed3937d5d31c547fd35fe91a64b12d Mon Sep 17 00:00:00 2001 From: Kostiantyn Kalynovskyi Date: Mon, 26 Oct 2020 16:23:22 -0500 Subject: [PATCH] Add support for custom kubeconfig contexts This commit extends cluster map interface to be able to specify a custom kubeconfig context per cluster in ClusterMap Related-To: #380 Related-To: #375 Closes: #380 Change-Id: I9a8a26e3a3666e069c243e871f89ae9222228f17 --- pkg/api/v1alpha1/cluster_map_types.go | 2 ++ pkg/cluster/clustermap/map.go | 19 +++++++++++++++++++ pkg/cluster/clustermap/map_test.go | 19 +++++++++++++++++++ pkg/clusterctl/client/executor.go | 23 ++++++++++++++++++++--- pkg/clusterctl/client/executor_test.go | 7 +++++++ pkg/k8s/applier/executor.go | 12 ++++++++++-- pkg/k8s/applier/executor_test.go | 21 +++++++++++++++++++++ 7 files changed, 98 insertions(+), 5 deletions(-) diff --git a/pkg/api/v1alpha1/cluster_map_types.go b/pkg/api/v1alpha1/cluster_map_types.go index e003ed78b..33b13f717 100644 --- a/pkg/api/v1alpha1/cluster_map_types.go +++ b/pkg/api/v1alpha1/cluster_map_types.go @@ -35,6 +35,8 @@ type Cluster struct { // DynamicKubeConfig kubeconfig allows to get kubeconfig from parent cluster, instead // expecting it to be in document bundle. Parent kubeconfig will be used to get kubeconfig DynamicKubeConfig bool `json:"dynamicKubeConf,omitempty"` + // KubeconfigContext is the context in kubeconfig, default is equals to clusterMap key + KubeconfigContext string `json:"kubeconfigContext,omitempty"` } // DefaultClusterMap can be used to safely unmarshal ClusterMap object without nil pointers diff --git a/pkg/cluster/clustermap/map.go b/pkg/cluster/clustermap/map.go index 37c0d6a39..3550479b0 100644 --- a/pkg/cluster/clustermap/map.go +++ b/pkg/cluster/clustermap/map.go @@ -27,6 +27,7 @@ type ClusterMap interface { AllClusters() []string DynamicKubeConfig(string) bool ClusterNamespace(string) (string, error) + ClusterKubeconfigContext(string) (string, error) } // clusterMap allows to view clusters and relationship between them @@ -77,3 +78,21 @@ func (cm clusterMap) AllClusters() []string { func (cm clusterMap) ClusterNamespace(clusterName string) (string, error) { return "default", nil } + +// ClusterNamespace a namespace for given cluster +// TODO implement how to get namespace for cluster +func (cm clusterMap) ClusterKubeconfigContext(clusterName string) (string, error) { + cluster, exists := cm.apiMap.Map[clusterName] + + if !exists { + return "", ErrClusterNotInMap{Map: cm.apiMap, Child: clusterName} + } + + kubeContext := cluster.KubeconfigContext + // if kubeContext is still empty, set it to clusterName + if kubeContext == "" { + kubeContext = clusterName + } + + return kubeContext, nil +} diff --git a/pkg/cluster/clustermap/map_test.go b/pkg/cluster/clustermap/map_test.go index fdd8af958..955ba6f2a 100644 --- a/pkg/cluster/clustermap/map_test.go +++ b/pkg/cluster/clustermap/map_test.go @@ -28,6 +28,7 @@ func TestClusterMap(t *testing.T) { targetCluster := "target" ephemeraCluster := "ephemeral" workloadCluster := "workload" + workloadClusterKubeconfigContext := "different-workload-context" workloadClusterNoParent := "workload without parent" apiMap := &v1alpha1.ClusterMap{ Map: map[string]*v1alpha1.Cluster{ @@ -39,6 +40,7 @@ func TestClusterMap(t *testing.T) { workloadCluster: { Parent: targetCluster, DynamicKubeConfig: true, + KubeconfigContext: workloadClusterKubeconfigContext, }, workloadClusterNoParent: { DynamicKubeConfig: true, @@ -87,4 +89,21 @@ func TestClusterMap(t *testing.T) { clusters := cMap.AllClusters() assert.Len(t, clusters, 4) }) + + t.Run("kubeconfig context", func(t *testing.T) { + kubeContext, err := cMap.ClusterKubeconfigContext(workloadCluster) + assert.NoError(t, err) + assert.Equal(t, workloadClusterKubeconfigContext, kubeContext) + }) + + t.Run("kubeconfig default context", func(t *testing.T) { + kubeContext, err := cMap.ClusterKubeconfigContext(targetCluster) + assert.NoError(t, err) + assert.Equal(t, targetCluster, kubeContext) + }) + + t.Run("kubeconfig context error", func(t *testing.T) { + _, err := cMap.ClusterKubeconfigContext("does not exist") + assert.Error(t, err) + }) } diff --git a/pkg/clusterctl/client/executor.go b/pkg/clusterctl/client/executor.go index fd2564d65..80d83d6cc 100644 --- a/pkg/clusterctl/client/executor.go +++ b/pkg/clusterctl/client/executor.go @@ -95,7 +95,17 @@ func (c *ClusterctlExecutor) move(opts ifc.RunOptions, evtCh chan events.Event) return } defer cleanup() - fromContext, err := c.clusterMap.ParentCluster(c.clusterName) + fromCluster, err := c.clusterMap.ParentCluster(c.clusterName) + if err != nil { + c.handleErr(err, evtCh) + return + } + fromContext, err := c.clusterMap.ClusterKubeconfigContext(fromCluster) + if err != nil { + c.handleErr(err, evtCh) + return + } + toContext, err := c.clusterMap.ClusterKubeconfigContext(c.clusterName) if err != nil { c.handleErr(err, evtCh) return @@ -104,7 +114,7 @@ func (c *ClusterctlExecutor) move(opts ifc.RunOptions, evtCh chan events.Event) log.Print("command 'clusterctl move' is going to be executed") // TODO (kkalynovskyi) add more details to dry-run, for now if dry run is set we skip move command if !opts.DryRun { - err = c.Move(kubeConfigFile, fromContext, kubeConfigFile, c.clusterName, ns) + err = c.Move(kubeConfigFile, fromContext, kubeConfigFile, toContext, ns) if err != nil { c.handleErr(err, evtCh) } @@ -138,8 +148,15 @@ func (c *ClusterctlExecutor) init(opts ifc.RunOptions, evtCh chan events.Event) }) return } + + context, err := c.clusterMap.ClusterKubeconfigContext(c.clusterName) + if err != nil { + c.handleErr(err, evtCh) + return + } + // Use cluster name as context in kubeconfig file - err = c.Init(kubeConfigFile, c.clusterName) + err = c.Init(kubeConfigFile, context) if err != nil { c.handleErr(err, evtCh) } diff --git a/pkg/clusterctl/client/executor_test.go b/pkg/clusterctl/client/executor_test.go index 7ac228474..54dd6616a 100644 --- a/pkg/clusterctl/client/executor_test.go +++ b/pkg/clusterctl/client/executor_test.go @@ -26,6 +26,8 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" + "opendev.org/airship/airshipctl/pkg/api/v1alpha1" + "opendev.org/airship/airshipctl/pkg/cluster/clustermap" cctlclient "opendev.org/airship/airshipctl/pkg/clusterctl/client" "opendev.org/airship/airshipctl/pkg/config" "opendev.org/airship/airshipctl/pkg/document" @@ -104,6 +106,7 @@ func TestExecutorRun(t *testing.T) { fs document.FileSystem bundlePath string expectedEvt []events.Event + clusterMap clustermap.ClusterMap }{ { name: "Error unknown action", @@ -112,6 +115,7 @@ func TestExecutorRun(t *testing.T) { expectedEvt: []events.Event{ wrapError(cctlclient.ErrUnknownExecutorAction{Action: "someAction"}), }, + clusterMap: clustermap.NewClusterMap(v1alpha1.DefaultClusterMap()), }, { name: "Error temporary file", @@ -128,6 +132,7 @@ func TestExecutorRun(t *testing.T) { }), wrapError(errTmpFile), }, + clusterMap: clustermap.NewClusterMap(v1alpha1.DefaultClusterMap()), }, { name: "Regular Run init", @@ -143,6 +148,7 @@ func TestExecutorRun(t *testing.T) { MockRemoveAll: func() error { return nil }, }, bundlePath: "testdata/executor_init", + clusterMap: clustermap.NewClusterMap(v1alpha1.DefaultClusterMap()), expectedEvt: []events.Event{ events.NewEvent().WithClusterctlEvent(events.ClusterctlEvent{ Operation: events.ClusterctlInitStart, @@ -166,6 +172,7 @@ func TestExecutorRun(t *testing.T) { ExecutorDocument: tt.cfgDoc, Helper: makeDefaultHelper(t), KubeConfig: kubeCfg, + ClusterMap: tt.clusterMap, }) require.NoError(t, err) ch := make(chan events.Event) diff --git a/pkg/k8s/applier/executor.go b/pkg/k8s/applier/executor.go index 12ee24579..3da91ee58 100644 --- a/pkg/k8s/applier/executor.go +++ b/pkg/k8s/applier/executor.go @@ -22,6 +22,7 @@ import ( "sigs.k8s.io/cli-utils/pkg/common" airshipv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1" + "opendev.org/airship/airshipctl/pkg/cluster/clustermap" "opendev.org/airship/airshipctl/pkg/document" "opendev.org/airship/airshipctl/pkg/errors" "opendev.org/airship/airshipctl/pkg/events" @@ -40,6 +41,7 @@ type ExecutorOptions struct { BundleFactory document.BundleFactoryFunc Kubeconfig kubeconfig.Interface Helper ifc.Helper + ClusterMap clustermap.ClusterMap } var _ ifc.Executor = &Executor{} @@ -64,6 +66,7 @@ func registerExecutor(cfg ifc.ExecutorConfig) (ifc.Executor, error) { ExecutorDocument: cfg.ExecutorDocument, BundleFactory: cfg.BundleFactory, Kubeconfig: cfg.KubeConfig, + ClusterMap: cfg.ClusterMap, }) } @@ -123,6 +126,11 @@ func (e *Executor) Run(ch chan events.Event, runOpts ifc.RunOptions) { } func (e *Executor) prepareApplier(ch chan events.Event) (*Applier, document.Bundle, error) { + log.Debug("Getting kubeconfig context name from cluster map") + context, err := e.Options.ClusterMap.ClusterKubeconfigContext(e.Options.ClusterName) + if err != nil { + return nil, nil, err + } log.Debug("Getting kubeconfig file information from kubeconfig provider") path, cleanup, err := e.Options.Kubeconfig.GetFile() if err != nil { @@ -136,8 +144,8 @@ func (e *Executor) prepareApplier(ch chan events.Event) (*Applier, document.Bund } // set up cleanup only if all calls up to here were successful e.cleanup = cleanup - // Use cluster name as context in kubeconfig file - factory := utils.FactoryFromKubeConfig(path, e.Options.ClusterName) + log.Debugf("Using kubeconfig at '%s' and context '%s'", path, context) + factory := utils.FactoryFromKubeConfig(path, context) return NewApplier(ch, factory), bundle, nil } diff --git a/pkg/k8s/applier/executor_test.go b/pkg/k8s/applier/executor_test.go index 42870287b..c21b227ca 100644 --- a/pkg/k8s/applier/executor_test.go +++ b/pkg/k8s/applier/executor_test.go @@ -21,6 +21,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "opendev.org/airship/airshipctl/pkg/api/v1alpha1" + "opendev.org/airship/airshipctl/pkg/cluster/clustermap" "opendev.org/airship/airshipctl/pkg/config" "opendev.org/airship/airshipctl/pkg/document" "opendev.org/airship/airshipctl/pkg/events" @@ -153,11 +155,13 @@ func TestExecutorRun(t *testing.T) { tests := []struct { name string containsErr string + clusterName string kubeconf kubeconfig.Interface execDoc document.Document bundleFactory document.BundleFactoryFunc helper ifc.Helper + clusterMap clustermap.ClusterMap }{ { name: "cant read kubeconfig error", @@ -166,6 +170,21 @@ func TestExecutorRun(t *testing.T) { bundleFactory: testBundleFactory("testdata/source_bundle"), kubeconf: testKubeconfig(`invalid kubeconfig`), execDoc: toKubernetesApply(t, ValidExecutorDocNamespaced), + clusterName: "ephemeral-cluster", + clusterMap: clustermap.NewClusterMap(&v1alpha1.ClusterMap{ + Map: map[string]*v1alpha1.Cluster{ + "ephemeral-cluster": {}, + }, + }), + }, + { + name: "error cluster not defined", + containsErr: "cluster is not defined in in cluster map", + helper: makeDefaultHelper(t), + bundleFactory: testBundleFactory("testdata/source_bundle"), + kubeconf: testKubeconfig(testValidKubeconfig), + execDoc: toKubernetesApply(t, ValidExecutorDocNamespaced), + clusterMap: clustermap.NewClusterMap(v1alpha1.DefaultClusterMap()), }, } for _, tt := range tests { @@ -177,6 +196,8 @@ func TestExecutorRun(t *testing.T) { Helper: tt.helper, BundleFactory: tt.bundleFactory, Kubeconfig: tt.kubeconf, + ClusterMap: tt.clusterMap, + ClusterName: tt.clusterName, }) if tt.name == "Nil bundle provided" { require.Error(t, err)