Merge "[#20] Add kubectl apply wrapper package"
This commit is contained in:
commit
51a5163e74
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-billy.v4 v4.3.2
|
||||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0
|
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0
|
||||||
gopkg.in/src-d/go-git.v4 v4.13.1
|
gopkg.in/src-d/go-git.v4 v4.13.1
|
||||||
|
k8s.io/api v0.0.0
|
||||||
k8s.io/apimachinery 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/client-go v11.0.0+incompatible
|
||||||
|
k8s.io/kubectl v0.0.0
|
||||||
k8s.io/kubernetes v1.16.3
|
k8s.io/kubernetes v1.16.3
|
||||||
opendev.org/airship/go-redfish v0.0.0-20200110185254-3ab47e28bae8
|
opendev.org/airship/go-redfish v0.0.0-20200110185254-3ab47e28bae8
|
||||||
opendev.org/airship/go-redfish/client 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