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
This commit is contained in:
Kostiantyn Kalynovskyi 2020-10-26 16:23:22 -05:00 committed by Kostyantyn Kalynovskyi
parent cb5849d024
commit 8567ddf749
7 changed files with 98 additions and 5 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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)
})
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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
}

View File

@ -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)