Allow to specify multiple clusters per get-kubeconfig request

Change-Id: I1b736a4b9cae4e6e47ddb7909a8fd619518e975c
Signed-off-by: Ruslan Aliev <raliev@mirantis.com>
Closes: #567
This commit is contained in:
Ruslan Aliev 2021-06-04 16:47:28 -05:00
parent f74a935df6
commit b8ddc1fe43
10 changed files with 64 additions and 63 deletions

View File

@ -23,11 +23,14 @@ import (
const ( const (
getKubeconfigLong = ` getKubeconfigLong = `
Retrieves kubeconfig of the cluster and prints it to stdout. Retrieves kubeconfig of the cluster(s) and prints it to stdout.
If you specify CLUSTER_NAME, kubeconfig will have a CurrentContext set to CLUSTER_NAME and If you specify single CLUSTER_NAME, kubeconfig will have a CurrentContext set to CLUSTER_NAME and
will have its context defined. will have its context defined.
If you specify multiple CLUSTER_NAME args, kubeconfig will contain contexts for all of them, but current one
won't be specified.
If you don't specify CLUSTER_NAME, kubeconfig will have multiple contexts for every cluster If you don't specify CLUSTER_NAME, kubeconfig will have multiple contexts for every cluster
in the airship site. Context names will correspond to cluster names. CurrentContext will be empty. in the airship site. Context names will correspond to cluster names. CurrentContext will be empty.
` `
@ -52,8 +55,8 @@ Airshipctl will overwrite the contents of the file, if you want merge with exist
func NewGetKubeconfigCommand(cfgFactory config.Factory) *cobra.Command { func NewGetKubeconfigCommand(cfgFactory config.Factory) *cobra.Command {
opts := &cluster.GetKubeconfigCommand{} opts := &cluster.GetKubeconfigCommand{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "get-kubeconfig CLUSTER_NAME", Use: "get-kubeconfig [CLUSTER_NAME...]",
Short: "Airshipctl command to retrieve kubeconfig for a desired cluster", Short: "Airshipctl command to retrieve kubeconfig for a desired cluster(s)",
Long: getKubeconfigLong[1:], Long: getKubeconfigLong[1:],
Args: GetKubeconfArgs(opts), Args: GetKubeconfArgs(opts),
Example: getKubeconfigExample, Example: getKubeconfigExample,
@ -82,9 +85,10 @@ func NewGetKubeconfigCommand(cfgFactory config.Factory) *cobra.Command {
// GetKubeconfArgs extracts one or less arguments from command line, and saves it as name // GetKubeconfArgs extracts one or less arguments from command line, and saves it as name
func GetKubeconfArgs(opts *cluster.GetKubeconfigCommand) cobra.PositionalArgs { func GetKubeconfArgs(opts *cluster.GetKubeconfigCommand) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error {
if len(args) == 1 { for _, arg := range args {
opts.ClusterName = args[0] opts.ClusterNames = append(opts.ClusterNames, arg)
} }
return cobra.MaximumNArgs(1)(cmd, args)
return cobra.MinimumNArgs(0)(cmd, args)
} }
} }

View File

@ -40,23 +40,23 @@ func TestNewKubeConfigCommandCmd(t *testing.T) {
func TestGetKubeconfArgs(t *testing.T) { func TestGetKubeconfArgs(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
args []string args []string
expectedErrStr string expectedErrStr string
expectedClusterName string expectedClusterNames []string
}{ }{
{ {
name: "success one cluster specified", name: "success one cluster specified",
args: []string{"cluster01"}, args: []string{"cluster01"},
expectedClusterName: "cluster01", expectedClusterNames: []string{"cluster01"},
}, },
{ {
name: "success no cluster specified", name: "success no cluster specified",
}, },
{ {
name: "error two cluster specified", name: "success two cluster specified",
expectedErrStr: "accepts at most 1 arg(s)", args: []string{"cluster01", "cluster02"},
args: []string{"cluster01", "cluster02"}, expectedClusterNames: []string{"cluster01", "cluster02"},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
@ -70,7 +70,7 @@ func TestGetKubeconfArgs(t *testing.T) {
assert.Contains(t, err.Error(), tt.expectedErrStr) assert.Contains(t, err.Error(), tt.expectedErrStr)
} else { } else {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.expectedClusterName, cmd.ClusterName) assert.Equal(t, tt.expectedClusterNames, cmd.ClusterNames)
} }
}) })
} }

View File

@ -5,7 +5,7 @@ Usage:
cluster [command] cluster [command]
Available Commands: Available Commands:
get-kubeconfig Airshipctl command to retrieve kubeconfig for a desired cluster get-kubeconfig Airshipctl command to retrieve kubeconfig for a desired cluster(s)
help Help about any command help Help about any command
list Airshipctl command to get and list defined clusters list Airshipctl command to get and list defined clusters
status Retrieve statuses of deployed cluster components status Retrieve statuses of deployed cluster components

View File

@ -1,13 +1,16 @@
Retrieves kubeconfig of the cluster and prints it to stdout. Retrieves kubeconfig of the cluster(s) and prints it to stdout.
If you specify CLUSTER_NAME, kubeconfig will have a CurrentContext set to CLUSTER_NAME and If you specify single CLUSTER_NAME, kubeconfig will have a CurrentContext set to CLUSTER_NAME and
will have its context defined. will have its context defined.
If you specify multiple CLUSTER_NAME args, kubeconfig will contain contexts for all of them, but current one
won't be specified.
If you don't specify CLUSTER_NAME, kubeconfig will have multiple contexts for every cluster If you don't specify CLUSTER_NAME, kubeconfig will have multiple contexts for every cluster
in the airship site. Context names will correspond to cluster names. CurrentContext will be empty. in the airship site. Context names will correspond to cluster names. CurrentContext will be empty.
Usage: Usage:
get-kubeconfig CLUSTER_NAME [flags] get-kubeconfig [CLUSTER_NAME...] [flags]
Examples: Examples:

View File

@ -32,7 +32,7 @@ SEE ALSO
~~~~~~~~ ~~~~~~~~
* :ref:`airshipctl <airshipctl>` - A unified command line tool for management of end-to-end kubernetes cluster deployment on cloud infrastructure environments. * :ref:`airshipctl <airshipctl>` - A unified command line tool for management of end-to-end kubernetes cluster deployment on cloud infrastructure environments.
* :ref:`airshipctl cluster get-kubeconfig <airshipctl_cluster_get-kubeconfig>` - Airshipctl command to retrieve kubeconfig for a desired cluster * :ref:`airshipctl cluster get-kubeconfig <airshipctl_cluster_get-kubeconfig>` - Airshipctl command to retrieve kubeconfig for a desired cluster(s)
* :ref:`airshipctl cluster list <airshipctl_cluster_list>` - Airshipctl command to get and list defined clusters * :ref:`airshipctl cluster list <airshipctl_cluster_list>` - Airshipctl command to get and list defined clusters
* :ref:`airshipctl cluster status <airshipctl_cluster_status>` - Retrieve statuses of deployed cluster components * :ref:`airshipctl cluster status <airshipctl_cluster_status>` - Retrieve statuses of deployed cluster components

View File

@ -3,24 +3,27 @@
airshipctl cluster get-kubeconfig airshipctl cluster get-kubeconfig
--------------------------------- ---------------------------------
Airshipctl command to retrieve kubeconfig for a desired cluster Airshipctl command to retrieve kubeconfig for a desired cluster(s)
Synopsis Synopsis
~~~~~~~~ ~~~~~~~~
Retrieves kubeconfig of the cluster and prints it to stdout. Retrieves kubeconfig of the cluster(s) and prints it to stdout.
If you specify CLUSTER_NAME, kubeconfig will have a CurrentContext set to CLUSTER_NAME and If you specify single CLUSTER_NAME, kubeconfig will have a CurrentContext set to CLUSTER_NAME and
will have its context defined. will have its context defined.
If you specify multiple CLUSTER_NAME args, kubeconfig will contain contexts for all of them, but current one
won't be specified.
If you don't specify CLUSTER_NAME, kubeconfig will have multiple contexts for every cluster If you don't specify CLUSTER_NAME, kubeconfig will have multiple contexts for every cluster
in the airship site. Context names will correspond to cluster names. CurrentContext will be empty. in the airship site. Context names will correspond to cluster names. CurrentContext will be empty.
:: ::
airshipctl cluster get-kubeconfig CLUSTER_NAME [flags] airshipctl cluster get-kubeconfig [CLUSTER_NAME...] [flags]
Examples Examples
~~~~~~~~ ~~~~~~~~

View File

@ -56,9 +56,9 @@ func StatusRunner(o StatusOptions, w io.Writer) error {
// GetKubeconfigCommand holds options for get kubeconfig command // GetKubeconfigCommand holds options for get kubeconfig command
type GetKubeconfigCommand struct { type GetKubeconfigCommand struct {
ClusterName string ClusterNames []string
File string File string
Merge bool Merge bool
} }
// RunE creates new kubeconfig interface object from secret, options hold the writer and merge(bool) // RunE creates new kubeconfig interface object from secret, options hold the writer and merge(bool)
@ -80,14 +80,14 @@ func (cmd *GetKubeconfigCommand) RunE(cfgFactory config.Factory, writer io.Write
} }
var siteWide bool var siteWide bool
if cmd.ClusterName == "" { if len(cmd.ClusterNames) == 0 {
siteWide = true siteWide = true
} }
kubeconf := kubeconfig.NewBuilder(). kubeconf := kubeconfig.NewBuilder().
WithBundle(helper.PhaseConfigBundle()). WithBundle(helper.PhaseConfigBundle()).
WithClusterMap(cMap). WithClusterMap(cMap).
WithClusterName(cmd.ClusterName). WithClusterNames(cmd.ClusterNames...).
WithTempRoot(helper.WorkDir()). WithTempRoot(helper.WorkDir()).
SiteWide(siteWide). SiteWide(siteWide).
Build() Build()

View File

@ -39,9 +39,9 @@ func NewBuilder() *Builder {
// Builder is an object that allows to build a kubeconfig based on various provided sources // Builder is an object that allows to build a kubeconfig based on various provided sources
// such as path to kubeconfig, path to bundle that should contain kubeconfig and parent cluster // such as path to kubeconfig, path to bundle that should contain kubeconfig and parent cluster
type Builder struct { type Builder struct {
siteWide bool siteWide bool
clusterName string clusterNames []string
root string root string
bundle document.Bundle bundle document.Bundle
client corev1.CoreV1Interface client corev1.CoreV1Interface
@ -62,9 +62,9 @@ func (b *Builder) WithClusterMap(cMap clustermap.ClusterMap) *Builder {
return b return b
} }
// WithClusterName allows to reach to a cluster to download kubeconfig from there // WithClusterNames allows to reach to a cluster to download kubeconfig from there
func (b *Builder) WithClusterName(clusterName string) *Builder { func (b *Builder) WithClusterNames(clusterNames ...string) *Builder {
b.clusterName = clusterName b.clusterNames = clusterNames
return b return b
} }
@ -106,40 +106,31 @@ func (b *Builder) Build() Interface {
} }
func (b *Builder) build() ([]byte, error) { func (b *Builder) build() ([]byte, error) {
// Set current context to clustername if it was provided if err := b.buildKubeconfig(); err != nil {
var result *api.Config return nil, err
var err error
if !b.siteWide {
var kubeContext string
kubeContext, result, err = b.buildOne(b.clusterName)
if err != nil {
return nil, err
}
b.siteKubeconf.CurrentContext = kubeContext
} else {
result, err = b.builtSiteKubeconf()
if err != nil {
return nil, err
}
} }
return clientcmd.Write(*result)
return clientcmd.Write(*b.siteKubeconf)
} }
func (b *Builder) builtSiteKubeconf() (*api.Config, error) { func (b *Builder) buildKubeconfig() error {
log.Debugf("Getting site kubeconfig") log.Debugf("Getting requested kubeconfig")
for _, clusterID := range b.clusterMap.AllClusters() { for _, clusterID := range (map[bool][]string{true: b.clusterMap.AllClusters(), false: b.clusterNames})[b.siteWide] {
log.Debugf("Getting kubeconfig for cluster '%s' to build site kubeconfig", clusterID) log.Debugf("Getting kubeconfig for cluster '%s'", clusterID)
// buildOne merges context into site kubeconfig // buildOne merges context into site kubeconfig
_, _, err := b.buildOne(clusterID) ctx, _, err := b.buildOne(clusterID)
if !b.siteWide && len(b.clusterNames) == 1 {
b.siteKubeconf.CurrentContext = ctx
}
if IsErrAllSourcesFailedErr(err) { if IsErrAllSourcesFailedErr(err) {
log.Debugf("All kubeconfig sources failed for cluster '%s', error '%v', skipping it", log.Debugf("All kubeconfig sources failed for cluster '%s', error '%v', skipping it",
clusterID, err) clusterID, err)
continue continue
} else if err != nil { } else if err != nil {
return nil, err return err
} }
} }
return b.siteKubeconf, nil return nil
} }
func (b *Builder) buildOne(clusterID string) (string, *api.Config, error) { func (b *Builder) buildOne(clusterID string) (string, *api.Config, error) {

View File

@ -260,7 +260,7 @@ func TestBuilderClusterctl(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
kube := kubeconfig.NewBuilder(). kube := kubeconfig.NewBuilder().
WithClusterMap(tt.clusterMap). WithClusterMap(tt.clusterMap).
WithClusterName(tt.requestedClusterName). WithClusterNames(tt.requestedClusterName).
WithBundle(testBundle). WithBundle(testBundle).
WithTempRoot(tt.tempRoot). WithTempRoot(tt.tempRoot).
WithCoreV1Client(tt.client). WithCoreV1Client(tt.client).

View File

@ -105,7 +105,7 @@ func (p *phase) executor(docFactory document.DocFactoryFunc,
WithBundle(p.helper.PhaseConfigBundle()). WithBundle(p.helper.PhaseConfigBundle()).
WithClusterMap(cMap). WithClusterMap(cMap).
WithTempRoot(p.helper.WorkDir()). WithTempRoot(p.helper.WorkDir()).
WithClusterName(p.apiObj.ClusterName). WithClusterNames(p.apiObj.ClusterName).
SiteWide(p.apiObj.Config.SiteWideKubeconfig). SiteWide(p.apiObj.Config.SiteWideKubeconfig).
Build() Build()