Add flags to airshipctl get-kubeconfig cmd

* This commits add --file and --merge flags to
  airshipctl get-kubeconfig cmd

Change-Id: I919d4f068d3ef9bcda6b3a7c9aeb0826a4e5c0d4
Signed-off-by: bijayasharma <vetbijaya@gmail.com>
Relates-To: #495
Closes: #495
This commit is contained in:
bijayasharma 2021-03-30 14:21:11 -04:00 committed by Bijaya Sharma
parent 8d882fcc85
commit 622d45f3bd
13 changed files with 201 additions and 22 deletions

View File

@ -37,6 +37,14 @@ Retrieve target-cluster kubeconfig
Retrieve kubeconfig for the entire site; the kubeconfig will have context for every cluster Retrieve kubeconfig for the entire site; the kubeconfig will have context for every cluster
# airshipctl cluster get-kubeconfig # airshipctl cluster get-kubeconfig
Specify a file where kubeconfig should be written
# airshipctl cluster get-kubeconfig --file ~/my-kubeconfig
Merge site kubeconfig with existing kubeconfig file.
Keep in mind that this can override a context if it has the same name
Airshipctl will overwrite the contents of the file, if you want merge with existing file, specify "--merge" flag
# airshipctl cluster get-kubeconfig --file ~/.airship/kubeconfig --merge
` `
) )
@ -53,6 +61,21 @@ func NewGetKubeconfigCommand(cfgFactory config.Factory) *cobra.Command {
return opts.RunE(cfgFactory, cmd.OutOrStdout()) return opts.RunE(cfgFactory, cmd.OutOrStdout())
}, },
} }
flags := cmd.Flags()
flags.StringVarP(
&opts.File,
"file",
"f",
"",
"specify where to write kubeconfig file. If flag isn't specified, airshipctl will write it to stdout",
)
flags.BoolVar(
&opts.Merge,
"merge",
false,
"specify if you want to merge kubeconfig with the one that exists at --file location",
)
return cmd return cmd
} }

View File

@ -17,6 +17,16 @@ Retrieve target-cluster kubeconfig
Retrieve kubeconfig for the entire site; the kubeconfig will have context for every cluster Retrieve kubeconfig for the entire site; the kubeconfig will have context for every cluster
# airshipctl cluster get-kubeconfig # airshipctl cluster get-kubeconfig
Specify a file where kubeconfig should be written
# airshipctl cluster get-kubeconfig --file ~/my-kubeconfig
Merge site kubeconfig with existing kubeconfig file.
Keep in mind that this can override a context if it has the same name
Airshipctl will overwrite the contents of the file, if you want merge with existing file, specify "--merge" flag
# airshipctl cluster get-kubeconfig --file ~/.airship/kubeconfig --merge
Flags: Flags:
-h, --help help for get-kubeconfig -f, --file string specify where to write kubeconfig file. If flag isn't specified, airshipctl will write it to stdout
-h, --help help for get-kubeconfig
--merge specify if you want to merge kubeconfig with the one that exists at --file location

View File

@ -27,12 +27,22 @@ Retrieve target-cluster kubeconfig
Retrieve kubeconfig for the entire site; the kubeconfig will have context for every cluster Retrieve kubeconfig for the entire site; the kubeconfig will have context for every cluster
# airshipctl cluster get-kubeconfig # airshipctl cluster get-kubeconfig
Specify a file where kubeconfig should be written
# airshipctl cluster get-kubeconfig --file ~/my-kubeconfig
Merge site kubeconfig with existing kubeconfig file.
Keep in mind that this can override a context if it has the same name
Airshipctl will overwrite the contents of the file, if you want merge with existing file, specify "--merge" flag
# airshipctl cluster get-kubeconfig --file ~/.airship/kubeconfig --merge
``` ```
### Options ### Options
``` ```
-h, --help help for get-kubeconfig -f, --file string specify where to write kubeconfig file. If flag isn't specified, airshipctl will write it to stdout
-h, --help help for get-kubeconfig
--merge specify if you want to merge kubeconfig with the one that exists at --file location
``` ```
### Options inherited from parent commands ### Options inherited from parent commands

