From 8b757b19fd638d9d5d7526abde289628423f22b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Thu, 8 May 2025 23:54:35 +0200 Subject: [PATCH 01/10] core: Add STACKIT CLI Auth flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Otto Kröpke --- .github/workflows/ci.yaml | 2 + CHANGELOG.md | 4 + core/CHANGELOG.md | 4 + core/auth/auth.go | 30 +++++- core/clients/stackit_cli_flow.go | 77 ++++++++++++++ core/clients/stackit_cli_flow_test.go | 143 ++++++++++++++++++++++++++ core/config/config.go | 1 + 7 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 core/clients/stackit_cli_flow.go create mode 100644 core/clients/stackit_cli_flow_test.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 06882911b..10c49a010 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,6 +17,8 @@ jobs: uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} + - name: Install STACKIT CLI + uses: jkroepke/setup-stackit-cli@v1 - name: Test run: make test diff --git a/CHANGELOG.md b/CHANGELOG.md index 764e3c089..e0d2dc227 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Release (2025-XX-YY) +- `core`: [v0.16.2](core/CHANGELOG.md#v0162-2025-XX-YY) + - **New:** If a custom http.Client is provided, the http.Transport is respected. This allows customizing the http.Client with custom timeouts or instrumentation. + ## Release (2025-03-14) - `certificates`: [v1.0.0](services/certificates/CHANGELOG.md#v100-2025-03-14) - **Breaking Change:** The region is no longer specified within the client configuration. Instead, the region must be passed as a parameter to any region-specific request. diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 194b06fe6..9d373a481 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -1,3 +1,7 @@ +## v0.16.2 (2025-XX-YY) + +- **New:** Add STACKIT CLI auth flow. + ## v0.16.1 (2025-02-25) - **Bugfix:** STACKIT_PRIVATE_KEY and STACKIT_SERVICE_ACCOUNT_KEY can be set via environment variable or via credentials file. diff --git a/core/auth/auth.go b/core/auth/auth.go index 19e400f94..579b68a45 100644 --- a/core/auth/auth.go +++ b/core/auth/auth.go @@ -2,6 +2,7 @@ package auth import ( "encoding/json" + "errors" "fmt" "net/http" "os" @@ -62,7 +63,14 @@ func SetupAuth(cfg *config.Configuration) (rt http.RoundTripper, err error) { return nil, fmt.Errorf("configuring token authentication: %w", err) } return tokenRoundTripper, nil + } else if !cfg.DisableCLIAuthFlow { + cliRoundTripper, err := StackitCliAuth(cfg) + if err != nil { + return nil, fmt.Errorf("configuring CLI authentication: %w", err) + } + return cliRoundTripper, nil } + authRoundTripper, err := DefaultAuth(cfg) if err != nil { return nil, fmt.Errorf("configuring default authentication: %w", err) @@ -90,7 +98,17 @@ func DefaultAuth(cfg *config.Configuration) (rt http.RoundTripper, err error) { // Token flow rt, err = TokenAuth(cfg) if err != nil { - return nil, fmt.Errorf("no valid credentials were found: trying key flow: %s, trying token flow: %w", keyFlowErr.Error(), err) + tokenFlowErr := err + if cfg.DisableCLIAuthFlow { + err = errors.New("CLI flow disabled") + } else { + // Stackit CLI flow + rt, err = StackitCliAuth(cfg) + } + + if err != nil { + return nil, fmt.Errorf("no valid credentials were found: trying key flow: %s, trying token flow: %s, trying stackit cli flow: %w", keyFlowErr.Error(), tokenFlowErr.Error(), err) + } } } return rt, nil @@ -195,6 +213,16 @@ func KeyAuth(cfg *config.Configuration) (http.RoundTripper, error) { return client, nil } +// StackitCliAuth configures the [clients.STACKITCLIFlow] and returns an http.RoundTripper +func StackitCliAuth(_ *config.Configuration) (http.RoundTripper, error) { + client := &clients.STACKITCLIFlow{} + if err := client.Init(&clients.STACKITCLIFlowConfig{}); err != nil { + return nil, fmt.Errorf("error initializing client: %w", err) + } + + return client, nil +} + // readCredentialsFile reads the credentials file from the specified path and returns Credentials func readCredentialsFile(path string) (*Credentials, error) { if path == "" { diff --git a/core/clients/stackit_cli_flow.go b/core/clients/stackit_cli_flow.go new file mode 100644 index 000000000..25c92f6d2 --- /dev/null +++ b/core/clients/stackit_cli_flow.go @@ -0,0 +1,77 @@ +package clients + +import ( + "bytes" + "context" + "errors" + "os" + "os/exec" + "runtime" + "strings" +) + +// STACKITCLIFlow invoke the STACKIT CLI from PATH to get the access token. +// If successful, then token is passed to clients.TokenFlow. +type STACKITCLIFlow struct { + TokenFlow +} + +// STACKITCLIFlowConfig is the flow config +type STACKITCLIFlowConfig struct{} + +// GetConfig returns the flow configuration +func (c *STACKITCLIFlow) GetConfig() STACKITCLIFlowConfig { + return STACKITCLIFlowConfig{} +} + +func (c *STACKITCLIFlow) Init(_ *STACKITCLIFlowConfig) error { + token, err := c.getTokenFromCLI() + if err != nil { + return err + } + + c.config = &TokenFlowConfig{ + ServiceAccountToken: strings.TrimSpace(token), + } + + c.configureHTTPClient() + return c.validate() +} + +func (c *STACKITCLIFlow) getTokenFromCLI() (string, error) { + return runSTACKITCLICommand(context.TODO(), "stackit auth get-access-token") +} + +// runSTACKITCLICommand executes the command line and returns the output. +func runSTACKITCLICommand(ctx context.Context, commandLine string) (string, error) { + var cliCmd *exec.Cmd + if runtime.GOOS == "windows" { + dir := os.Getenv("SYSTEMROOT") + if dir == "" { + return "", errors.New("environment variable 'SYSTEMROOT' has no value") + } + cliCmd = exec.CommandContext(ctx, "cmd.exe", "/c", commandLine) + cliCmd.Dir = dir + } else { + cliCmd = exec.CommandContext(ctx, "/bin/sh", "-c", commandLine) + cliCmd.Dir = "/bin" + } + cliCmd.Env = os.Environ() + var stderr bytes.Buffer + cliCmd.Stderr = &stderr + + output, err := cliCmd.Output() + if err != nil { + msg := stderr.String() + var exErr *exec.ExitError + if errors.As(err, &exErr) && exErr.ExitCode() == 127 || strings.HasPrefix(msg, "'stackit' is not recognized") { + msg = "STACKIT CLI not found on path" + } + if msg == "" { + msg = err.Error() + } + return "", errors.New(msg) + } + + return string(output), nil +} diff --git a/core/clients/stackit_cli_flow_test.go b/core/clients/stackit_cli_flow_test.go new file mode 100644 index 000000000..c79b34177 --- /dev/null +++ b/core/clients/stackit_cli_flow_test.go @@ -0,0 +1,143 @@ +package clients + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +const testServiceAccountToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImR1bW15QGV4YW1wbGUuY29tIiwiZXhwIjo5MDA3MTkyNTQ3NDA5OTF9.sM2yd5GL9kK4h8IKHbr_fA2XmrzEsLOeLTIPrU0VfMg" + +func TestSTACKITCLIFlow_Init(t *testing.T) { + type args struct { + cfg *STACKITCLIFlowConfig + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"ok", args{&STACKITCLIFlowConfig{}}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &STACKITCLIFlow{} + + _, _ = runSTACKITCLICommand(t.Context(), "stackit config profile delete test-stackit-cli-flow-init -y") + _, err := runSTACKITCLICommand(t.Context(), "stackit config profile create test-stackit-cli-flow-init") + if err != nil { + t.Errorf("runSTACKITCLICommand() error = %v", err) + return + } + + _, err = runSTACKITCLICommand(t.Context(), "stackit auth activate-service-account --service-account-token="+testServiceAccountToken) + if err != nil { + t.Errorf("runSTACKITCLICommand() error = %v", err) + return + } + + defer func() { + _, _ = runSTACKITCLICommand(t.Context(), "stackit config profile delete test-stackit-cli-flow-init -y") + }() + + if err := c.Init(tt.args.cfg); (err != nil) != tt.wantErr { + t.Errorf("TokenFlow.Init() error = %v, wantErr %v", err, tt.wantErr) + } + if c.config == nil { + t.Error("config is nil") + } + }) + } +} + +func TestSTACKITCLIFlow_Do(t *testing.T) { + type fields struct { + client *http.Client + config *STACKITCLIFlowConfig + } + type args struct{} + tests := []struct { + name string + fields fields + args args + want int + wantErr bool + }{ + {"success", fields{&http.Client{}, &STACKITCLIFlowConfig{}}, args{}, http.StatusOK, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, _ = runSTACKITCLICommand(t.Context(), "stackit config profile delete test-stackit-cli-flow-do -y") + _, err := runSTACKITCLICommand(t.Context(), "stackit config profile create test-stackit-cli-flow-do") + if err != nil { + t.Errorf("runSTACKITCLICommand() error = %v", err) + return + } + + _, err = runSTACKITCLICommand(t.Context(), "stackit auth activate-service-account --service-account-token="+testServiceAccountToken) + if err != nil { + t.Errorf("runSTACKITCLICommand() error = %v", err) + return + } + + defer func() { + _, _ = runSTACKITCLICommand(t.Context(), "stackit config profile delete test-stackit-cli-flow-do -y") + }() + + c := &STACKITCLIFlow{} + err = c.Init(tt.fields.config) + if err != nil { + t.Errorf("Init() error = %v", err) + return + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authorization := r.Header.Get("Authorization") + if authorization != "Bearer "+testServiceAccountToken { + w.WriteHeader(http.StatusUnauthorized) + _, _ = fmt.Fprintln(w, `{"error":"missing authorization header"}`) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintln(w, `{"status":"ok"}`) + }) + server := httptest.NewServer(handler) + defer server.Close() + + u, err := url.Parse(server.URL) + if err != nil { + t.Error(err) + return + } + req, err := http.NewRequest(http.MethodGet, u.String(), http.NoBody) + if err != nil { + t.Error(err) + return + } + got, err := c.RoundTrip(req) + if err == nil { + // Defer discard and close the body + defer func() { + if _, discardErr := io.Copy(io.Discard, got.Body); discardErr != nil && err == nil { + err = discardErr + } + if closeErr := got.Body.Close(); closeErr != nil && err == nil { + err = closeErr + } + }() + } + if (err != nil) != tt.wantErr { + t.Errorf("STACKITCLIFlow.Do() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != nil && got.StatusCode != tt.want { + t.Errorf("STACKITCLIFlow.Do() = %v, want %v", got.StatusCode, tt.want) + } + }) + } +} diff --git a/core/config/config.go b/core/config/config.go index 93002c02a..0559d53b1 100644 --- a/core/config/config.go +++ b/core/config/config.go @@ -90,6 +90,7 @@ type Configuration struct { CredentialsFilePath string `json:"credentialsFilePath,omitempty"` TokenCustomUrl string `json:"tokenCustomUrl,omitempty"` Region string `json:"region,omitempty"` + DisableCLIAuthFlow bool `json:"disableCLIAuthFlow,omitempty"` // Should be set to true, if called by STACKIT CLI to avoid infinite loops. CustomAuth http.RoundTripper Servers ServerConfigurations OperationServers map[string]ServerConfigurations From 057555484539387fa577a112edb94910205570c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Fri, 9 May 2025 00:30:09 +0200 Subject: [PATCH 02/10] fix merge conflicts --- core/auth/auth.go | 9 +++++++-- core/clients/stackit_cli_flow.go | 15 ++++++++------- core/clients/stackit_cli_flow_test.go | 21 +++++++++++++-------- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/core/auth/auth.go b/core/auth/auth.go index 35f99f4e1..0ec0592d1 100644 --- a/core/auth/auth.go +++ b/core/auth/auth.go @@ -235,9 +235,14 @@ func KeyAuth(cfg *config.Configuration) (http.RoundTripper, error) { } // StackitCliAuth configures the [clients.STACKITCLIFlow] and returns an http.RoundTripper -func StackitCliAuth(_ *config.Configuration) (http.RoundTripper, error) { +func StackitCliAuth(cfg *config.Configuration) (http.RoundTripper, error) { + cliCfg := clients.STACKITCLIFlowConfig{} + if cfg.HTTPClient != nil && cfg.HTTPClient.Transport != nil { + cliCfg.HTTPTransport = cfg.HTTPClient.Transport + } + client := &clients.STACKITCLIFlow{} - if err := client.Init(&clients.STACKITCLIFlowConfig{}); err != nil { + if err := client.Init(&cliCfg); err != nil { return nil, fmt.Errorf("error initializing client: %w", err) } diff --git a/core/clients/stackit_cli_flow.go b/core/clients/stackit_cli_flow.go index 25c92f6d2..cdb729dcb 100644 --- a/core/clients/stackit_cli_flow.go +++ b/core/clients/stackit_cli_flow.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "errors" + "net/http" "os" "os/exec" "runtime" @@ -17,25 +18,25 @@ type STACKITCLIFlow struct { } // STACKITCLIFlowConfig is the flow config -type STACKITCLIFlowConfig struct{} +type STACKITCLIFlowConfig struct { + HTTPTransport http.RoundTripper +} // GetConfig returns the flow configuration func (c *STACKITCLIFlow) GetConfig() STACKITCLIFlowConfig { return STACKITCLIFlowConfig{} } -func (c *STACKITCLIFlow) Init(_ *STACKITCLIFlowConfig) error { +func (c *STACKITCLIFlow) Init(cfg *STACKITCLIFlowConfig) error { token, err := c.getTokenFromCLI() if err != nil { return err } - c.config = &TokenFlowConfig{ + return c.TokenFlow.Init(&TokenFlowConfig{ ServiceAccountToken: strings.TrimSpace(token), - } - - c.configureHTTPClient() - return c.validate() + HTTPTransport: cfg.HTTPTransport, + }) } func (c *STACKITCLIFlow) getTokenFromCLI() (string, error) { diff --git a/core/clients/stackit_cli_flow_test.go b/core/clients/stackit_cli_flow_test.go index c79b34177..b555fc92c 100644 --- a/core/clients/stackit_cli_flow_test.go +++ b/core/clients/stackit_cli_flow_test.go @@ -1,6 +1,7 @@ package clients import ( + "context" "fmt" "io" "net/http" @@ -24,23 +25,25 @@ func TestSTACKITCLIFlow_Init(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + ctx := context.TODO() + c := &STACKITCLIFlow{} - _, _ = runSTACKITCLICommand(t.Context(), "stackit config profile delete test-stackit-cli-flow-init -y") - _, err := runSTACKITCLICommand(t.Context(), "stackit config profile create test-stackit-cli-flow-init") + _, _ = runSTACKITCLICommand(ctx, "stackit config profile delete test-stackit-cli-flow-init -y") + _, err := runSTACKITCLICommand(ctx, "stackit config profile create test-stackit-cli-flow-init") if err != nil { t.Errorf("runSTACKITCLICommand() error = %v", err) return } - _, err = runSTACKITCLICommand(t.Context(), "stackit auth activate-service-account --service-account-token="+testServiceAccountToken) + _, err = runSTACKITCLICommand(ctx, "stackit auth activate-service-account --service-account-token="+testServiceAccountToken) if err != nil { t.Errorf("runSTACKITCLICommand() error = %v", err) return } defer func() { - _, _ = runSTACKITCLICommand(t.Context(), "stackit config profile delete test-stackit-cli-flow-init -y") + _, _ = runSTACKITCLICommand(ctx, "stackit config profile delete test-stackit-cli-flow-init -y") }() if err := c.Init(tt.args.cfg); (err != nil) != tt.wantErr { @@ -70,21 +73,23 @@ func TestSTACKITCLIFlow_Do(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, _ = runSTACKITCLICommand(t.Context(), "stackit config profile delete test-stackit-cli-flow-do -y") - _, err := runSTACKITCLICommand(t.Context(), "stackit config profile create test-stackit-cli-flow-do") + ctx := context.TODO() + + _, _ = runSTACKITCLICommand(ctx, "stackit config profile delete test-stackit-cli-flow-do -y") + _, err := runSTACKITCLICommand(ctx, "stackit config profile create test-stackit-cli-flow-do") if err != nil { t.Errorf("runSTACKITCLICommand() error = %v", err) return } - _, err = runSTACKITCLICommand(t.Context(), "stackit auth activate-service-account --service-account-token="+testServiceAccountToken) + _, err = runSTACKITCLICommand(ctx, "stackit auth activate-service-account --service-account-token="+testServiceAccountToken) if err != nil { t.Errorf("runSTACKITCLICommand() error = %v", err) return } defer func() { - _, _ = runSTACKITCLICommand(t.Context(), "stackit config profile delete test-stackit-cli-flow-do -y") + _, _ = runSTACKITCLICommand(ctx, "stackit config profile delete test-stackit-cli-flow-do -y") }() c := &STACKITCLIFlow{} From 47d007f30f13d18800f06ee73ecb365cad118a57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Fri, 9 May 2025 07:21:29 +0200 Subject: [PATCH 03/10] add test, if token not present --- core/clients/stackit_cli_flow_test.go | 33 +++++++++++++++++++-------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/core/clients/stackit_cli_flow_test.go b/core/clients/stackit_cli_flow_test.go index b555fc92c..a8b3ac012 100644 --- a/core/clients/stackit_cli_flow_test.go +++ b/core/clients/stackit_cli_flow_test.go @@ -14,14 +14,28 @@ const testServiceAccountToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbC func TestSTACKITCLIFlow_Init(t *testing.T) { type args struct { - cfg *STACKITCLIFlowConfig + cfg *STACKITCLIFlowConfig + confFn func(t *testing.T) } tests := []struct { name string args args wantErr bool }{ - {"ok", args{&STACKITCLIFlowConfig{}}, false}, + {"ok", args{ + cfg: &STACKITCLIFlowConfig{}, + confFn: func(t *testing.T) { + _, err := runSTACKITCLICommand(context.TODO(), "stackit auth activate-service-account --service-account-token="+testServiceAccountToken) + if err != nil { + t.Errorf("runSTACKITCLICommand() error = %v", err) + return + } + }, + }, false}, + {"no token", args{ + cfg: &STACKITCLIFlowConfig{}, + confFn: func(t *testing.T) {}, + }, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -36,19 +50,20 @@ func TestSTACKITCLIFlow_Init(t *testing.T) { return } - _, err = runSTACKITCLICommand(ctx, "stackit auth activate-service-account --service-account-token="+testServiceAccountToken) - if err != nil { - t.Errorf("runSTACKITCLICommand() error = %v", err) - return - } + tt.args.confFn(t) defer func() { _, _ = runSTACKITCLICommand(ctx, "stackit config profile delete test-stackit-cli-flow-init -y") }() - if err := c.Init(tt.args.cfg); (err != nil) != tt.wantErr { - t.Errorf("TokenFlow.Init() error = %v, wantErr %v", err, tt.wantErr) + if err := c.Init(tt.args.cfg); err != nil { + if (err != nil) != tt.wantErr { + t.Errorf("TokenFlow.Init() error = %v, wantErr %v", err, tt.wantErr) + } + + return } + if c.config == nil { t.Error("config is nil") } From df57b2610ebc1b9c92e19ffbea5197522fa38d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Fri, 9 May 2025 07:28:58 +0200 Subject: [PATCH 04/10] fix lint --- core/clients/stackit_cli_flow_test.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/core/clients/stackit_cli_flow_test.go b/core/clients/stackit_cli_flow_test.go index a8b3ac012..8d3e5472f 100644 --- a/core/clients/stackit_cli_flow_test.go +++ b/core/clients/stackit_cli_flow_test.go @@ -10,6 +10,7 @@ import ( "testing" ) +//nolint:gosec // testServiceAccountToken is a test token const testServiceAccountToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImR1bW15QGV4YW1wbGUuY29tIiwiZXhwIjo5MDA3MTkyNTQ3NDA5OTF9.sM2yd5GL9kK4h8IKHbr_fA2XmrzEsLOeLTIPrU0VfMg" func TestSTACKITCLIFlow_Init(t *testing.T) { @@ -32,9 +33,9 @@ func TestSTACKITCLIFlow_Init(t *testing.T) { } }, }, false}, - {"no token", args{ + {"no-token", args{ cfg: &STACKITCLIFlowConfig{}, - confFn: func(t *testing.T) {}, + confFn: func(_ *testing.T) {}, }, true}, } for _, tt := range tests { @@ -43,8 +44,10 @@ func TestSTACKITCLIFlow_Init(t *testing.T) { c := &STACKITCLIFlow{} - _, _ = runSTACKITCLICommand(ctx, "stackit config profile delete test-stackit-cli-flow-init -y") - _, err := runSTACKITCLICommand(ctx, "stackit config profile create test-stackit-cli-flow-init") + cliProfileName := "test-stackit-cli-flow-init" + tt.name + + _, _ = runSTACKITCLICommand(ctx, fmt.Sprintf("stackit config profile delete %s -y", cliProfileName)) + _, err := runSTACKITCLICommand(ctx, "stackit config profile create "+cliProfileName) if err != nil { t.Errorf("runSTACKITCLICommand() error = %v", err) return @@ -53,7 +56,7 @@ func TestSTACKITCLIFlow_Init(t *testing.T) { tt.args.confFn(t) defer func() { - _, _ = runSTACKITCLICommand(ctx, "stackit config profile delete test-stackit-cli-flow-init -y") + _, _ = runSTACKITCLICommand(ctx, fmt.Sprintf("stackit config profile delete %s -y", cliProfileName)) }() if err := c.Init(tt.args.cfg); err != nil { From 762b5c889224ebdf305ae7469c629cdfe097c8fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Fri, 9 May 2025 07:37:57 +0200 Subject: [PATCH 05/10] Add test for CLI --- core/auth/auth.go | 6 ------ core/auth/auth_test.go | 31 +++++++++++++++++++++++++++ core/clients/stackit_cli_flow.go | 6 +++--- core/clients/stackit_cli_flow_test.go | 24 ++++++++++----------- 4 files changed, 46 insertions(+), 21 deletions(-) diff --git a/core/auth/auth.go b/core/auth/auth.go index 0ec0592d1..130d6d11d 100644 --- a/core/auth/auth.go +++ b/core/auth/auth.go @@ -63,12 +63,6 @@ func SetupAuth(cfg *config.Configuration) (rt http.RoundTripper, err error) { return nil, fmt.Errorf("configuring token authentication: %w", err) } return tokenRoundTripper, nil - } else if !cfg.DisableCLIAuthFlow { - cliRoundTripper, err := StackitCliAuth(cfg) - if err != nil { - return nil, fmt.Errorf("configuring CLI authentication: %w", err) - } - return cliRoundTripper, nil } authRoundTripper, err := DefaultAuth(cfg) diff --git a/core/auth/auth_test.go b/core/auth/auth_test.go index 413399bde..9b347aeb1 100644 --- a/core/auth/auth_test.go +++ b/core/auth/auth_test.go @@ -1,6 +1,7 @@ package auth import ( + "context" "crypto/rand" "crypto/rsa" "crypto/x509" @@ -17,6 +18,9 @@ import ( "github.com/stackitcloud/stackit-sdk-go/core/config" ) +//nolint:gosec // testServiceAccountToken is a test token +const testServiceAccountToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImR1bW15QGV4YW1wbGUuY29tIiwiZXhwIjo5MDA3MTkyNTQ3NDA5OTF9.sM2yd5GL9kK4h8IKHbr_fA2XmrzEsLOeLTIPrU0VfMg" + func setTemporaryHome(t *testing.T) { old := userHomeDir t.Cleanup(func() { @@ -150,6 +154,7 @@ func TestSetupAuth(t *testing.T) { setKeyPaths bool setCredentialsFilePathToken bool setCredentialsFilePathKey bool + setCLIToken bool isValid bool }{ { @@ -189,6 +194,13 @@ func TestSetupAuth(t *testing.T) { setCredentialsFilePathToken: true, isValid: true, }, + { + desc: "cli auth", + config: nil, + setCredentialsFilePathToken: false, + setCLIToken: true, + isValid: true, + }, { desc: "custom_config_token", config: &config.Configuration{ @@ -240,6 +252,25 @@ func TestSetupAuth(t *testing.T) { t.Setenv("STACKIT_CREDENTIALS_PATH", "") } + if test.setCLIToken { + _, _ = clients.RunSTACKITCLICommand(context.TODO(), "stackit config profile delete test-setup-auth -y") + _, err := clients.RunSTACKITCLICommand(context.TODO(), "stackit config profile create test-setup-auth") + if err != nil { + t.Errorf("RunSTACKITCLICommand() error = %v", err) + return + } + + _, err = clients.RunSTACKITCLICommand(context.TODO(), "stackit auth activate-service-account --service-account-token="+testServiceAccountToken) + if err != nil { + t.Errorf("RunSTACKITCLICommand() error = %v", err) + return + } + + defer func() { + _, _ = clients.RunSTACKITCLICommand(context.TODO(), "stackit config profile delete test-setup-auth -y") + }() + } + t.Setenv("STACKIT_SERVICE_ACCOUNT_EMAIL", "test-email") authRoundTripper, err := SetupAuth(test.config) diff --git a/core/clients/stackit_cli_flow.go b/core/clients/stackit_cli_flow.go index cdb729dcb..6006fae27 100644 --- a/core/clients/stackit_cli_flow.go +++ b/core/clients/stackit_cli_flow.go @@ -40,11 +40,11 @@ func (c *STACKITCLIFlow) Init(cfg *STACKITCLIFlowConfig) error { } func (c *STACKITCLIFlow) getTokenFromCLI() (string, error) { - return runSTACKITCLICommand(context.TODO(), "stackit auth get-access-token") + return RunSTACKITCLICommand(context.TODO(), "stackit auth get-access-token") } -// runSTACKITCLICommand executes the command line and returns the output. -func runSTACKITCLICommand(ctx context.Context, commandLine string) (string, error) { +// RunSTACKITCLICommand executes the command line and returns the output. +func RunSTACKITCLICommand(ctx context.Context, commandLine string) (string, error) { var cliCmd *exec.Cmd if runtime.GOOS == "windows" { dir := os.Getenv("SYSTEMROOT") diff --git a/core/clients/stackit_cli_flow_test.go b/core/clients/stackit_cli_flow_test.go index 8d3e5472f..e0c708e5f 100644 --- a/core/clients/stackit_cli_flow_test.go +++ b/core/clients/stackit_cli_flow_test.go @@ -26,9 +26,9 @@ func TestSTACKITCLIFlow_Init(t *testing.T) { {"ok", args{ cfg: &STACKITCLIFlowConfig{}, confFn: func(t *testing.T) { - _, err := runSTACKITCLICommand(context.TODO(), "stackit auth activate-service-account --service-account-token="+testServiceAccountToken) + _, err := RunSTACKITCLICommand(context.TODO(), "stackit auth activate-service-account --service-account-token="+testServiceAccountToken) if err != nil { - t.Errorf("runSTACKITCLICommand() error = %v", err) + t.Errorf("RunSTACKITCLICommand() error = %v", err) return } }, @@ -46,17 +46,17 @@ func TestSTACKITCLIFlow_Init(t *testing.T) { cliProfileName := "test-stackit-cli-flow-init" + tt.name - _, _ = runSTACKITCLICommand(ctx, fmt.Sprintf("stackit config profile delete %s -y", cliProfileName)) - _, err := runSTACKITCLICommand(ctx, "stackit config profile create "+cliProfileName) + _, _ = RunSTACKITCLICommand(ctx, fmt.Sprintf("stackit config profile delete %s -y", cliProfileName)) + _, err := RunSTACKITCLICommand(ctx, "stackit config profile create "+cliProfileName) if err != nil { - t.Errorf("runSTACKITCLICommand() error = %v", err) + t.Errorf("RunSTACKITCLICommand() error = %v", err) return } tt.args.confFn(t) defer func() { - _, _ = runSTACKITCLICommand(ctx, fmt.Sprintf("stackit config profile delete %s -y", cliProfileName)) + _, _ = RunSTACKITCLICommand(ctx, fmt.Sprintf("stackit config profile delete %s -y", cliProfileName)) }() if err := c.Init(tt.args.cfg); err != nil { @@ -93,21 +93,21 @@ func TestSTACKITCLIFlow_Do(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := context.TODO() - _, _ = runSTACKITCLICommand(ctx, "stackit config profile delete test-stackit-cli-flow-do -y") - _, err := runSTACKITCLICommand(ctx, "stackit config profile create test-stackit-cli-flow-do") + _, _ = RunSTACKITCLICommand(ctx, "stackit config profile delete test-stackit-cli-flow-do -y") + _, err := RunSTACKITCLICommand(ctx, "stackit config profile create test-stackit-cli-flow-do") if err != nil { - t.Errorf("runSTACKITCLICommand() error = %v", err) + t.Errorf("RunSTACKITCLICommand() error = %v", err) return } - _, err = runSTACKITCLICommand(ctx, "stackit auth activate-service-account --service-account-token="+testServiceAccountToken) + _, err = RunSTACKITCLICommand(ctx, "stackit auth activate-service-account --service-account-token="+testServiceAccountToken) if err != nil { - t.Errorf("runSTACKITCLICommand() error = %v", err) + t.Errorf("RunSTACKITCLICommand() error = %v", err) return } defer func() { - _, _ = runSTACKITCLICommand(ctx, "stackit config profile delete test-stackit-cli-flow-do -y") + _, _ = RunSTACKITCLICommand(ctx, "stackit config profile delete test-stackit-cli-flow-do -y") }() c := &STACKITCLIFlow{} From 2a71390e65c2db4db92c9c73d0583c3f7805c808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Fri, 9 May 2025 10:27:36 +0200 Subject: [PATCH 06/10] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22f510281..07c8f3bc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## Release (2025-XX-YY) - `core`: [v0.17.2](core/CHANGELOG.md#v0172-2025-XX-YY) - - **New:** If a custom http.Client is provided, the http.Transport is respected. This allows customizing the http.Client with custom timeouts or instrumentation. + - **New:** Add STACKIT CLI auth flow. ## Release (2025-05-XX) - `resourcemanager`: [v0.13.2](services/resourcemanager/CHANGELOG.md#v0132-2025-05-02) From 765f60b229550d7e27de9adf19b4596759827dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Fri, 9 May 2025 20:50:05 +0200 Subject: [PATCH 07/10] Replace jkroepke/setup-stackit-cli MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Otto Kröpke --- .github/workflows/ci.yaml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 10c49a010..4bd13cdcb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,7 +18,17 @@ jobs: with: go-version: ${{ matrix.go-version }} - name: Install STACKIT CLI - uses: jkroepke/setup-stackit-cli@v1 + run: |- + OS=$(uname | tr '[:upper:]' '[:lower:]') + ARCH=$(uname -m) + + [ "$ARCH" = "x86_64" ] && ARCH=amd64 + [ "$ARCH" = "aarch64" ] && ARCH=amd64 + + echo Downloading stackit-cli from https://github.com/stackitcloud/stackit-cli/releases/download/v${STACKIT_CLI_VERSION}/stackit-cli_${STACKIT_CLI_VERSION}_${OS}_${ARCH}.tar.gz + curl -sSfLo - https://github.com/stackitcloud/stackit-cli/releases/download/v${STACKIT_CLI_VERSION}/stackit-cli_${STACKIT_CLI_VERSION}_${OS}_${ARCH}.tar.gz | sudo tar zxf - -C /usr/local/bin/ stackit + env: + STACKIT_CLI_VERSION: 0.31.0 - name: Test run: make test From ff92b6183adf7241c8894de0bf7307bd41e804e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Wed, 21 May 2025 17:11:26 +0200 Subject: [PATCH 08/10] Update core/CHANGELOG.md Co-authored-by: Marcel Jacek <72880145+marceljk@users.noreply.github.com> --- core/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 64c3a254d..da18c2177 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -1,4 +1,4 @@ -## v0.17.2 (2025-XX-YY) +## v0.17.2 (2025-05-21) - **New:** Add STACKIT CLI auth flow. ## v0.17.1 (2025-04-09) From a1154daa168af7cef285a3db271c6083579741b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Wed, 21 May 2025 17:11:32 +0200 Subject: [PATCH 09/10] Update CHANGELOG.md Co-authored-by: Marcel Jacek <72880145+marceljk@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69bdac8d2..db4899dcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## Release (2025-XX-YY) -- `core`: [v0.17.2](core/CHANGELOG.md#v0172-2025-XX-YY) +- `core`: [v0.17.2](core/CHANGELOG.md#v0172-2025-05-21) - **New:** Add STACKIT CLI auth flow. ## Release (2025-05-09) From 60aea17a01c7e2a49052c6dc11883378d0ac6ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Thu, 22 May 2025 11:43:09 +0200 Subject: [PATCH 10/10] Replace DisableCLIAuthFlow with CLIAuthFlow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Otto Kröpke --- core/auth/auth.go | 2 +- core/auth/auth_test.go | 6 ++++-- core/config/config.go | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/core/auth/auth.go b/core/auth/auth.go index 130d6d11d..3a7c4f871 100644 --- a/core/auth/auth.go +++ b/core/auth/auth.go @@ -93,7 +93,7 @@ func DefaultAuth(cfg *config.Configuration) (rt http.RoundTripper, err error) { rt, err = TokenAuth(cfg) if err != nil { tokenFlowErr := err - if cfg.DisableCLIAuthFlow { + if !cfg.CLIAuthFlow { err = errors.New("CLI flow disabled") } else { // Stackit CLI flow diff --git a/core/auth/auth_test.go b/core/auth/auth_test.go index 9b347aeb1..8dc97d631 100644 --- a/core/auth/auth_test.go +++ b/core/auth/auth_test.go @@ -195,8 +195,10 @@ func TestSetupAuth(t *testing.T) { isValid: true, }, { - desc: "cli auth", - config: nil, + desc: "cli auth", + config: &config.Configuration{ + CLIAuthFlow: true, + }, setCredentialsFilePathToken: false, setCLIToken: true, isValid: true, diff --git a/core/config/config.go b/core/config/config.go index 0559d53b1..3668453cb 100644 --- a/core/config/config.go +++ b/core/config/config.go @@ -90,7 +90,7 @@ type Configuration struct { CredentialsFilePath string `json:"credentialsFilePath,omitempty"` TokenCustomUrl string `json:"tokenCustomUrl,omitempty"` Region string `json:"region,omitempty"` - DisableCLIAuthFlow bool `json:"disableCLIAuthFlow,omitempty"` // Should be set to true, if called by STACKIT CLI to avoid infinite loops. + CLIAuthFlow bool `json:"cliAuthFlow,omitempty"` CustomAuth http.RoundTripper Servers ServerConfigurations OperationServers map[string]ServerConfigurations