diff --git a/go.mod b/go.mod index 2bb7dbf68..7b5869bac 100644 --- a/go.mod +++ b/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 diff --git a/pkg/k8s/kubectl/apply_options.go b/pkg/k8s/kubectl/apply_options.go new file mode 100644 index 000000000..54fd885f6 --- /dev/null +++ b/pkg/k8s/kubectl/apply_options.go @@ -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 +} diff --git a/pkg/k8s/kubectl/apply_options_test.go b/pkg/k8s/kubectl/apply_options_test.go new file mode 100644 index 000000000..756f7d0e8 --- /dev/null +++ b/pkg/k8s/kubectl/apply_options_test.go @@ -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) + } +} diff --git a/pkg/k8s/kubectl/filesystem.go b/pkg/k8s/kubectl/filesystem.go new file mode 100644 index 000000000..36e62c843 --- /dev/null +++ b/pkg/k8s/kubectl/filesystem.go @@ -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) +} diff --git a/pkg/k8s/kubectl/interfaces.go b/pkg/k8s/kubectl/interfaces.go new file mode 100644 index 000000000..a378614a5 --- /dev/null +++ b/pkg/k8s/kubectl/interfaces.go @@ -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 +} diff --git a/pkg/k8s/kubectl/kubectl.go b/pkg/k8s/kubectl/kubectl.go new file mode 100644 index 000000000..17d9a9d2f --- /dev/null +++ b/pkg/k8s/kubectl/kubectl.go @@ -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) +} diff --git a/pkg/k8s/kubectl/kubectl_test.go b/pkg/k8s/kubectl/kubectl_test.go new file mode 100644 index 000000000..07b81eda9 --- /dev/null +++ b/pkg/k8s/kubectl/kubectl_test.go @@ -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) + } +} diff --git a/pkg/k8s/kubectl/testdata/kubeconfig.yaml b/pkg/k8s/kubectl/testdata/kubeconfig.yaml new file mode 100644 index 000000000..967864a76 --- /dev/null +++ b/pkg/k8s/kubectl/testdata/kubeconfig.yaml @@ -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= diff --git a/pkg/k8s/kubectl/testdata/kustomization.yaml b/pkg/k8s/kubectl/testdata/kustomization.yaml new file mode 100644 index 000000000..72ab8548e --- /dev/null +++ b/pkg/k8s/kubectl/testdata/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - replicationcontroller.yaml diff --git a/pkg/k8s/kubectl/testdata/replicationcontroller.yaml b/pkg/k8s/kubectl/testdata/replicationcontroller.yaml new file mode 100644 index 000000000..1e39abdd8 --- /dev/null +++ b/pkg/k8s/kubectl/testdata/replicationcontroller.yaml @@ -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 diff --git a/pkg/k8s/utils/utils.go b/pkg/k8s/utils/utils.go new file mode 100644 index 000000000..2977eede9 --- /dev/null +++ b/pkg/k8s/utils/utils.go @@ -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) +} diff --git a/testutil/k8sutils/mock_kubectl_factory.go b/testutil/k8sutils/mock_kubectl_factory.go new file mode 100644 index 000000000..2bb13c8fc --- /dev/null +++ b/testutil/k8sutils/mock_kubectl_factory.go @@ -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 +}