airshipctl/pkg/k8s/kubeconfig/kubeconfig_test.go
Vladislav Kuzmin 6742aab691 Extend kubeconfig interface for document bundle
Add new function to extract kubeconfig from a document bundle.
This is a small step to start using kubeconfig from the document bundle.

Closes: #264

Change-Id: Ib601c2212b901da76a2df14e73058cd3a77d1f98
2020-08-25 14:20:04 +04:00

407 lines
16 KiB
Go

/*
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"
"fmt"
"io"
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
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/k8s/kubeconfig"
"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
`
//nolint: lll
testFullValidKubeconfig = `apiVersion: v1
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRFNU1USXlOakE0TWpneU5Gb1hEVEk1TVRJeU16QTRNamd5TkZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTTFSClM0d3lnajNpU0JBZjlCR0JUS1p5VTFwYmdDaGQ2WTdJektaZWRoakM2K3k1ZEJpWm81ZUx6Z2tEc2gzOC9YQ1MKenFPS2V5cE5RcDN5QVlLdmJKSHg3ODZxSFZZNjg1ZDVYVDNaOHNyVVRzVDR5WmNzZHAzV3lHdDM0eXYzNi9BSQoxK1NlUFErdU5JemN6bzNEdWhXR0ZoQjk3VjZwRitFUTBlVWN5bk05c2hkL3AwWVFzWDR1ZlhxaENENVpzZnZUCnBka3UvTWkyWnVGUldUUUtNeGpqczV3Z2RBWnBsNnN0L2ZkbmZwd1Q5cC9WTjRuaXJnMEsxOURTSFFJTHVrU2MKb013bXNBeDJrZmxITWhPazg5S3FpMEloL2cyczRFYTRvWURZemt0Y2JRZ24wd0lqZ2dmdnVzM3pRbEczN2lwYQo4cVRzS2VmVGdkUjhnZkJDNUZNQ0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFJek9BL00xWmRGUElzd2VoWjFuemJ0VFNURG4KRHMyVnhSV0VnclFFYzNSYmV3a1NkbTlBS3MwVGR0ZHdEbnBEL2tRYkNyS2xEeFF3RWg3NFZNSFZYYkFadDdsVwpCSm90T21xdXgxYThKYklDRTljR0FHRzFvS0g5R29jWERZY0JzOTA3ckxIdStpVzFnL0xVdG5hN1dSampqZnBLCnFGelFmOGdJUHZIM09BZ3B1RVVncUx5QU8ya0VnelZwTjZwQVJxSnZVRks2TUQ0YzFmMnlxWGxwNXhrN2dFSnIKUzQ4WmF6d0RmWUVmV3Jrdld1YWdvZ1M2SktvbjVEZ0Z1ZHhINXM2Snl6R3lPVnZ0eG1TY2FvOHNxaCs3UXkybgoyLzFVcU5ZK0hlN0x4d04rYkhwYkIxNUtIMTU5ZHNuS3BRbjRORG1jSTZrVnJ3MDVJMUg5ZGRBbGF0bz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
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
kind: Config
preferences: {}
users:
- name: kubernetes-admin
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQwRENDQXJnQ0ZFdFBveEZYSjVrVFNWTXQ0OVlqcHBQL3hCYnlNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1CVXgKRXpBUkJnTlZCQU1UQ210MVltVnlibVYwWlhNd0hoY05NakF3TVRJME1Ua3hOVEV3V2hjTk1qa3hNakF5TVRreApOVEV3V2pBME1Sa3dGd1lEVlFRRERCQnJkV0psY201bGRHVnpMV0ZrYldsdU1SY3dGUVlEVlFRS0RBNXplWE4wClpXMDZiV0Z6ZEdWeWN6Q0NBaUl3RFFZSktvWklodmNOQVFFQkJRQURnZ0lQQURDQ0Fnb0NnZ0lCQU1iaFhUUmsKVjZiZXdsUjBhZlpBdTBGYWVsOXRtRThaSFEvaGtaSHhuTjc2bDZUUFltcGJvaDRvRjNGMFFqbzROS1o5NVRuWgo0OWNoV240eFJiZVlPU25EcDBpV0Qzd0pXUlZ5aVFvVUFyYTlNcHVPNkVFU1FpbFVGNXNxc0VXUVdVMjBETStBCkdxK1k0Z2c3eDJ1Q0hTdk1GUmkrNEw5RWlXR2xnRDIvb1hXUm5NWEswNExQajZPb3Vkb2Zid2RmT3J6dTBPVkUKUzR0eGtuS1BCY1BUU3YxMWVaWVhja0JEVjNPbExENEZ3dTB3NTcwcnczNzAraEpYdlZxd3Zjb2RjZjZEL1BXWQowamlnd2ppeUJuZ2dXYW04UVFjd1Nud3o0d05sV3hKOVMyWUJFb1ptdWxVUlFaWVk5ZXRBcEpBdFMzTjlUNlQ2ClovSlJRdEdhZDJmTldTYkxEck5qdU1OTGhBYWRMQnhJUHpBNXZWWk5aalJkdEMwU25pMlFUMTVpSFp4d1RxcjQKakRQQ0pYRXU3KytxcWpQVldUaUZLK3JqcVNhS1pqVWZVaUpHQkJWcm5RZkJENHNtRnNkTjB5cm9tYTZOYzRMNQpKS21RV1NHdmd1aG0zbW5sYjFRaVRZanVyZFJQRFNmdmwrQ0NHbnA1QkkvZ1pwMkF1SHMvNUpKVTJlc1ZvL0xsCkVPdHdSOXdXd3dXcTAvZjhXS3R4bVRrMTUyOUp2dFBGQXQweW1CVjhQbHZlYnVwYmJqeW5pL2xWbTJOYmV6dWUKeCtlMEpNbGtWWnFmYkRSS243SjZZSnJHWW1CUFV0QldoSVkzb1pJVTFEUXI4SUlIbkdmYlZoWlR5ME1IMkFCQQp1dlVQcUtSVk80UGkxRTF4OEE2eWVPeVRDcnB4L0pBazVyR2RBZ01CQUFFd0RRWUpLb1pJaHZjTkFRRUxCUUFECmdnRUJBSWNFM1BxZHZDTVBIMnJzMXJESk9ESHY3QWk4S01PVXZPRi90RjlqR2EvSFBJbkh3RlVFNEltbldQeDYKVUdBMlE1bjFsRDFGQlU0T0M4eElZc3VvS1VQVHk1T0t6SVNMNEZnL0lEcG54STlrTXlmNStMR043aG8rblJmawpCZkpJblVYb0tERW1neHZzSWFGd1h6bGtSTDJzL1lKYUZRRzE1Uis1YzFyckJmd2dJOFA5Tkd6aEM1cXhnSmovCm04K3hPMGhXUmJIYklrQ21NekRib2pCSWhaL00rb3VYR1doei9TakpodXhZTVBnek5MZkFGcy9PMTVaSjd3YXcKZ3ZoSGc3L2E5UzRvUCtEYytPa3VrMkV1MUZjL0E5WHpWMzc5aWhNWW5ub3RQMldWeFZ3b0ZZQUg0NUdQcDZsUApCQmwyNnkxc2JMbjl6aGZYUUJIMVpFN0EwZVE9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBdXpHSll2UFo2RG9pNDIxREs4V0phWkNuTkFkMnF6NXAvMDQyb0Z6UVBickFnekUyCnFZVmt6T0w4eEFWZVI3U041d1dvVFdFeUY4RVY3cnIvNCtIaEhHaXE1UG1xdUlGeXp6bjYvSVpjOGpVOXhFZnoKdmlrY2lyTGZVNHZSWEpRd1Z3Z0FTTmwyQVdBSWgyZERkVHJqQkNmaUtXTUh5ajBSYkhhbHNCek9wZ1QvSFR2MwpHUXpuVVF6Rkt2MmRqNVYxTmtTL0RIanlSUkorRUw2UUJZbUdzZWc1UDROYkM5ZWJ1aXBtTVRBcS9KdW1Pb29kCitGakwyblpxTDZmSTZmQnRGNU9HbHBDQjFZSjhmekN0R1FRWjdIVEliZGIydHA0M0ZWT2h5UWJWY0hxVEEwNFAKSjE1KzBXQXltVUpVejhYQTU0NHIvYnY3NEpjSlVSRmhhclppUXdJREFRQUJBb0lCQVFDU0pycjlaeVpiQ2dqegpSL3VKMFZEWCt2aVF4c01BTUZyUjJsOE1GV3NBeHk1SFA4Vk4xYmc5djN0YUVGYnI1U3hsa3lVMFJRNjNQU25DCm1uM3ZqZ3dVQWlScllnTEl5MGk0UXF5VFBOU1V4cnpTNHRxTFBjM3EvSDBnM2FrNGZ2cSsrS0JBUUlqQnloamUKbnVFc1JpMjRzT3NESlM2UDE5NGlzUC9yNEpIM1M5bFZGbkVuOGxUR2c0M1kvMFZoMXl0cnkvdDljWjR5ZUNpNwpjMHFEaTZZcXJZaFZhSW9RRW1VQjdsbHRFZkZzb3l4VDR6RTE5U3pVbkRoMmxjYTF1TzhqcmI4d2xHTzBoQ2JyClB1R1l2WFFQa3Q0VlNmalhvdGJ3d2lBNFRCVERCRzU1bHp6MmNKeS9zSS8zSHlYbEMxcTdXUmRuQVhhZ1F0VzkKOE9DZGRkb0JBb0dCQU5NcUNtSW94REtyckhZZFRxT1M1ZFN4cVMxL0NUN3ZYZ0pScXBqd2Y4WHA2WHo0KzIvTAozVXFaVDBEL3dGTkZkc1Z4eFYxMnNYMUdwMHFWZVlKRld5OVlCaHVSWGpTZ0ZEWldSY1Z1Y01sNVpPTmJsbmZGCjVKQ0xnNXFMZ1g5VTNSRnJrR3A0R241UDQxamg4TnhKVlhzZG5xWE9xNTFUK1RRT1UzdkpGQjc1QW9HQkFPTHcKalp1cnZtVkZyTHdaVGgvRDNpWll5SVV0ZUljZ2NKLzlzbTh6L0pPRmRIbFd4dGRHUFVzYVd1MnBTNEhvckFtbgpqTm4vSTluUXd3enZ3MWUzVVFPbUhMRjVBczk4VU5hbk5TQ0xNMW1yaXZHRXJ1VHFnTDM1bU41eFZPdTUxQU5JCm4yNkFtODBJT2JDeEtLa0R0ZXJSaFhHd3g5c1pONVJCbG9VRThZNGJBb0dBQ3ZsdVhMZWRxcng5VkE0bDNoNXUKVDJXRVUxYjgxZ1orcmtRc1I1S0lNWEw4cllBTElUNUpHKzFuendyN3BkaEFXZmFWdVV2SDRhamdYT0h6MUs5aQpFODNSVTNGMG9ldUg0V01PY1RwU0prWm0xZUlXcWRiaEVCb1FGdUlWTXRib1BsV0d4ZUhFRHJoOEtreGp4aThSCmdEcUQyajRwY1IzQ0g5QjJ5a0lqQjVFQ2dZRUExc0xXLys2enE1c1lNSm14K1JXZThhTXJmL3pjQnVTSU1LQWgKY0dNK0wwMG9RSHdDaUU4TVNqcVN1ajV3R214YUFuanhMb3ZwSFlRV1VmUEVaUW95UE1YQ2VhRVBLOU4xbk8xMwp0V2lHRytIZkIxaU5PazFCc0lhNFNDbndOM1FRVTFzeXBaeEgxT3hueS9LYmkvYmEvWEZ5VzNqMGFUK2YvVWxrCmJGV1ZVdWtDZ1lFQTBaMmRTTFlmTjV5eFNtYk5xMWVqZXdWd1BjRzQxR2hQclNUZEJxdHFac1doWGE3aDdLTWEKeHdvamh5SXpnTXNyK2tXODdlajhDQ2h0d21sQ1p5QU92QmdOZytncnJ1cEZLM3FOSkpKeU9YREdHckdpbzZmTQp5aXB3Q2tZVGVxRThpZ1J6UkI5QkdFUGY4eVpjMUtwdmZhUDVhM0lRZmxiV0czbGpUemNNZVZjPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
`
)
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)
fs := document.NewDocumentFs()
kubeconf := kubeconfig.NewKubeConfig(
kubeconfig.FromByte(expectedData),
kubeconfig.InjectFileSystem(fs),
kubeconfig.InjectTempRoot("."))
path, clean, err := kubeconf.GetFile()
require.NoError(t, err)
defer clean()
actualData, err := fs.ReadFile(path)
require.NoError(t, err)
assert.Equal(t, expectedData, actualData)
}
func TestFromBundle(t *testing.T) {
tests := []struct {
name string
rootPath string
shouldFail bool
expectedData []byte
}{
{
name: "valid kubeconfig",
rootPath: "testdata",
shouldFail: false,
expectedData: []byte(testFullValidKubeconfig),
},
{
name: "wrong path",
rootPath: "wrong/path",
shouldFail: true,
expectedData: nil,
},
{
name: "kubeconfig not found",
rootPath: "testdata_fail",
shouldFail: true,
expectedData: nil,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
kubeconf, err := kubeconfig.FromBundle(tt.rootPath)()
if tt.shouldFail {
require.Error(t, err)
assert.Nil(t, kubeconf)
} else {
require.NoError(t, err)
assert.Equal(t, tt.expectedData, kubeconf)
}
})
}
}
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(
fs.MockFileSystem{
MockTempFile: func(root, pattern string) (document.File, error) {
return fs.TestFile{
MockName: func() string { return "kubeconfig-142398" },
MockWrite: func() (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(
fs.MockFileSystem{
MockTempFile: func(root, _ string) (document.File, error) {
// check if root path is passed to the TempFile interface
if root != "/my-unique-root" {
return nil, errTempFile
}
return fs.TestFile{
MockName: func() string { return "kubeconfig-142398" },
MockWrite: func() (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(
fs.MockFileSystem{
MockTempFile: func(string, string) (document.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
fs document.FileSystem
src kubeconfig.KubeSourceFunc
}{
{
name: "Basic write file",
src: kubeconfig.FromByte([]byte(testValidKubeconfig)),
expectedContent: testValidKubeconfig,
fs: fsWithFile(t, "/test-path"),
path: "/test-path",
},
{
name: "Source error",
src: func() ([]byte, error) { return nil, errSourceFunc },
expectedErrorContains: errSourceFunc.Error(),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
kubeconf := kubeconfig.NewKubeConfig(tt.src, kubeconfig.InjectFileSystem(tt.fs))
err := kubeconf.WriteFile(tt.path)
if tt.expectedErrorContains != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedErrorContains)
} else {
require.NoError(t, err)
assert.Equal(t, tt.expectedContent, readFile(t, tt.path, tt.fs))
}
})
}
}
func readFile(t *testing.T, path string, fs document.FileSystem) string {
b, err := fs.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) document.FileSystem {
fSys := fs.MockFileSystem{
FileSystem: kustfs.MakeFsInMemory(),
MockRemoveAll: func() error {
return 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(p []byte) (n int, err error) {
return 0, f.readErr
}
func (f fakeReaderWriter) Write(p []byte) (n int, err error) {
return 0, f.writeErr
}