diff --git a/cmd/up/profile/set.go b/cmd/up/profile/set.go index 3dcaa038..ab46495b 100644 --- a/cmd/up/profile/set.go +++ b/cmd/up/profile/set.go @@ -15,20 +15,28 @@ package profile import ( - _ "embed" + "context" "fmt" "github.com/alecthomas/kong" - "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/pterm/pterm" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/upbound/up/internal/profile" "github.com/upbound/up/internal/upbound" + + _ "embed" ) const ( errSetProfile = "unable to set profile" errUpdateConfig = "unable to update config file" + errNoSpace = "cannot find Spaces in the Kubernetes cluster. Run 'up space init' to install Spaces." + errKubeContact = "unable to check for Spaces on Kubernetes cluster" ) type setCmd struct { @@ -37,6 +45,8 @@ type setCmd struct { type spaceCmd struct { Kube upbound.KubeFlags `embed:""` + + getClient func() (kubernetes.Interface, error) } //go:embed space_help.txt @@ -50,7 +60,7 @@ func (c *spaceCmd) AfterApply(kongCtx *kong.Context) error { return c.Kube.AfterApply() } -func (c *spaceCmd) Run(p pterm.TextPrinter, upCtx *upbound.Context) error { +func (c *spaceCmd) Run(ctx context.Context, p pterm.TextPrinter, upCtx *upbound.Context) error { setDefault := false // If profile name was not provided and no default exists, set name to @@ -69,6 +79,14 @@ func (c *spaceCmd) Run(p pterm.TextPrinter, upCtx *upbound.Context) error { BaseConfig: upCtx.Profile.BaseConfig, } + installed, err := c.checkForSpaces(ctx) + if err != nil { + return err + } + if !installed { + return errors.New(errNoSpace) + } + if err := upCtx.Cfg.AddOrUpdateUpboundProfile(upCtx.ProfileName, prof); err != nil { return errors.Wrap(err, errSetProfile) } @@ -95,3 +113,24 @@ func (c *spaceCmd) Run(p pterm.TextPrinter, upCtx *upbound.Context) error { return nil } + +func (c *spaceCmd) checkForSpaces(ctx context.Context) (bool, error) { + kubeconfig := c.Kube.GetConfig() + var kClient kubernetes.Interface + var err error + if c.getClient != nil { + kClient, err = c.getClient() + } else { + kClient, err = kubernetes.NewForConfig(kubeconfig) + } + if err != nil { + return false, err + } + if _, err := kClient.AppsV1().Deployments("upbound-system").Get(ctx, "mxe-controller", metav1.GetOptions{}); kerrors.IsNotFound(err) { + return false, nil + } else if err != nil { + return false, errors.Wrap(err, errKubeContact) + } + + return true, nil +} diff --git a/cmd/up/profile/set_test.go b/cmd/up/profile/set_test.go index 4931b76a..4cb9b963 100644 --- a/cmd/up/profile/set_test.go +++ b/cmd/up/profile/set_test.go @@ -15,13 +15,22 @@ package profile import ( + "context" "io" "os" "testing" - "github.com/crossplane/crossplane-runtime/pkg/test" "github.com/google/go-cmp/cmp" "github.com/pterm/pterm" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" + clientgotesting "k8s.io/client-go/testing" + + "github.com/crossplane/crossplane-runtime/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/test" "github.com/upbound/up/internal/config" "github.com/upbound/up/internal/profile" @@ -54,8 +63,16 @@ func TestSpaceCmd_Run(t *testing.T) { } kubeconfig := wd + "/testdata/kubeconfig" + mxeController := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mxe-controller", + Namespace: "upbound-system", + }, + } + type args struct { - ctx *upbound.Context + ctx *upbound.Context + getClient func() (kubernetes.Interface, error) } type want struct { cfg *config.Config @@ -73,6 +90,9 @@ func TestSpaceCmd_Run(t *testing.T) { Account: "test-account", Cfg: &config.Config{}, }, + getClient: func() (kubernetes.Interface, error) { + return fake.NewSimpleClientset(mxeController), nil + }, }, want: want{ cfg: &config.Config{Upbound: config.Upbound{ @@ -88,6 +108,42 @@ func TestSpaceCmd_Run(t *testing.T) { }}, }, }, + "NoSpacesError": { + reason: "There is no upbound/mxe-controller in the cluster.", + args: args{ + ctx: &upbound.Context{ + Account: "test-account", + Cfg: &config.Config{}, + }, + getClient: func() (kubernetes.Interface, error) { + return fake.NewSimpleClientset(), nil + }, + }, + want: want{ + err: errors.New(errNoSpace), + cfg: &config.Config{}, + }, + }, + "KubeClientError": { + reason: "The kube clients returns a non-NotFound error.", + args: args{ + ctx: &upbound.Context{ + Account: "test-account", + Cfg: &config.Config{}, + }, + getClient: func() (kubernetes.Interface, error) { + c := fake.NewSimpleClientset() + c.PrependReactor("get", "deployments", func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, errors.New("contact error") + }) + return c, nil + }, + }, + want: want{ + err: errors.Wrap(errors.New("contact error"), errKubeContact), + cfg: &config.Config{}, + }, + }, "PopulatedConfigDefaultProfile": { reason: "Setting the default profile with populated config updates the default profile.", args: args{ @@ -111,6 +167,9 @@ func TestSpaceCmd_Run(t *testing.T) { }, }}, }, + getClient: func() (kubernetes.Interface, error) { + return fake.NewSimpleClientset(mxeController), nil + }, }, want: want{ cfg: &config.Config{Upbound: config.Upbound{ @@ -150,6 +209,9 @@ func TestSpaceCmd_Run(t *testing.T) { }, }}, }, + getClient: func() (kubernetes.Interface, error) { + return fake.NewSimpleClientset(mxeController), nil + }, }, want: want{ cfg: &config.Config{Upbound: config.Upbound{ @@ -195,6 +257,9 @@ func TestSpaceCmd_Run(t *testing.T) { }, }}, }, + getClient: func() (kubernetes.Interface, error) { + return fake.NewSimpleClientset(mxeController), nil + }, }, want: want{ cfg: &config.Config{Upbound: config.Upbound{ @@ -220,7 +285,7 @@ func TestSpaceCmd_Run(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { - cmd := &spaceCmd{Kube: upbound.KubeFlags{Kubeconfig: kubeconfig}} + cmd := &spaceCmd{Kube: upbound.KubeFlags{Kubeconfig: kubeconfig}, getClient: tc.args.getClient} if diff := cmp.Diff(nil, cmd.AfterApply(nil), test.EquateErrors()); diff != "" { t.Errorf("\n%s\nspaceCmd.AfterApply(...): -want error, +got error:\n%s", tc.reason, diff) } @@ -228,7 +293,7 @@ func TestSpaceCmd_Run(t *testing.T) { cfgSrc := &mockConfigSource{cfg: tc.args.ctx.Cfg} tc.args.ctx.CfgSrc = cfgSrc p := pterm.DefaultBasicText.WithWriter(io.Discard) - if diff := cmp.Diff(tc.want.err, cmd.Run(p, tc.args.ctx), test.EquateErrors()); diff != "" { + if diff := cmp.Diff(tc.want.err, cmd.Run(context.TODO(), p, tc.args.ctx), test.EquateErrors()); diff != "" { t.Errorf("\n%s\nspaceCmd.Run(...): -want error, +got error:\n%s", tc.reason, diff) } if diff := cmp.Diff(tc.want.cfg, cfgSrc.cfg); diff != "" {