Skip to content

core: Add STACKIT CLI Auth flow #2179

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
12 changes: 12 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ jobs:
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Install STACKIT CLI
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

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## Release (2025-05-21)
- `core`: [v0.17.2](core/CHANGELOG.md#v0172-2025-05-21)
- **New:** Add STACKIT CLI auth flow.

## Release (2025-05-15)
- `alb`:
- [v0.4.0](services/alb/CHANGELOG.md#v040-2025-05-15)
Expand Down
3 changes: 3 additions & 0 deletions core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## v0.17.2 (2025-05-21)
- **New:** Add STACKIT CLI auth flow.

## v0.17.1 (2025-04-09)
- **Improvement:** Improve error message for key flow authentication

Expand Down
29 changes: 28 additions & 1 deletion core/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package auth

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
Expand Down Expand Up @@ -63,6 +64,7 @@ func SetupAuth(cfg *config.Configuration) (rt http.RoundTripper, err error) {
}
return tokenRoundTripper, nil
}

authRoundTripper, err := DefaultAuth(cfg)
if err != nil {
return nil, fmt.Errorf("configuring default authentication: %w", err)
Expand Down Expand Up @@ -90,7 +92,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.CLIAuthFlow {
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
Expand Down Expand Up @@ -216,6 +228,21 @@ func KeyAuth(cfg *config.Configuration) (http.RoundTripper, error) {
return client, nil
}

// StackitCliAuth configures the [clients.STACKITCLIFlow] and returns an http.RoundTripper
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(&cliCfg); 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 == "" {
Expand Down
33 changes: 33 additions & 0 deletions core/auth/auth_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package auth

import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
Expand All @@ -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() {
Expand Down Expand Up @@ -150,6 +154,7 @@ func TestSetupAuth(t *testing.T) {
setKeyPaths bool
setCredentialsFilePathToken bool
setCredentialsFilePathKey bool
setCLIToken bool
isValid bool
}{
{
Expand Down Expand Up @@ -189,6 +194,15 @@ func TestSetupAuth(t *testing.T) {
setCredentialsFilePathToken: true,
isValid: true,
},
{
desc: "cli auth",
config: &config.Configuration{
CLIAuthFlow: true,
},
setCredentialsFilePathToken: false,
setCLIToken: true,
isValid: true,
},
{
desc: "custom_config_token",
config: &config.Configuration{
Expand Down Expand Up @@ -240,6 +254,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)
Expand Down
78 changes: 78 additions & 0 deletions core/clients/stackit_cli_flow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package clients

import (
"bytes"
"context"
"errors"
"net/http"
"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 {
HTTPTransport http.RoundTripper
}

// GetConfig returns the flow configuration
func (c *STACKITCLIFlow) GetConfig() STACKITCLIFlowConfig {
return STACKITCLIFlowConfig{}
}

func (c *STACKITCLIFlow) Init(cfg *STACKITCLIFlowConfig) error {
token, err := c.getTokenFromCLI()
if err != nil {
return err
}

return c.TokenFlow.Init(&TokenFlowConfig{
ServiceAccountToken: strings.TrimSpace(token),
HTTPTransport: cfg.HTTPTransport,
})
}

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
}
Loading
Loading