diff --git a/cmd/common_test.go b/cmd/common_test.go index cb8dfa76..490d253b 100644 --- a/cmd/common_test.go +++ b/cmd/common_test.go @@ -37,6 +37,8 @@ type test[R any] struct { fsMocks func(fs afero.Fs, want R) cmd func(want R) []string + disableMockClient bool // can switch off mock client creation + wantErr error want R // for json and yaml wantTable *string // for table printer @@ -91,14 +93,22 @@ func (c *test[R]) newMockConfig(t *testing.T) (*client.MetalMockClient, *bytes.B c.fsMocks(fs, c.want) } - var out bytes.Buffer + var ( + out bytes.Buffer + config = &config{ + fs: fs, + client: client, + out: &out, + log: zaptest.NewLogger(t).Sugar(), + comp: &completion.Completion{}, + } + ) - return mock, &out, &config{ - fs: fs, - out: &out, - client: client, - log: zaptest.NewLogger(t).Sugar(), + if c.disableMockClient { + config.client = nil } + + return mock, &out, config } func assertExhaustiveArgs(t *testing.T, args []string, exclude ...string) { diff --git a/cmd/firewall_test.go b/cmd/firewall_test.go index 6aa7619b..c386c9b6 100644 --- a/cmd/firewall_test.go +++ b/cmd/firewall_test.go @@ -1,7 +1,6 @@ package cmd import ( - "fmt" "strings" "testing" "time" @@ -301,8 +300,6 @@ ID AGE HOSTNAME PROJECT NETWORKS IPS PARTITION "--tags", strings.Join(want.Tags, ","), "--userdata", want.Allocation.UserData, } - - fmt.Println(args) assertExhaustiveArgs(t, args, "file") return args }, diff --git a/cmd/root.go b/cmd/root.go index 9f2aa85e..1347b863 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -65,10 +65,12 @@ func newRootCmd(c *config) *cobra.Command { Short: "a cli to manage entities in the metal-stack api", SilenceUsage: true, PersistentPreRun: func(cmd *cobra.Command, args []string) { + viper.SetFs(c.fs) must(viper.BindPFlags(cmd.Flags())) must(viper.BindPFlags(cmd.PersistentFlags())) // we cannot instantiate the config earlier because // cobra flags do not work so early in the game + must(readConfigFile()) must(initConfigWithViperCtx(c)) }, } @@ -130,13 +132,8 @@ metalctl machine list -o template --template "{{ .id }}:{{ .size.id }}" rootCmd.AddCommand(newLogoutCmd(c)) rootCmd.AddCommand(newWhoamiCmd(c)) rootCmd.AddCommand(newContextCmd(c)) - rootCmd.AddCommand(newUpdateCmd()) - cobra.OnInitialize(func() { - must(readConfigFile()) - }) - return rootCmd } @@ -202,7 +199,7 @@ func initConfigWithViperCtx(c *config) error { if hmacKey == "" && ctx.HMAC != nil { hmacKey = *ctx.HMAC } - apiToken := viper.GetString("apitoken") + apiToken := viper.GetString("api-token") // if there is no api token explicitly specified we try to pull it out of the kubeconfig context if apiToken == "" { diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 00000000..f867e30d --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,108 @@ +package cmd + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/metal-stack/metal-go/api/models" + "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/metal-stack/metal-lib/rest" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_BasicRootCmdStuff(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if strings.HasPrefix(authHeader, "Bearer") { + assert.Equal(t, authHeader, "Bearer i-am-token") + } else if strings.HasPrefix(authHeader, "Metal-Admin") { + assert.Len(t, strings.Split(authHeader, " "), 2) + } else { + assert.Fail(t, "missing auth header") + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := w.Write(mustMarshal(t, &models.RestHealthResponse{ + Status: pointer.Pointer(string(rest.HealthStatusHealthy)), + })) + assert.NoError(t, err) + })) + defer ts.Close() + + tests := []*test[*rest.HealthResponse]{ + { + name: "overwrite api-url and api-token from config-file", + fsMocks: func(fs afero.Fs, want *rest.HealthResponse) { + require.NoError(t, afero.WriteFile(fs, fmt.Sprintf("/etc/%s/config.yaml", binaryName), []byte(fmt.Sprintf(`--- +api-url: "%s" +api-token: "i-am-token" +`, ts.URL)), 0755)) + }, + cmd: func(want *rest.HealthResponse) []string { + return []string{"health"} + }, + disableMockClient: true, + want: &rest.HealthResponse{ + Status: rest.HealthStatusHealthy, + }, + }, + { + name: "overwrite api-url and api-token from user-given config-file path", + fsMocks: func(fs afero.Fs, want *rest.HealthResponse) { + require.NoError(t, afero.WriteFile(fs, "/config.yaml", []byte(fmt.Sprintf(`--- +api-url: "%s" +api-token: "i-am-token" +`, ts.URL)), 0755)) + }, + cmd: func(want *rest.HealthResponse) []string { + return []string{"health", "--config", "/config.yaml"} + }, + disableMockClient: true, + want: &rest.HealthResponse{ + Status: rest.HealthStatusHealthy, + }, + }, + { + name: "overwrite api-url and api-token from command line", + cmd: func(want *rest.HealthResponse) []string { + return []string{"health", "--api-url", ts.URL, "--api-token", "i-am-token"} + }, + disableMockClient: true, + want: &rest.HealthResponse{ + Status: rest.HealthStatusHealthy, + }, + }, + { + name: "overwrite api-url and api-token from environment", + cmd: func(want *rest.HealthResponse) []string { + t.Setenv("METALCTL_API_URL", ts.URL) + t.Setenv("METALCTL_API_TOKEN", "i-am-token") + return []string{"health"} + }, + disableMockClient: true, + want: &rest.HealthResponse{ + Status: rest.HealthStatusHealthy, + }, + }, + { + name: "use hmac", + cmd: func(want *rest.HealthResponse) []string { + t.Setenv("METALCTL_HMAC", "i-am-hmac") + return []string{"health"} + }, + disableMockClient: true, + want: &rest.HealthResponse{ + Status: rest.HealthStatusHealthy, + }, + }, + } + for _, tt := range tests { + tt.testCmd(t) + } +} diff --git a/pkg/api/context.go b/pkg/api/context.go index 722087b9..e5a32a40 100644 --- a/pkg/api/context.go +++ b/pkg/api/context.go @@ -34,7 +34,7 @@ var defaultCtx = Context{ func GetContexts() (*Contexts, error) { var ctxs Contexts - cfgFile := viper.GetViper().ConfigFileUsed() + cfgFile := viper.ConfigFileUsed() c, err := os.ReadFile(cfgFile) if err != nil { return nil, fmt.Errorf("unable to read config, please create a config.yaml in either: /etc/metalctl/, $HOME/.metalctl/ or in the current directory, see metalctl ctx -h for examples") @@ -48,7 +48,7 @@ func WriteContexts(ctxs *Contexts) error { if err != nil { return err } - cfgFile := viper.GetViper().ConfigFileUsed() + cfgFile := viper.ConfigFileUsed() err = os.WriteFile(cfgFile, c, 0600) if err != nil { return err