[#20] Add kubectl apply wrapper package
The wrapper is called ApplyAdapter and is a struct, that has Apply(..) method and some setters that allow to control kubectl apply behaviour Addapter is expected to be used through Apply(..) function, which takes slice of document.Document interface objects, writes them out to temporary file system, from where they are picked up by kubectl Apply module, and delivered to kubernetes cluster. The decision to use temporary file system is based on the fact, that in current state kubectl project currently only works with actual files, and ignores io.Streams object, that is part of ApplyOptions struct, so it is currently the only way to use it. Change-Id: Idc5d79794149c00198f420d76cf9aa3b5264946e
This commit is contained in:
parent
9a47c9b423
commit
d588c73e38
3
go.mod
3
go.mod
@ -26,8 +26,11 @@ require (
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1
|
||||
k8s.io/api v0.0.0
|
||||
k8s.io/apimachinery v0.0.0
|
||||
k8s.io/cli-runtime v0.0.0
|
||||
k8s.io/client-go v11.0.0+incompatible
|
||||
k8s.io/kubectl v0.0.0
|
||||
k8s.io/kubernetes v1.16.3
|
||||
opendev.org/airship/go-redfish v0.0.0-20200110185254-3ab47e28bae8
|
||||
opendev.org/airship/go-redfish/client v0.0.0-20200110185254-3ab47e28bae8
|
||||
|
78
pkg/k8s/kubectl/apply_options.go
Normal file
78
pkg/k8s/kubectl/apply_options.go
Normal file
@ -0,0 +1,78 @@
|
||||
package kubectl
|
||||
|
||||
import (
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/kubectl/pkg/cmd/apply"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
// NewApplyOptions is a helper function that Creates ApplyOptions of kubectl apply module
|
||||
// Values set here, are default, and do not conflict with each other, can be used if you
|
||||
// need `kubectl apply` functionality without calling executing command in shell
|
||||
// To function properly, you may need to specify files from where to read the resources:
|
||||
// DeleteOptions.Filenames of returned object has to be set for that
|
||||
func NewApplyOptions(f cmdutil.Factory, streams genericclioptions.IOStreams) (*apply.ApplyOptions, error) {
|
||||
o := apply.NewApplyOptions(streams)
|
||||
o.ServerSideApply = false
|
||||
o.ForceConflicts = false
|
||||
|
||||
o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {
|
||||
o.PrintFlags.NamePrintFlags.Operation = operation
|
||||
if o.DryRun {
|
||||
err := o.PrintFlags.Complete("%s (dry run)")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if o.ServerDryRun {
|
||||
err := o.PrintFlags.Complete("%s (server dry run)")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return o.PrintFlags.ToPrinter()
|
||||
}
|
||||
|
||||
var err error
|
||||
o.Recorder, err = o.RecordFlags.ToRecorder()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o.DiscoveryClient, err = f.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dynamicClient, err := f.DynamicClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o.DeleteOptions = o.DeleteFlags.ToOptions(dynamicClient, o.IOStreams)
|
||||
// This can only fail if ToDiscoverClient() function fails
|
||||
o.OpenAPISchema, err = f.OpenAPISchema()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o.Validator, err = f.Validator(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o.Builder = f.NewBuilder()
|
||||
o.Mapper, err = f.ToRESTMapper()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o.DynamicClient = dynamicClient
|
||||
|
||||
o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return o, nil
|
||||
}
|
73
pkg/k8s/kubectl/apply_options_test.go
Normal file
73
pkg/k8s/kubectl/apply_options_test.go
Normal file
@ -0,0 +1,73 @@
|
||||
package kubectl_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/k8s/kubectl"
|
||||
k8stest "opendev.org/airship/airshipctl/testutil/k8sutils"
|
||||
)
|
||||
|
||||
var (
|
||||
filenameRC = "testdata/replicationcontroller.yaml"
|
||||
|
||||
testStreams = genericclioptions.NewTestIOStreamsDiscard()
|
||||
ToDiscoveryError = errors.New("ToDiscoveryError")
|
||||
DynamicClientError = errors.New("DynamicClientError")
|
||||
ValidateError = errors.New("ValidateError")
|
||||
ToRESTMapperError = errors.New("ToRESTMapperError")
|
||||
NamespaceError = errors.New("NamespaceError")
|
||||
)
|
||||
|
||||
func TestApplyOptionsRun(t *testing.T) {
|
||||
f := k8stest.NewFakeFactoryForRC(t, filenameRC)
|
||||
defer f.Cleanup()
|
||||
|
||||
streams := genericclioptions.NewTestIOStreamsDiscard()
|
||||
|
||||
aa, err := kubectl.NewApplyOptions(f, streams)
|
||||
require.NoError(t, err, "Could not build ApplyAdapter")
|
||||
aa.DryRun = true
|
||||
aa.DeleteOptions.Filenames = []string{filenameRC}
|
||||
assert.NoError(t, aa.Run())
|
||||
}
|
||||
|
||||
func TestNewApplyOptionsFactoryFailures(t *testing.T) {
|
||||
tests := []struct {
|
||||
f cmdutil.Factory
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
f: k8stest.NewMockKubectlFactory().WithToDiscoveryClientByError(nil, ToDiscoveryError),
|
||||
expectedError: ToDiscoveryError,
|
||||
},
|
||||
{
|
||||
f: k8stest.NewMockKubectlFactory().WithDynamicClientByError(nil, DynamicClientError),
|
||||
expectedError: DynamicClientError,
|
||||
},
|
||||
{
|
||||
f: k8stest.NewMockKubectlFactory().WithValidatorByError(nil, ValidateError),
|
||||
expectedError: ValidateError,
|
||||
},
|
||||
{
|
||||
f: k8stest.NewMockKubectlFactory().WithToRESTMapperByError(nil, ToRESTMapperError),
|
||||
expectedError: ToRESTMapperError,
|
||||
},
|
||||
{
|
||||
f: k8stest.NewMockKubectlFactory().
|
||||
WithToRawKubeConfigLoaderByError(k8stest.
|
||||
NewMockClientConfig().
|
||||
WithNamespace("", false, NamespaceError)),
|
||||
expectedError: NamespaceError,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
_, err := kubectl.NewApplyOptions(test.f, testStreams)
|
||||
assert.Equal(t, err, test.expectedError)
|
||||
}
|
||||
}
|
29
pkg/k8s/kubectl/filesystem.go
Normal file
29
pkg/k8s/kubectl/filesystem.go
Normal file
@ -0,0 +1,29 @@
|
||||
package kubectl
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/pkg/fs"
|
||||
)
|
||||
|
||||
// File extends kustomize File and provide abstraction to creating temporary files
|
||||
type File interface {
|
||||
fs.File
|
||||
Name() string
|
||||
}
|
||||
|
||||
// FileSystem extends kustomize FileSystem and provide abstraction to creating temporary files
|
||||
type FileSystem interface {
|
||||
fs.FileSystem
|
||||
TempFile(string, string) (File, error)
|
||||
}
|
||||
|
||||
// Buffer is adaptor to TempFile
|
||||
type Buffer struct {
|
||||
fs.FileSystem
|
||||
}
|
||||
|
||||
// TempFile creates file in temporary filesystem, at default os.TempDir
|
||||
func (b Buffer) TempFile(tmpDir string, prefix string) (File, error) {
|
||||
return ioutil.TempFile(tmpDir, prefix)
|
||||
}
|
11
pkg/k8s/kubectl/interfaces.go
Normal file
11
pkg/k8s/kubectl/interfaces.go
Normal file
@ -0,0 +1,11 @@
|
||||
package kubectl
|
||||
|
||||
import (
|
||||
"k8s.io/kubectl/pkg/cmd/apply"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/document"
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
Apply(docs []document.Document, ao *apply.ApplyOptions) error
|
||||
}
|
76
pkg/k8s/kubectl/kubectl.go
Normal file
76
pkg/k8s/kubectl/kubectl.go
Normal file
@ -0,0 +1,76 @@
|
||||
package kubectl
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/kubectl/pkg/cmd/apply"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"sigs.k8s.io/kustomize/v3/pkg/fs"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/document"
|
||||
"opendev.org/airship/airshipctl/pkg/log"
|
||||
utilyaml "opendev.org/airship/airshipctl/pkg/util/yaml"
|
||||
)
|
||||
|
||||
// Kubectl container holds Factory, Streams and FileSystem to
|
||||
// interact with upstream kubectl objects and serves as abstraction to kubectl project
|
||||
type Kubectl struct {
|
||||
cmdutil.Factory
|
||||
genericclioptions.IOStreams
|
||||
FileSystem
|
||||
// Directory to buffer documents before passing them to kubectl commands
|
||||
// default is empty, this means that /tmp dir will be used
|
||||
bufferDir string
|
||||
}
|
||||
|
||||
// NewKubectlFromKubeconfigPath builds an instance
|
||||
// of Kubectl struct from Path to kubeconfig file
|
||||
func NewKubectl(f cmdutil.Factory) *Kubectl {
|
||||
return &Kubectl{
|
||||
Factory: f,
|
||||
IOStreams: genericclioptions.IOStreams{
|
||||
In: os.Stdin,
|
||||
Out: os.Stdout,
|
||||
ErrOut: os.Stderr,
|
||||
},
|
||||
FileSystem: Buffer{FileSystem: fs.MakeRealFS()},
|
||||
}
|
||||
}
|
||||
|
||||
func (kubectl *Kubectl) WithBufferDir(bd string) *Kubectl {
|
||||
kubectl.bufferDir = bd
|
||||
return kubectl
|
||||
}
|
||||
|
||||
// Apply is abstraction to kubectl apply command
|
||||
func (kubectl *Kubectl) Apply(docs []document.Document, ao *apply.ApplyOptions) error {
|
||||
tf, err := kubectl.TempFile(kubectl.bufferDir, "initinfra")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func(f File) {
|
||||
fName := f.Name()
|
||||
dErr := kubectl.RemoveAll(fName)
|
||||
if dErr != nil {
|
||||
log.Fatalf("Failed to cleanup temporary file %s during kubectl apply", fName)
|
||||
}
|
||||
}(tf)
|
||||
defer tf.Close()
|
||||
for _, doc := range docs {
|
||||
// Write out documents to temporary file
|
||||
err = utilyaml.WriteOut(tf, doc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ao.DeleteOptions.Filenames = []string{tf.Name()}
|
||||
return ao.Run()
|
||||
}
|
||||
|
||||
// ApplyOptions is a wrapper over kubectl ApplyOptions, used to build
|
||||
// new options from the factory and iostreams defined in Kubectl container
|
||||
func (kubectl *Kubectl) ApplyOptions() (*apply.ApplyOptions, error) {
|
||||
return NewApplyOptions(kubectl.Factory, kubectl.IOStreams)
|
||||
}
|
109
pkg/k8s/kubectl/kubectl_test.go
Normal file
109
pkg/k8s/kubectl/kubectl_test.go
Normal file
@ -0,0 +1,109 @@
|
||||
package kubectl_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sigs.k8s.io/kustomize/v3/pkg/fs"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/k8s/kubectl"
|
||||
k8sutils "opendev.org/airship/airshipctl/pkg/k8s/utils"
|
||||
"opendev.org/airship/airshipctl/testutil"
|
||||
k8stest "opendev.org/airship/airshipctl/testutil/k8sutils"
|
||||
)
|
||||
|
||||
var (
|
||||
kubeconfigPath = "testdata/kubeconfig.yaml"
|
||||
fixtureDir = "testdata/"
|
||||
|
||||
writeOutError = errors.New("writeOutError")
|
||||
TempFileError = errors.New("TempFileError")
|
||||
)
|
||||
|
||||
type MockFileSystem struct {
|
||||
MockRemoveAll func() error
|
||||
MockTempFile func() (kubectl.File, error)
|
||||
fs.FileSystem
|
||||
}
|
||||
|
||||
func (fsys MockFileSystem) RemoveAll(name string) error { return fsys.MockRemoveAll() }
|
||||
func (fsys MockFileSystem) TempFile(bufferDir string, prefix string) (kubectl.File, error) {
|
||||
return fsys.MockTempFile()
|
||||
}
|
||||
|
||||
type TestFile struct {
|
||||
kubectl.File
|
||||
MockName func() string
|
||||
MockWrite func() (int, error)
|
||||
MockClose func() error
|
||||
}
|
||||
|
||||
func (f TestFile) Name() string { return f.MockName() }
|
||||
func (f TestFile) Write([]byte) (int, error) { return f.MockWrite() }
|
||||
func (f TestFile) Close() error { return f.MockClose() }
|
||||
|
||||
func TestNewKubectlFromKubeconfigPath(t *testing.T) {
|
||||
f := k8sutils.FactoryFromKubeconfigPath(kubeconfigPath)
|
||||
kctl := kubectl.NewKubectl(f).WithBufferDir("/tmp/.airship")
|
||||
|
||||
assert.NotNil(t, kctl.Factory)
|
||||
assert.NotNil(t, kctl.FileSystem)
|
||||
assert.NotNil(t, kctl.IOStreams)
|
||||
}
|
||||
|
||||
func TestApply(t *testing.T) {
|
||||
f := k8stest.NewFakeFactoryForRC(t, filenameRC)
|
||||
defer f.Cleanup()
|
||||
kctl := kubectl.NewKubectl(f).WithBufferDir("/tmp/.airship")
|
||||
kctl.Factory = f
|
||||
ao, err := kctl.ApplyOptions()
|
||||
require.NoError(t, err, "failed to get documents from bundle")
|
||||
ao.DryRun = true
|
||||
|
||||
b := testutil.NewTestBundle(t, fixtureDir)
|
||||
docs, err := b.GetByAnnotation("airshipit.org/initinfra")
|
||||
require.NoError(t, err, "failed to get documents from bundle")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
expectedErr error
|
||||
fs kubectl.FileSystem
|
||||
}{
|
||||
{
|
||||
expectedErr: nil,
|
||||
fs: MockFileSystem{
|
||||
MockRemoveAll: func() error { return nil },
|
||||
MockTempFile: func() (kubectl.File, error) {
|
||||
return TestFile{
|
||||
MockName: func() string { return filenameRC },
|
||||
MockWrite: func() (int, error) { return 0, nil },
|
||||
MockClose: func() error { return nil },
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
expectedErr: writeOutError,
|
||||
fs: MockFileSystem{
|
||||
MockTempFile: func() (kubectl.File, error) { return nil, writeOutError }},
|
||||
},
|
||||
{
|
||||
expectedErr: TempFileError,
|
||||
fs: MockFileSystem{
|
||||
MockRemoveAll: func() error { return nil },
|
||||
MockTempFile: func() (kubectl.File, error) {
|
||||
return TestFile{
|
||||
MockWrite: func() (int, error) { return 0, TempFileError },
|
||||
MockName: func() string { return filenameRC },
|
||||
MockClose: func() error { return nil },
|
||||
}, nil
|
||||
}},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
kctl.FileSystem = test.fs
|
||||
assert.Equal(t, kctl.Apply(docs, ao), test.expectedErr)
|
||||
}
|
||||
}
|
19
pkg/k8s/kubectl/testdata/kubeconfig.yaml
vendored
Normal file
19
pkg/k8s/kubectl/testdata/kubeconfig.yaml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRFNU1Ea3lPVEUzTURNd09Wb1hEVEk1TURreU5qRTNNRE13T1Zvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTUZyCkdxM0kyb2dZci81Y01Udy9Na1pORTNWQURzdEdyU240WjU2TDhPUGhMcUhDN2t1dno2dVpES3dCSGtGeTBNK2MKRXIzd2piUGE1aTV5NmkyMGtxSHBVMjdPZTA0dzBXV2s4N0RSZVlWaGNoZVJHRXoraWt3SndIcGRmMjJVemZNKwpkSDBzaUhuMVd6UnovYk4za3hMUzJlMnZ2U1Y3bmNubk1YRUd4OXV0MUY0NThHeWxxdmxXTUlWMzg5Q2didXFDCkcwcFdiMTBLM0RVZWdiT25Xa1FmSm5sTWRRVVZDUVdZZEZaaklrcWtkWi9hVTRobkNEV01oZXNWRnFNaDN3VVAKczhQay9BNWh1ZFFPbnFRNDVIWXZLdjZ5RjJWcDUyWExBRUx3NDJ4aVRKZlh0V1h4eHR6cU4wY1lyL2VxeS9XMQp1YVVGSW5xQjFVM0JFL1oxbmFrQ0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFKUUVKQVBLSkFjVDVuK3dsWGJsdU9mS0J3c2gKZTI4R1c5R2QwM0N0NGF3RzhzMXE1ZHNua2tpZmVTUENHVFZ1SXF6UTZDNmJaSk9SMDMvVEl5ejh6NDJnaitDVApjWUZXZkltM2RKTnpRL08xWkdySXZZNWdtcWJtWDlpV0JaU24rRytEOGxubzd2aGMvY0tBRFR5OTMvVU92MThuCkdhMnIrRGJJcHcyTWVBVEl2elpxRS9RWlVSQ25DMmdjUFhTVzFqN2h4R3o1a3ZNcGVDZTdQYVUvdVFvblVHSWsKZ2t6ZzI4NHQvREhUUzc4N1V1SUg5cXBaV09yTFNMOGFBeUxQUHhWSXBteGZmbWRETE9TS2VUemRlTmxoSitUMwowQlBVaHBQTlJBNTNJN0hRQjhVUDR2elNONTkzZ1VFbVlFQ2Jic2RYSzB6ZVR6SDdWWHR2Zmd5WTVWWT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
|
||||
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: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM4akNDQWRxZ0F3SUJBZ0lJQXhEdzk2RUY4SXN3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB4T1RBNU1qa3hOekF6TURsYUZ3MHlNREE1TWpneE56QXpNVEphTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXV6R0pZdlBaNkRvaTQyMUQKSzhXSmFaQ25OQWQycXo1cC8wNDJvRnpRUGJyQWd6RTJxWVZrek9MOHhBVmVSN1NONXdXb1RXRXlGOEVWN3JyLwo0K0hoSEdpcTVQbXF1SUZ5enpuNi9JWmM4alU5eEVmenZpa2NpckxmVTR2UlhKUXdWd2dBU05sMkFXQUloMmRECmRUcmpCQ2ZpS1dNSHlqMFJiSGFsc0J6T3BnVC9IVHYzR1F6blVRekZLdjJkajVWMU5rUy9ESGp5UlJKK0VMNlEKQlltR3NlZzVQNE5iQzllYnVpcG1NVEFxL0p1bU9vb2QrRmpMMm5acUw2Zkk2ZkJ0RjVPR2xwQ0IxWUo4ZnpDdApHUVFaN0hUSWJkYjJ0cDQzRlZPaHlRYlZjSHFUQTA0UEoxNSswV0F5bVVKVXo4WEE1NDRyL2J2NzRKY0pVUkZoCmFyWmlRd0lEQVFBQm95Y3dKVEFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFMMmhIUmVibEl2VHJTMFNmUVg1RG9ueVVhNy84aTg1endVWApSd3dqdzFuS0U0NDJKbWZWRGZ5b0hRYUM4Ti9MQkxyUXM0U0lqU1JYdmFHU1dSQnRnT1RRV21Db1laMXdSbjdwCndDTXZQTERJdHNWWm90SEZpUFl2b1lHWFFUSXA3YlROMmg1OEJaaEZ3d25nWUovT04zeG1rd29IN1IxYmVxWEYKWHF1TTluekhESk41VlZub1lQR09yRHMwWlg1RnNxNGtWVU0wVExNQm9qN1ZIRDhmU0E5RjRYNU4yMldsZnNPMAo4aksrRFJDWTAyaHBrYTZQQ0pQS0lNOEJaMUFSMG9ZakZxT0plcXpPTjBqcnpYWHh4S2pHVFVUb1BldVA5dCtCCjJOMVA1TnI4a2oxM0lrend5Q1NZclFVN09ZM3ltZmJobHkrcXZxaFVFa014MlQ1SkpmQT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
|
||||
client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBdXpHSll2UFo2RG9pNDIxREs4V0phWkNuTkFkMnF6NXAvMDQyb0Z6UVBickFnekUyCnFZVmt6T0w4eEFWZVI3U041d1dvVFdFeUY4RVY3cnIvNCtIaEhHaXE1UG1xdUlGeXp6bjYvSVpjOGpVOXhFZnoKdmlrY2lyTGZVNHZSWEpRd1Z3Z0FTTmwyQVdBSWgyZERkVHJqQkNmaUtXTUh5ajBSYkhhbHNCek9wZ1QvSFR2MwpHUXpuVVF6Rkt2MmRqNVYxTmtTL0RIanlSUkorRUw2UUJZbUdzZWc1UDROYkM5ZWJ1aXBtTVRBcS9KdW1Pb29kCitGakwyblpxTDZmSTZmQnRGNU9HbHBDQjFZSjhmekN0R1FRWjdIVEliZGIydHA0M0ZWT2h5UWJWY0hxVEEwNFAKSjE1KzBXQXltVUpVejhYQTU0NHIvYnY3NEpjSlVSRmhhclppUXdJREFRQUJBb0lCQVFDU0pycjlaeVpiQ2dqegpSL3VKMFZEWCt2aVF4c01BTUZyUjJsOE1GV3NBeHk1SFA4Vk4xYmc5djN0YUVGYnI1U3hsa3lVMFJRNjNQU25DCm1uM3ZqZ3dVQWlScllnTEl5MGk0UXF5VFBOU1V4cnpTNHRxTFBjM3EvSDBnM2FrNGZ2cSsrS0JBUUlqQnloamUKbnVFc1JpMjRzT3NESlM2UDE5NGlzUC9yNEpIM1M5bFZGbkVuOGxUR2c0M1kvMFZoMXl0cnkvdDljWjR5ZUNpNwpjMHFEaTZZcXJZaFZhSW9RRW1VQjdsbHRFZkZzb3l4VDR6RTE5U3pVbkRoMmxjYTF1TzhqcmI4d2xHTzBoQ2JyClB1R1l2WFFQa3Q0VlNmalhvdGJ3d2lBNFRCVERCRzU1bHp6MmNKeS9zSS8zSHlYbEMxcTdXUmRuQVhhZ1F0VzkKOE9DZGRkb0JBb0dCQU5NcUNtSW94REtyckhZZFRxT1M1ZFN4cVMxL0NUN3ZYZ0pScXBqd2Y4WHA2WHo0KzIvTAozVXFaVDBEL3dGTkZkc1Z4eFYxMnNYMUdwMHFWZVlKRld5OVlCaHVSWGpTZ0ZEWldSY1Z1Y01sNVpPTmJsbmZGCjVKQ0xnNXFMZ1g5VTNSRnJrR3A0R241UDQxamg4TnhKVlhzZG5xWE9xNTFUK1RRT1UzdkpGQjc1QW9HQkFPTHcKalp1cnZtVkZyTHdaVGgvRDNpWll5SVV0ZUljZ2NKLzlzbTh6L0pPRmRIbFd4dGRHUFVzYVd1MnBTNEhvckFtbgpqTm4vSTluUXd3enZ3MWUzVVFPbUhMRjVBczk4VU5hbk5TQ0xNMW1yaXZHRXJ1VHFnTDM1bU41eFZPdTUxQU5JCm4yNkFtODBJT2JDeEtLa0R0ZXJSaFhHd3g5c1pONVJCbG9VRThZNGJBb0dBQ3ZsdVhMZWRxcng5VkE0bDNoNXUKVDJXRVUxYjgxZ1orcmtRc1I1S0lNWEw4cllBTElUNUpHKzFuendyN3BkaEFXZmFWdVV2SDRhamdYT0h6MUs5aQpFODNSVTNGMG9ldUg0V01PY1RwU0prWm0xZUlXcWRiaEVCb1FGdUlWTXRib1BsV0d4ZUhFRHJoOEtreGp4aThSCmdEcUQyajRwY1IzQ0g5QjJ5a0lqQjVFQ2dZRUExc0xXLys2enE1c1lNSm14K1JXZThhTXJmL3pjQnVTSU1LQWgKY0dNK0wwMG9RSHdDaUU4TVNqcVN1ajV3R214YUFuanhMb3ZwSFlRV1VmUEVaUW95UE1YQ2VhRVBLOU4xbk8xMwp0V2lHRytIZkIxaU5PazFCc0lhNFNDbndOM1FRVTFzeXBaeEgxT3hueS9LYmkvYmEvWEZ5VzNqMGFUK2YvVWxrCmJGV1ZVdWtDZ1lFQTBaMmRTTFlmTjV5eFNtYk5xMWVqZXdWd1BjRzQxR2hQclNUZEJxdHFac1doWGE3aDdLTWEKeHdvamh5SXpnTXNyK2tXODdlajhDQ2h0d21sQ1p5QU92QmdOZytncnJ1cEZLM3FOSkpKeU9YREdHckdpbzZmTQp5aXB3Q2tZVGVxRThpZ1J6UkI5QkdFUGY4eVpjMUtwdmZhUDVhM0lRZmxiV0czbGpUemNNZVZjPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
|
2
pkg/k8s/kubectl/testdata/kustomization.yaml
vendored
Normal file
2
pkg/k8s/kubectl/testdata/kustomization.yaml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
resources:
|
||||
- replicationcontroller.yaml
|
22
pkg/k8s/kubectl/testdata/replicationcontroller.yaml
vendored
Normal file
22
pkg/k8s/kubectl/testdata/replicationcontroller.yaml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: test-rc
|
||||
namespace: test
|
||||
annotations:
|
||||
airshipit.org/initinfra: "workflow"
|
||||
labels:
|
||||
name: test-rc
|
||||
airshipit.org/initinfra: "workflow"
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: test-rc
|
||||
spec:
|
||||
containers:
|
||||
- name: test-rc
|
||||
image: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
12
pkg/k8s/utils/utils.go
Normal file
12
pkg/k8s/utils/utils.go
Normal file
@ -0,0 +1,12 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
func FactoryFromKubeconfigPath(kp string) cmdutil.Factory {
|
||||
kf := genericclioptions.NewConfigFlags(false)
|
||||
kf.KubeConfig = &kp
|
||||
return cmdutil.NewFactory(kf)
|
||||
}
|
215
testutil/k8sutils/mock_kubectl_factory.go
Normal file
215
testutil/k8sutils/mock_kubectl_factory.go
Normal file
@ -0,0 +1,215 @@
|
||||
package k8sutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
kubeconfig "k8s.io/client-go/tools/clientcmd/api"
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/openapi"
|
||||
"k8s.io/kubectl/pkg/validation"
|
||||
)
|
||||
|
||||
// MockKubectlFactory implements Factory interface for testing purposes.
|
||||
type MockKubectlFactory struct {
|
||||
MockToDiscoveryClient func() (discovery.CachedDiscoveryInterface, error)
|
||||
MockDynamicClient func() (dynamic.Interface, error)
|
||||
MockOpenAPISchema func() (openapi.Resources, error)
|
||||
MockValidator func() (validation.Schema, error)
|
||||
MockToRESTMapper func() (meta.RESTMapper, error)
|
||||
MockToRESTConfig func() (*rest.Config, error)
|
||||
MockNewBuilder func() *resource.Builder
|
||||
MockToRawKubeConfigLoader func() clientcmd.ClientConfig
|
||||
MockClientForMapping func() (resource.RESTClient, error)
|
||||
KubeConfig kubeconfig.Config
|
||||
genericclioptions.ConfigFlags
|
||||
cmdutil.Factory
|
||||
}
|
||||
|
||||
// ToDiscoveryClient implements Factory interface
|
||||
func (f *MockKubectlFactory) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
|
||||
return f.MockToDiscoveryClient()
|
||||
}
|
||||
func (f *MockKubectlFactory) DynamicClient() (dynamic.Interface, error) { return f.MockDynamicClient() }
|
||||
func (f *MockKubectlFactory) OpenAPISchema() (openapi.Resources, error) { return f.MockOpenAPISchema() }
|
||||
func (f *MockKubectlFactory) Validator(validate bool) (validation.Schema, error) {
|
||||
return f.MockValidator()
|
||||
}
|
||||
func (f *MockKubectlFactory) ToRESTMapper() (meta.RESTMapper, error) { return f.MockToRESTMapper() }
|
||||
func (f *MockKubectlFactory) ToRESTConfig() (*rest.Config, error) { return f.MockToRESTConfig() }
|
||||
func (f *MockKubectlFactory) NewBuilder() *resource.Builder { return f.MockNewBuilder() }
|
||||
func (f *MockKubectlFactory) ToRawKubeConfigLoader() clientcmd.ClientConfig {
|
||||
return f.MockToRawKubeConfigLoader()
|
||||
}
|
||||
func (f *MockKubectlFactory) ClientForMapping(*meta.RESTMapping) (resource.RESTClient, error) {
|
||||
return f.MockClientForMapping()
|
||||
}
|
||||
|
||||
func (f *MockKubectlFactory) WithToDiscoveryClientByError(d dynamic.Interface, err error) *MockKubectlFactory {
|
||||
f.MockDynamicClient = func() (dynamic.Interface, error) { return d, err }
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *MockKubectlFactory) WithOpenAPISchemaByError(r openapi.Resources, err error) *MockKubectlFactory {
|
||||
f.MockOpenAPISchema = func() (openapi.Resources, error) { return r, err }
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *MockKubectlFactory) WithDynamicClientByError(d discovery.CachedDiscoveryInterface,
|
||||
err error) *MockKubectlFactory {
|
||||
f.MockToDiscoveryClient = func() (discovery.CachedDiscoveryInterface, error) { return d, err }
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *MockKubectlFactory) WithValidatorByError(v validation.Schema, err error) *MockKubectlFactory {
|
||||
f.MockValidator = func() (validation.Schema, error) { return v, err }
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *MockKubectlFactory) WithToRESTMapperByError(r meta.RESTMapper, err error) *MockKubectlFactory {
|
||||
f.MockToRESTMapper = func() (meta.RESTMapper, error) { return r, err }
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *MockKubectlFactory) WithToRESTConfigByError(r *rest.Config, err error) *MockKubectlFactory {
|
||||
f.MockToRESTConfig = func() (*rest.Config, error) { return r, err }
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *MockKubectlFactory) WithNewBuilderByError(r *resource.Builder) *MockKubectlFactory {
|
||||
f.MockNewBuilder = func() *resource.Builder { return r }
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *MockKubectlFactory) WithToRawKubeConfigLoaderByError(c clientcmd.ClientConfig) *MockKubectlFactory {
|
||||
f.MockToRawKubeConfigLoader = func() clientcmd.ClientConfig { return c }
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *MockKubectlFactory) WithClientForMappingByError(r resource.RESTClient, err error) *MockKubectlFactory {
|
||||
f.MockClientForMapping = func() (resource.RESTClient, error) { return r, err }
|
||||
return f
|
||||
}
|
||||
|
||||
func NewMockKubectlFactory() *MockKubectlFactory {
|
||||
return &MockKubectlFactory{MockDynamicClient: func() (dynamic.Interface, error) { return nil, nil },
|
||||
MockToDiscoveryClient: func() (discovery.CachedDiscoveryInterface, error) { return nil, nil },
|
||||
MockOpenAPISchema: func() (openapi.Resources, error) { return nil, nil },
|
||||
MockValidator: func() (validation.Schema, error) { return nil, nil },
|
||||
MockToRESTMapper: func() (meta.RESTMapper, error) { return nil, nil },
|
||||
MockToRESTConfig: func() (*rest.Config, error) { return nil, nil },
|
||||
MockNewBuilder: func() *resource.Builder { return nil },
|
||||
MockToRawKubeConfigLoader: func() clientcmd.ClientConfig { return nil },
|
||||
MockClientForMapping: func() (resource.RESTClient, error) { return nil, nil },
|
||||
}
|
||||
}
|
||||
|
||||
type MockClientConfig struct {
|
||||
clientcmd.DirectClientConfig
|
||||
MockNamespace func() (string, bool, error)
|
||||
}
|
||||
|
||||
func (c MockClientConfig) Namespace() (string, bool, error) { return c.MockNamespace() }
|
||||
|
||||
func (c *MockClientConfig) WithNamespace(s string, b bool, err error) *MockClientConfig {
|
||||
c.MockNamespace = func() (string, bool, error) { return s, b, err }
|
||||
return c
|
||||
}
|
||||
|
||||
func NewMockClientConfig() *MockClientConfig {
|
||||
return &MockClientConfig{
|
||||
MockNamespace: func() (string, bool, error) { return "test", false, nil },
|
||||
}
|
||||
}
|
||||
|
||||
func NewFakeFactoryForRC(t *testing.T, filenameRC string) *cmdtesting.TestFactory {
|
||||
c := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
f := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
|
||||
f.ClientConfigVal = cmdtesting.DefaultClientConfig()
|
||||
|
||||
pathRC := "/namespaces/test/replicationcontrollers/test-rc"
|
||||
get := "GET"
|
||||
_, rcBytes := readReplicationController(t, filenameRC, c)
|
||||
|
||||
f.UnstructuredClient = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: "v1"},
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == pathRC && m == get:
|
||||
bodyRC := ioutil.NopCloser(bytes.NewReader(rcBytes))
|
||||
return &http.Response{StatusCode: http.StatusOK,
|
||||
Header: cmdtesting.DefaultHeader(),
|
||||
Body: bodyRC}, nil
|
||||
case p == "/namespaces/test/replicationcontrollers" && m == get:
|
||||
bodyRC := ioutil.NopCloser(bytes.NewReader(rcBytes))
|
||||
return &http.Response{StatusCode: http.StatusOK,
|
||||
Header: cmdtesting.DefaultHeader(),
|
||||
Body: bodyRC}, nil
|
||||
case p == "/namespaces/test/replicationcontrollers/no-match" && m == get:
|
||||
return &http.Response{StatusCode: http.StatusNotFound,
|
||||
Header: cmdtesting.DefaultHeader(),
|
||||
Body: cmdtesting.ObjBody(c, &corev1.Pod{})}, nil
|
||||
case p == "/api/v1/namespaces/test" && m == get:
|
||||
return &http.Response{StatusCode: http.StatusOK,
|
||||
Header: cmdtesting.DefaultHeader(),
|
||||
Body: cmdtesting.ObjBody(c, &corev1.Namespace{})}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// Below functions are taken from Kubectl library.
|
||||
// https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/apply/apply_test.go
|
||||
func readReplicationController(t *testing.T, filenameRC string, c runtime.Codec) (string, []byte) {
|
||||
t.Helper()
|
||||
rcObj := readReplicationControllerFromFile(t, filenameRC, c)
|
||||
metaAccessor, err := meta.Accessor(rcObj)
|
||||
require.NoError(t, err, "Could not read replcation controller")
|
||||
rcBytes, err := runtime.Encode(c, rcObj)
|
||||
require.NoError(t, err, "Could not read replcation controller")
|
||||
return metaAccessor.GetName(), rcBytes
|
||||
}
|
||||
|
||||
func readReplicationControllerFromFile(t *testing.T,
|
||||
filename string, c runtime.Decoder) *corev1.ReplicationController {
|
||||
data := readBytesFromFile(t, filename)
|
||||
rc := corev1.ReplicationController{}
|
||||
require.NoError(t, runtime.DecodeInto(c, data, &rc), "Could not read replcation controller")
|
||||
|
||||
return &rc
|
||||
}
|
||||
|
||||
func readBytesFromFile(t *testing.T, filename string) []byte {
|
||||
file, err := os.Open(filename)
|
||||
require.NoError(t, err, "Could not read file")
|
||||
defer file.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(file)
|
||||
require.NoError(t, err, "Could not read file")
|
||||
|
||||
return data
|
||||
}
|
Loading…
Reference in New Issue
Block a user