diff --git a/pkg/k8s/kubectl/apply_options_test.go b/pkg/k8s/kubectl/apply_options_test.go index 59302e212..227771c82 100644 --- a/pkg/k8s/kubectl/apply_options_test.go +++ b/pkg/k8s/kubectl/apply_options_test.go @@ -19,7 +19,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "k8s.io/cli-runtime/pkg/genericclioptions" cmdutil "k8s.io/kubectl/pkg/cmd/util" @@ -38,19 +37,6 @@ var ( ErrNamespaceError = errors.New("ErrNamespaceError") ) -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.SetDryRun(true) - aa.SetSourceFiles([]string{filenameRC}) - assert.NoError(t, aa.Run()) -} - func TestNewApplyOptionsFactoryFailures(t *testing.T) { tests := []struct { f cmdutil.Factory diff --git a/pkg/k8s/kubectl/kubectl_test.go b/pkg/k8s/kubectl/kubectl_test.go index cfed67595..3db24948c 100644 --- a/pkg/k8s/kubectl/kubectl_test.go +++ b/pkg/k8s/kubectl/kubectl_test.go @@ -20,6 +20,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" "opendev.org/airship/airshipctl/pkg/document" "opendev.org/airship/airshipctl/pkg/k8s/kubectl" @@ -68,18 +69,28 @@ func TestNewKubectlFromKubeConfigPath(t *testing.T) { } func TestApply(t *testing.T) { - f := k8stest.NewFakeFactoryForRC(t, filenameRC) + b := testutil.NewTestBundle(t, fixtureDir) + docs, err := b.GetByAnnotation("airshipit.org/initinfra") + require.NoError(t, err, "failed to get documents from bundle") + replicationController, err := b.SelectOne(document.NewSelector().ByKind("ReplicationController")) + require.NoError(t, err) + rcBytes, err := replicationController.AsYAML() + require.NoError(t, err) + f := k8stest.FakeFactory(t, + []k8stest.ClientHandler{ + &k8stest.GenericHandler{ + Obj: &corev1.ReplicationController{}, + Bytes: rcBytes, + URLPath: "/namespaces/%s/replicationcontrollers", + Namespace: replicationController.GetNamespace(), + }, + }) 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.SetDryRun(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 diff --git a/pkg/phase/apply/apply_test.go b/pkg/phase/apply/apply_test.go index ce92082c2..9e3b59ffe 100644 --- a/pkg/phase/apply/apply_test.go +++ b/pkg/phase/apply/apply_test.go @@ -21,18 +21,20 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "opendev.org/airship/airshipctl/pkg/document" "opendev.org/airship/airshipctl/pkg/environment" "opendev.org/airship/airshipctl/pkg/k8s/client" "opendev.org/airship/airshipctl/pkg/k8s/client/fake" "opendev.org/airship/airshipctl/pkg/k8s/kubectl" "opendev.org/airship/airshipctl/pkg/phase/apply" + "opendev.org/airship/airshipctl/testutil" "opendev.org/airship/airshipctl/testutil/k8sutils" ) const ( kubeconfigPath = "testdata/kubeconfig.yaml" - filenameRC = "testdata/primary/site/test-site/ephemeral/initinfra/replicationcontroller.yaml" airshipConfigFile = "testdata/config.yaml" ) @@ -42,14 +44,29 @@ var ( func TestDeploy(t *testing.T) { rs := makeNewFakeRootSettings(t, kubeconfigPath, airshipConfigFile) - tf := k8sutils.NewFakeFactoryForRC(t, filenameRC) - defer tf.Cleanup() + bundle := testutil.NewTestBundle(t, "testdata/primary/site/test-site/ephemeral/initinfra") + replicationController, err := bundle.SelectOne(document.NewSelector().ByKind("ReplicationController")) + require.NoError(t, err) + b, err := replicationController.AsYAML() + require.NoError(t, err) + f := k8sutils.FakeFactory(t, + []k8sutils.ClientHandler{ + &k8sutils.InventoryObjectHandler{}, + &k8sutils.NamespaceHandler{}, + &k8sutils.GenericHandler{ + Obj: &corev1.ReplicationController{}, + Bytes: b, + URLPath: "/namespaces/%s/replicationcontrollers", + Namespace: replicationController.GetNamespace(), + }, + }) + defer f.Cleanup() ao := apply.NewOptions(rs) ao.PhaseName = "initinfra" ao.DryRun = true - kctl := kubectl.NewKubectl(tf) + kctl := kubectl.NewKubectl(f) tests := []struct { theApplyOptions *apply.Options diff --git a/testutil/k8sutils/mock_kubectl_factory.go b/testutil/k8sutils/mock_kubectl_factory.go index f6698d709..3b47f2511 100644 --- a/testutil/k8sutils/mock_kubectl_factory.go +++ b/testutil/k8sutils/mock_kubectl_factory.go @@ -16,16 +16,19 @@ package k8sutils import ( "bytes" + "fmt" "io/ioutil" "net/http" - "os" + "path" + "regexp" "testing" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "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" @@ -195,79 +198,183 @@ func NewMockClientConfig() *MockClientConfig { } } -// NewFakeFactoryForRC returns a fake Factory object for testing -// It is used to mock network interactions via a rest.Request -func NewFakeFactoryForRC(t *testing.T, filenameRC string) *cmdtesting.TestFactory { +// ClientHandler is an interface that can be injected into FakeFactory +// it's purpose to mock http request handling done by the Kubernetes Clients produced by cmdutils.Factory +type ClientHandler interface { + Handle(t *testing.T, req *http.Request) (*http.Response, bool, error) +} + +var ( + nsNamedPathRegex = regexp.MustCompile(`/api/v1/namespaces/([^/]+)`) + nsPath = "/api/v1/namespaces" +) + +// NamespaceHandler implements ClientHandler, that is to be used to handle +// Http Requests made by clients that are produced by cmdutils.Factory interface +type NamespaceHandler struct { +} + +var _ ClientHandler = &NamespaceHandler{} + +// Handle implements handler +func (h *NamespaceHandler) Handle(_ *testing.T, req *http.Request) (*http.Response, bool, error) { c := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + switch match, method := nsNamedPathRegex.FindStringSubmatch(req.URL.Path), req.Method; { + case match != nil && method == http.MethodGet: + ns := &corev1.Namespace{ + TypeMeta: v1.TypeMeta{ + Kind: "Namespace", + APIVersion: "v1"}, + ObjectMeta: v1.ObjectMeta{ + // check that this index exists is performed at case statement match != nil + // this means that [0] and [1] exist + Name: match[1], + }} + response := &http.Response{ + StatusCode: http.StatusOK, + Header: cmdtesting.DefaultHeader(), + Body: cmdtesting.ObjBody(c, ns)} + return response, true, nil + case req.URL.Path == nsPath && method == http.MethodPost: + ns := &corev1.Namespace{ + TypeMeta: v1.TypeMeta{ + Kind: "Namespace", + APIVersion: "v1"}, + } + response := &http.Response{StatusCode: http.StatusOK, + Header: cmdtesting.DefaultHeader(), + Body: cmdtesting.ObjBody(c, ns)} + return response, true, nil + } + + return nil, false, nil +} + +// InventoryObjectHandler handles configmap inventory object from cli-utils by mocking +// http calls made by clients produced by cmdutils.Factory interface +type InventoryObjectHandler struct { + inventoryObj *corev1.ConfigMap +} + +var _ ClientHandler = &InventoryObjectHandler{} + +var ( + cmPathRegex = regexp.MustCompile(`^/namespaces/([^/]+)/configmaps$`) + resourceNameRegexpString = `^[a-zA-Z]+-[a-z0-9]+$` + invObjNameRegex = regexp.MustCompile(resourceNameRegexpString) + invObjPathRegex = regexp.MustCompile(`^/namespaces/([^/]+)/configmaps/` + resourceNameRegexpString[:1]) + codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) +) + +// Handle implements handler +func (i *InventoryObjectHandler) Handle(t *testing.T, req *http.Request) (*http.Response, bool, error) { + if req.Method == http.MethodPost && cmPathRegex.Match([]byte(req.URL.Path)) { + b, err := ioutil.ReadAll(req.Body) + if err != nil { + return nil, false, err + } + cm := corev1.ConfigMap{} + err = runtime.DecodeInto(codec, b, &cm) + if err != nil { + return nil, false, err + } + if invObjNameRegex.Match([]byte(cm.Name)) { + i.inventoryObj = &cm + bodyRC := ioutil.NopCloser(bytes.NewReader(b)) + return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil + } + return nil, false, nil + } + + if req.Method == http.MethodGet && cmPathRegex.Match([]byte(req.URL.Path)) { + cmList := corev1.ConfigMapList{ + TypeMeta: v1.TypeMeta{ + APIVersion: "v1", + Kind: "List", + }, + Items: []corev1.ConfigMap{}, + } + if i.inventoryObj != nil { + cmList.Items = append(cmList.Items, *i.inventoryObj) + } + bodyRC := ioutil.NopCloser(bytes.NewReader(toJSONBytes(t, &cmList))) + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil + } + + if req.Method == http.MethodGet && invObjPathRegex.Match([]byte(req.URL.Path)) { + if i.inventoryObj == nil { + return &http.Response{ + StatusCode: http.StatusNotFound, + Header: cmdtesting.DefaultHeader(), + Body: cmdtesting.StringBody("")}, true, nil + } + bodyRC := ioutil.NopCloser(bytes.NewReader(toJSONBytes(t, i.inventoryObj))) + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil + } + return nil, false, nil +} + +// GenericHandler is a handler for generic objects +type GenericHandler struct { + Obj runtime.Object + Namespace string + // URLPath is a string for formater in which it should be defined how to inject a namespace into it + // example : /namespaces/%s/deployments + URLPath string + Bytes []byte +} + +var _ ClientHandler = &GenericHandler{} + +// Handle implements handler +func (g *GenericHandler) Handle(t *testing.T, req *http.Request) (*http.Response, bool, error) { + err := runtime.DecodeInto(codec, g.Bytes, g.Obj) + if err != nil { + return nil, false, err + } + accessor, err := meta.Accessor(g.Obj) + if err != nil { + return nil, false, err + } + basePath := fmt.Sprintf(g.URLPath, g.Namespace) + resourcePath := path.Join(basePath, accessor.GetName()) + if req.URL.Path == resourcePath && req.Method == http.MethodGet { + bodyRC := ioutil.NopCloser(bytes.NewReader(toJSONBytes(t, g.Obj))) + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil + } + if req.URL.Path == resourcePath && req.Method == http.MethodPatch { + bodyRC := ioutil.NopCloser(bytes.NewReader(toJSONBytes(t, g.Obj))) + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil + } + return nil, false, nil +} + +func toJSONBytes(t *testing.T, obj runtime.Object) []byte { + objBytes, err := runtime.Encode(unstructured.NewJSONFallbackEncoder(codec), obj) + require.NoError(t, err) + return objBytes +} + +// FakeFactory returns a fake factory based on provided handlers +func FakeFactory(t *testing.T, handlers []ClientHandler) *cmdtesting.TestFactory { 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"}, + defer f.Cleanup() + testRESTClient := &fake.RESTClient{ 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 + for _, h := range handlers { + resp, handled, err := h.Handle(t, req) + if handled { + return resp, err + } } + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + // dummy return + return nil, nil }), } + f.Client = testRESTClient + f.UnstructuredClient = testRESTClient 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 -}