View File

@ -57,9 +57,12 @@ 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 ClusterName string
File string
Merge bool
} }
// RunE creates new kubeconfig interface object from secret and prints its content to writer // RunE creates new kubeconfig interface object from secret, options hold the writer and merge(bool)
// to merge the kubeconfig. Writer in options is ignored if a file is provided.
func (cmd *GetKubeconfigCommand) RunE(cfgFactory config.Factory, writer io.Writer) error { func (cmd *GetKubeconfigCommand) RunE(cfgFactory config.Factory, writer io.Writer) error {
cfg, err := cfgFactory() cfg, err := cfgFactory()
if err != nil { if err != nil {
@ -89,5 +92,8 @@ func (cmd *GetKubeconfigCommand) RunE(cfgFactory config.Factory, writer io.Write
SiteWide(siteWide). SiteWide(siteWide).
Build() Build()
if cmd.File != "" {
return kubeconf.WriteFile(cmd.File, kubeconfig.WriteOptions{Merge: cmd.Merge})
}
return kubeconf.Write(writer) return kubeconf.Write(writer)
} }

View File

@ -215,7 +215,7 @@ func TestBuilderClusterctl(t *testing.T) {
MockTempFile: func(s1, s2 string) (fs.File, error) { MockTempFile: func(s1, s2 string) (fs.File, error) {
return testfs.TestFile{ return testfs.TestFile{
MockName: func() string { return kubeconfigPath }, MockName: func() string { return kubeconfigPath },
MockWrite: func() (int, error) { return 0, nil }, MockWrite: func([]byte) (int, error) { return 0, nil },
MockClose: func() error { return nil }, MockClose: func() error { return nil },
}, nil }, nil
}, },

View File

@ -24,6 +24,7 @@ import (
corev1 "k8s.io/client-go/kubernetes/typed/core/v1" corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api" "k8s.io/client-go/tools/clientcmd/api"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"opendev.org/airship/airshipctl/pkg/api/v1alpha1" "opendev.org/airship/airshipctl/pkg/api/v1alpha1"
@ -48,7 +49,8 @@ type Interface interface {
// Write will write kubeconfig to the provided writer // Write will write kubeconfig to the provided writer
Write(w io.Writer) error Write(w io.Writer) error
// WriteFile will write kubeconfig data to specified path // WriteFile will write kubeconfig data to specified path
WriteFile(path string) error // WriteOptions holds additional option when writing kubeconfig to file
WriteFile(path string, options WriteOptions) error
// WriteTempFile writes a file a temporary file, returns path to it, cleanup function and error // WriteTempFile writes a file a temporary file, returns path to it, cleanup function and error
// it is responsibility of the caller to use the cleanup function to make sure that there are no leftovers // it is responsibility of the caller to use the cleanup function to make sure that there are no leftovers
WriteTempFile(dumpRoot string) (string, Cleanup, error) WriteTempFile(dumpRoot string) (string, Cleanup, error)
@ -65,6 +67,11 @@ type kubeConfig struct {
sourceFunc KubeSourceFunc sourceFunc KubeSourceFunc
} }
// WriteOptions holds additional option while writing kubeconfig to the file
type WriteOptions struct {
Merge bool
}
// NewKubeConfig serves as a constructor for kubeconfig Interface // NewKubeConfig serves as a constructor for kubeconfig Interface
// first argument is a function that should return bytes with kubeconfig and error // first argument is a function that should return bytes with kubeconfig and error
// see FromByte() FromAPIalphaV1() FromFile() functions or extend with your own // see FromByte() FromAPIalphaV1() FromFile() functions or extend with your own
@ -206,8 +213,14 @@ func InjectFilePath(path string, fSys fs.FileSystem) Option {
} }
} }
func (k *kubeConfig) WriteFile(path string) (err error) { func (k *kubeConfig) WriteFile(path string, options WriteOptions) error {
data, err := k.bytes() var data []byte
var err error
if options.Merge && path != "" {
data, err = k.mergedBytes(path)
} else {
data, err = k.bytes()
}
if err != nil { if err != nil {
return err return err
} }
@ -253,6 +266,24 @@ func (k *kubeConfig) bytes() ([]byte, error) {
return k.savedByes, err return k.savedByes, err
} }
// mergedBytes takes the file path and return byte data of the kubeconfig file to be written
func (k *kubeConfig) mergedBytes(path string) ([]byte, error) {
kFile, cleanup, err := k.WriteTempFile(k.dumpRoot)
if err != nil {
return []byte{}, err
}
defer cleanup()
rules := clientcmd.ClientConfigLoadingRules{
Precedence: []string{path, kFile},
}
mergedConfig, err := rules.Load()
if err != nil {
return []byte{}, err
}
return clientcmd.Write(*mergedConfig)
}
// GetFile checks if path to kubeconfig is already set and returns it no cleanup is necessary, // GetFile checks if path to kubeconfig is already set and returns it no cleanup is necessary,
// and Cleanup() method will do nothing. // and Cleanup() method will do nothing.
// If path is not set kubeconfig will be written to temporary file system, returned path will // If path is not set kubeconfig will be written to temporary file system, returned path will

View File

@ -62,6 +62,52 @@ users:
user: user:
client-certificate-data: cert-data client-certificate-data: cert-data
client-key-data: client-keydata client-key-data: client-keydata
`
testValidKubeconfigTwo = `
apiVersion: v1
clusters:
- cluster:
server: https://10.0.1.7:6443
name: kubernetes_target
contexts:
- context:
cluster: kubernetes_target
user: kubernetes-admin
name: kubernetes-admin@kubernetes
current-context: ""
kind: Config
preferences: {}
users:
- name: kubernetes-admin
user: {}
`
//testMergedValidKubeconfig tests to confirm whether two kubeconfig got merged or not
testMergedValidKubeconfig = `
apiVersion: v1
clusters:
- cluster:
server: https://10.23.25.101:6443
name: dummycluster_ephemeral
- cluster:
server: https://10.0.1.7:6443
name: kubernetes_target
contexts:
- context:
cluster: dummycluster_ephemeral
user: kubernetes-admin
name: dummy_cluster
- context:
cluster: kubernetes_target
user: kubernetes-admin
name: kubernetes-admin@kubernetes
current-context: dummy_cluster
kind: Config
preferences: {}
users:
- name: kubernetes-admin
user:
client-certificate-data: dGVzdAo=
client-key-data: dGVzdAo=
` `
) )
@ -297,7 +343,7 @@ func TestNewKubeConfig(t *testing.T) {
MockTempFile: func(root, pattern string) (fs.File, error) { MockTempFile: func(root, pattern string) (fs.File, error) {
return testfs.TestFile{ return testfs.TestFile{
MockName: func() string { return "kubeconfig-142398" }, MockName: func() string { return "kubeconfig-142398" },
MockWrite: func() (int, error) { return 0, nil }, MockWrite: func([]byte) (int, error) { return 0, nil },
MockClose: func() error { return nil }, MockClose: func() error { return nil },
}, nil }, nil
}, },
@ -322,7 +368,7 @@ func TestNewKubeConfig(t *testing.T) {
} }
return testfs.TestFile{ return testfs.TestFile{
MockName: func() string { return "kubeconfig-142398" }, MockName: func() string { return "kubeconfig-142398" },
MockWrite: func() (int, error) { return 0, nil }, MockWrite: func([]byte) (int, error) { return 0, nil },
MockClose: func() error { return nil }, MockClose: func() error { return nil },
}, nil }, nil
}, },
@ -440,6 +486,7 @@ func TestKubeConfigWriteFile(t *testing.T) {
expectedContent string expectedContent string
path string path string
expectedErrorContains string expectedErrorContains string
merge bool
fs fs.FileSystem fs fs.FileSystem
src kubeconfig.KubeSourceFunc src kubeconfig.KubeSourceFunc
@ -448,26 +495,52 @@ func TestKubeConfigWriteFile(t *testing.T) {
name: "Basic write file", name: "Basic write file",
src: kubeconfig.FromByte([]byte(testValidKubeconfig)), src: kubeconfig.FromByte([]byte(testValidKubeconfig)),
expectedContent: testValidKubeconfig, expectedContent: testValidKubeconfig,
merge: false,
fs: fsWithFile(t, "/test-path"), fs: fsWithFile(t, "/test-path"),
path: "/test-path", path: "/test-path",
}, },
{
name: "Basic merge write file",
src: kubeconfig.FromByte([]byte(testValidKubeconfigTwo)),
expectedContent: testMergedValidKubeconfig,
merge: true,
fs: fs.NewDocumentFs(),
path: "testdata/kubeconfig-test",
},
{ {
name: "Source error", name: "Source error",
src: func() ([]byte, error) { return nil, errSourceFunc }, src: func() ([]byte, error) { return nil, errSourceFunc },
expectedErrorContains: errSourceFunc.Error(), expectedErrorContains: errSourceFunc.Error(),
merge: false,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
tt := tt tt := tt
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
kubeconf := kubeconfig.NewKubeConfig(tt.src, kubeconfig.InjectFileSystem(tt.fs)) kubeconf := kubeconfig.NewKubeConfig(tt.src,
err := kubeconf.WriteFile(tt.path) kubeconfig.InjectTempRoot("testdata/"),
kubeconfig.InjectFileSystem(tt.fs))
options := kubeconfig.WriteOptions{
Merge: tt.merge,
}
if tt.merge {
_, clean, err := kubeconf.GetFile()
require.NoError(t, err)
defer clean()
original, err := ioutil.ReadFile(tt.path)
require.NoError(t, err)
defer func() {
err = ioutil.WriteFile(tt.path, original, 0600)
require.NoError(t, err)
}()
}
err := kubeconf.WriteFile(tt.path, options)
if tt.expectedErrorContains != "" { if tt.expectedErrorContains != "" {
require.Error(t, err) require.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedErrorContains) assert.Contains(t, err.Error(), tt.expectedErrorContains)
} else { } else {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, tt.expectedContent, readFile(t, tt.path, tt.fs)) assert.YAMLEq(t, tt.expectedContent, readFile(t, tt.path, tt.fs))
} }
}) })
} }
@ -486,11 +559,19 @@ func read(t *testing.T, r io.Reader) string {
} }
func fsWithFile(t *testing.T, path string) fs.FileSystem { func fsWithFile(t *testing.T, path string) fs.FileSystem {
memFs := kustfs.MakeFsInMemory()
fSys := testfs.MockFileSystem{ fSys := testfs.MockFileSystem{
FileSystem: kustfs.MakeFsInMemory(), FileSystem: memFs,
MockRemoveAll: func() error { MockRemoveAll: func() error {
return nil return nil
}, },
MockTempFile: func(root, pattern string) (fs.File, error) {
return testfs.TestFile{
MockName: func() string { return "kubeconfig-142398" },
MockWrite: func(b []byte) (int, error) { return 0, memFs.WriteFile("kubeconfig-142398", b) },
MockClose: func() error { return nil },
}, nil
},
} }
err := fSys.WriteFile(path, []byte(testValidKubeconfig)) err := fSys.WriteFile(path, []byte(testValidKubeconfig))
require.NoError(t, err) require.NoError(t, err)

View File

@ -0,0 +1,18 @@
apiVersion: v1
kind: Config
clusters:
- cluster:
server: https://10.23.25.101:6443
name: dummycluster_ephemeral
contexts:
- context:
cluster: dummycluster_ephemeral
user: kubernetes-admin
name: dummy_cluster
current-context: dummy_cluster
preferences: {}
users:
- name: kubernetes-admin
user:
client-certificate-data: dGVzdAo=
client-key-data: dGVzdAo=

View File

@ -83,7 +83,7 @@ func TestApply(t *testing.T) {
MockTempFile: func(string, string) (fs.File, error) { MockTempFile: func(string, string) (fs.File, error) {
return testfs.TestFile{ return testfs.TestFile{
MockName: func() string { return filenameRC }, MockName: func() string { return filenameRC },
MockWrite: func() (int, error) { return 0, nil }, MockWrite: func([]byte) (int, error) { return 0, nil },
MockClose: func() error { return nil }, MockClose: func() error { return nil },
}, nil }, nil
}, },
@ -100,7 +100,7 @@ func TestApply(t *testing.T) {
MockRemoveAll: func() error { return nil }, MockRemoveAll: func() error { return nil },
MockTempFile: func(string, string) (fs.File, error) { MockTempFile: func(string, string) (fs.File, error) {
return testfs.TestFile{ return testfs.TestFile{
MockWrite: func() (int, error) { return 0, ErrTempFileError }, MockWrite: func([]byte) (int, error) { return 0, ErrTempFileError },
MockName: func() string { return filenameRC }, MockName: func() string { return filenameRC },
MockClose: func() error { return nil }, MockClose: func() error { return nil },
}, nil }, nil

View File

@ -154,7 +154,7 @@ func TestClusterctlExecutorRun(t *testing.T) {
MockTempFile: func(string, string) (fs.File, error) { MockTempFile: func(string, string) (fs.File, error) {
return testfs.TestFile{ return testfs.TestFile{
MockName: func() string { return "filename" }, MockName: func() string { return "filename" },
MockWrite: func() (int, error) { return 0, nil }, MockWrite: func([]byte) (int, error) { return 0, nil },
MockClose: func() error { return nil }, MockClose: func() error { return nil },
}, nil }, nil
}, },

View File

@ -344,9 +344,9 @@ type fakeKubeConfig struct {
getFile func() (string, kubeconfig.Cleanup, error) getFile func() (string, kubeconfig.Cleanup, error)
} }
func (k fakeKubeConfig) GetFile() (string, kubeconfig.Cleanup, error) { return k.getFile() } func (k fakeKubeConfig) GetFile() (string, kubeconfig.Cleanup, error) { return k.getFile() }
func (k fakeKubeConfig) Write(_ io.Writer) error { return nil } func (k fakeKubeConfig) Write(_ io.Writer) error { return nil }
func (k fakeKubeConfig) WriteFile(_ string) error { return nil } func (k fakeKubeConfig) WriteFile(_ string, _ kubeconfig.WriteOptions) error { return nil }
func (k fakeKubeConfig) WriteTempFile(_ string) (string, kubeconfig.Cleanup, error) { func (k fakeKubeConfig) WriteTempFile(_ string) (string, kubeconfig.Cleanup, error) {
return k.getFile() return k.getFile()
} }

View File

@ -290,7 +290,7 @@ func testKubeconfig(stringData string) kubeconfig.Interface {
MockTempFile: func(root, pattern string) (fs.File, error) { MockTempFile: func(root, pattern string) (fs.File, error) {
return testfs.TestFile{ return testfs.TestFile{
MockName: func() string { return "kubeconfig-142398" }, MockName: func() string { return "kubeconfig-142398" },
MockWrite: func() (int, error) { return 0, nil }, MockWrite: func([]byte) (int, error) { return 0, nil },
MockClose: func() error { return nil }, MockClose: func() error { return nil },
}, nil }, nil
}, },

View File

@ -62,7 +62,7 @@ func (fsys MockFileSystem) Dir(path string) string {
type TestFile struct { type TestFile struct {
fs.File fs.File
MockName func() string MockName func() string
MockWrite func() (int, error) MockWrite func([]byte) (int, error)
MockClose func() error MockClose func() error
} }
@ -70,7 +70,7 @@ type TestFile struct {
func (f TestFile) Name() string { return f.MockName() } func (f TestFile) Name() string { return f.MockName() }
// Write File interface implementation // Write File interface implementation
func (f TestFile) Write([]byte) (int, error) { return f.MockWrite() } func (f TestFile) Write(b []byte) (int, error) { return f.MockWrite(b) }
// Close File interface implementation // Close File interface implementation
func (f TestFile) Close() error { return f.MockClose() } func (f TestFile) Close() error { return f.MockClose() }