/* 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 https://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 kubeconfig_test import ( "bytes" "errors" "fmt" "io" "io/ioutil" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" apiv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/watch" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" v1 "k8s.io/client-go/tools/clientcmd/api/v1" kustfs "sigs.k8s.io/kustomize/api/filesys" "opendev.org/airship/airshipctl/pkg/api/v1alpha1" "opendev.org/airship/airshipctl/pkg/document" "opendev.org/airship/airshipctl/pkg/fs" "opendev.org/airship/airshipctl/pkg/k8s/kubeconfig" testfs "opendev.org/airship/airshipctl/testutil/fs" ) const ( testValidKubeconfig = `apiVersion: v1 clusters: - cluster: certificate-authority-data: ca-data 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: client-certificate-data: cert-data 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= ` ) var ( errTempFile = fmt.Errorf("tempFile Error") errSourceFunc = fmt.Errorf("source func error") errWriter = fmt.Errorf("writer error") testValidKubeconfigAPI = &v1alpha1.KubeConfig{ Config: v1.Config{ CurrentContext: "test", Clusters: []v1.NamedCluster{ { Name: "some-cluster", Cluster: v1.Cluster{ CertificateAuthority: "ca", Server: "https://10.0.1.7:6443", }, }, }, APIVersion: "v1", Contexts: []v1.NamedContext{ { Name: "test", Context: v1.Context{ Cluster: "some-cluster", AuthInfo: "some-user", }, }, }, AuthInfos: []v1.NamedAuthInfo{ { Name: "some-user", AuthInfo: v1.AuthInfo{ ClientCertificate: "cert-data", ClientKey: "client-key", }, }, }, }, } ) func TestKubeconfigContent(t *testing.T) { expectedData := []byte(testValidKubeconfig) fSys := fs.NewDocumentFs() kubeconf := kubeconfig.NewKubeConfig( kubeconfig.FromByte(expectedData), kubeconfig.InjectFileSystem(fSys), kubeconfig.InjectTempRoot(".")) path, clean, err := kubeconf.GetFile() require.NoError(t, err) defer clean() actualData, err := fSys.ReadFile(path) require.NoError(t, err) assert.Equal(t, expectedData, actualData) } type MockCoreV1Interface struct { MockSecrets func(string) corev1.SecretInterface corev1.CoreV1Interface } func (c MockCoreV1Interface) Secrets(n string) corev1.SecretInterface { return c.MockSecrets(n) } var _ corev1.SecretInterface = &SecretMockInterface{} type SecretMockInterface struct { mock.Mock } func (s *SecretMockInterface) Create(_ *apiv1.Secret) (*apiv1.Secret, error) { panic("implement me") } func (s *SecretMockInterface) Update(_ *apiv1.Secret) (*apiv1.Secret, error) { panic("implement me") } func (s *SecretMockInterface) Delete(_ string, _ *metav1.DeleteOptions) error { panic("implement me") } func (s *SecretMockInterface) DeleteCollection(_ *metav1.DeleteOptions, _ metav1.ListOptions) error { panic("implement me") } func (s *SecretMockInterface) Get(name string, options metav1.GetOptions) (*apiv1.Secret, error) { args := s.Called(name, options) expectedResult, ok := args.Get(0).(*apiv1.Secret) if !ok { return nil, fmt.Errorf("wrong input") } return expectedResult, args.Error(1) } func (s *SecretMockInterface) List(_ metav1.ListOptions) (*apiv1.SecretList, error) { panic("implement me") } func (s *SecretMockInterface) Watch(_ metav1.ListOptions) (watch.Interface, error) { panic("implement me") } func (s *SecretMockInterface) Patch(_ string, _ types.PatchType, _ []byte, _ ...string) (*apiv1.Secret, error) { panic("implement me") } func TestFromSecret(t *testing.T) { tests := []struct { name string options *v1alpha1.GetKubeconfigOptions getSecret *apiv1.Secret getErr error expectedData []byte expectedErr error }{ { name: "empty cluster name", options: &v1alpha1.GetKubeconfigOptions{}, expectedErr: kubeconfig.ErrClusterNameEmpty{}, }, { name: "multiple retries and error", options: &v1alpha1.GetKubeconfigOptions{ManagedClusterName: "cluster", Timeout: "1s"}, getErr: errors.New("error"), expectedErr: wait.ErrWaitTimeout, }, { name: "empty secret object", options: &v1alpha1.GetKubeconfigOptions{ManagedClusterName: "cluster"}, getSecret: &apiv1.Secret{}, expectedErr: kubeconfig.ErrMalformedKubeconfig{ClusterName: "cluster"}, }, { name: "empty data value", options: &v1alpha1.GetKubeconfigOptions{ManagedClusterName: "cluster"}, getSecret: &apiv1.Secret{Data: map[string][]byte{"value": {}}}, expectedErr: kubeconfig.ErrMalformedKubeconfig{ClusterName: "cluster"}, }, { name: "successfully get kubeconfig", options: &v1alpha1.GetKubeconfigOptions{ManagedClusterName: "cluster"}, getSecret: &apiv1.Secret{Data: map[string][]byte{"value": []byte(testValidKubeconfig)}}, expectedData: []byte(testValidKubeconfig), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { sm := &SecretMockInterface{ Mock: mock.Mock{}, } sm.On("Get", tt.options.ManagedClusterName+"-kubeconfig", metav1.GetOptions{}). Return(tt.getSecret, tt.getErr) coreV1Interface := MockCoreV1Interface{ MockSecrets: func(s string) corev1.SecretInterface { return sm }, } kubeconf, err := kubeconfig.FromSecret(coreV1Interface, tt.options)() if tt.expectedErr != nil { require.Error(t, err) require.Equal(t, tt.expectedErr, err) require.Nil(t, kubeconf) } else { require.NoError(t, err) require.Equal(t, tt.expectedData, kubeconf) } }) } } func TestFromBundle(t *testing.T) { tests := []struct { name string rootPath string expectedContains string shouldFail bool }{ { name: "valid kubeconfig", rootPath: "testdata", shouldFail: false, expectedContains: "parent_parent_context", }, { name: "wrong path", rootPath: "wrong/path", shouldFail: true, }, { name: "kubeconfig not found", rootPath: "testdata_fail", shouldFail: true, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { bundle, err := document.NewBundleByPath(tt.rootPath) if tt.shouldFail { require.Error(t, err) return } kubeconf, err := kubeconfig.FromBundle(bundle)() require.NoError(t, err) assert.Contains(t, string(kubeconf), tt.expectedContains) }) } } func TestNewKubeConfig(t *testing.T) { tests := []struct { shouldPanic bool name string expectedPathContains string expectedErrorContains string src kubeconfig.KubeSourceFunc options []kubeconfig.Option }{ { name: "write to temp file", src: kubeconfig.FromByte([]byte(testValidKubeconfig)), options: []kubeconfig.Option{ kubeconfig.InjectFileSystem( testfs.MockFileSystem{ MockTempFile: func(root, pattern string) (fs.File, error) { return testfs.TestFile{ MockName: func() string { return "kubeconfig-142398" }, MockWrite: func([]byte) (int, error) { return 0, nil }, MockClose: func() error { return nil }, }, nil }, MockRemoveAll: func() error { return nil }, }, ), }, expectedPathContains: "kubeconfig-142398", }, { name: "cleanup with dump root", expectedPathContains: "kubeconfig-142398", src: kubeconfig.FromByte([]byte(testValidKubeconfig)), options: []kubeconfig.Option{ kubeconfig.InjectTempRoot("/my-unique-root"), kubeconfig.InjectFileSystem( testfs.MockFileSystem{ MockTempFile: func(root, _ string) (fs.File, error) { // check if root path is passed to the TempFile interface if root != "/my-unique-root" { return nil, errTempFile } return testfs.TestFile{ MockName: func() string { return "kubeconfig-142398" }, MockWrite: func([]byte) (int, error) { return 0, nil }, MockClose: func() error { return nil }, }, nil }, MockRemoveAll: func() error { return nil }, }, ), }, }, { name: "from file, and fs option", src: kubeconfig.FromFile("/my/kubeconfig", fsWithFile(t, "/my/kubeconfig")), options: []kubeconfig.Option{ kubeconfig.InjectFilePath("/my/kubeconfig", fsWithFile(t, "/my/kubeconfig")), }, expectedPathContains: "/my/kubeconfig", }, { name: "write to real fs", src: kubeconfig.FromAPIalphaV1(testValidKubeconfigAPI), expectedPathContains: "kubeconfig-", }, { name: "from file, use SourceFile", src: kubeconfig.FromFile("/my/kubeconfig", fsWithFile(t, "/my/kubeconfig")), expectedPathContains: "kubeconfig-", }, { name: "temp file error", src: kubeconfig.FromAPIalphaV1(testValidKubeconfigAPI), expectedErrorContains: errTempFile.Error(), options: []kubeconfig.Option{ kubeconfig.InjectFileSystem( testfs.MockFileSystem{ MockTempFile: func(string, string) (fs.File, error) { return nil, errTempFile }, MockRemoveAll: func() error { return nil }, }, ), }, }, { name: "source func error", src: func() ([]byte, error) { return nil, errSourceFunc }, expectedPathContains: "kubeconfig-", expectedErrorContains: errSourceFunc.Error(), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { kubeconf := kubeconfig.NewKubeConfig(tt.src, tt.options...) path, clean, err := kubeconf.GetFile() if tt.expectedErrorContains != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.expectedErrorContains) } else { require.NoError(t, err) actualPath := path assert.Contains(t, actualPath, tt.expectedPathContains) clean() } }) } } func TestKubeConfigWrite(t *testing.T) { tests := []struct { name string expectedContent string expectedErrorContains string readWrite io.ReadWriter options []kubeconfig.Option src kubeconfig.KubeSourceFunc }{ { name: "Basic write", src: kubeconfig.FromByte([]byte(testValidKubeconfig)), expectedContent: testValidKubeconfig, readWrite: bytes.NewBuffer([]byte{}), }, { name: "Source error", src: func() ([]byte, error) { return nil, errSourceFunc }, expectedErrorContains: errSourceFunc.Error(), }, { name: "Writer error", src: kubeconfig.FromByte([]byte(testValidKubeconfig)), expectedErrorContains: errWriter.Error(), readWrite: fakeReaderWriter{writeErr: errWriter}, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { kubeconf := kubeconfig.NewKubeConfig(tt.src, tt.options...) err := kubeconf.Write(tt.readWrite) if tt.expectedErrorContains != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.expectedErrorContains) } else { require.NoError(t, err) assert.Equal(t, tt.expectedContent, read(t, tt.readWrite)) } }) } } func TestKubeConfigWriteFile(t *testing.T) { tests := []struct { name string expectedContent string path string expectedErrorContains string merge bool fs fs.FileSystem src kubeconfig.KubeSourceFunc }{ { name: "Basic write file", src: kubeconfig.FromByte([]byte(testValidKubeconfig)), expectedContent: testValidKubeconfig, merge: false, fs: fsWithFile(t, "/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", src: func() ([]byte, error) { return nil, errSourceFunc }, expectedErrorContains: errSourceFunc.Error(), merge: false, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { kubeconf := kubeconfig.NewKubeConfig(tt.src, 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 != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.expectedErrorContains) } else { require.NoError(t, err) assert.YAMLEq(t, tt.expectedContent, readFile(t, tt.path, tt.fs)) } }) } } func readFile(t *testing.T, path string, fSys fs.FileSystem) string { b, err := fSys.ReadFile(path) require.NoError(t, err) return string(b) } func read(t *testing.T, r io.Reader) string { b, err := ioutil.ReadAll(r) require.NoError(t, err) return string(b) } func fsWithFile(t *testing.T, path string) fs.FileSystem { memFs := kustfs.MakeFsInMemory() fSys := testfs.MockFileSystem{ FileSystem: memFs, MockRemoveAll: func() error { 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)) require.NoError(t, err) return fSys } type fakeReaderWriter struct { readErr error writeErr error } var _ io.Reader = fakeReaderWriter{} var _ io.Writer = fakeReaderWriter{} func (f fakeReaderWriter) Read(_ []byte) (n int, err error) { return 0, f.readErr } func (f fakeReaderWriter) Write(_ []byte) (n int, err error) { return 0, f.writeErr }