Skip to content

Commit

Permalink
Migrate CLI implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
joaopalet committed Nov 17, 2023
1 parent a8b2819 commit 99f924e
Show file tree
Hide file tree
Showing 82 changed files with 12,154 additions and 7 deletions.
47 changes: 45 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,55 @@ module github.com/stackitcloud/stackit-cli

go 1.21.3

require github.com/spf13/cobra v1.8.0
require (
github.com/golang-jwt/jwt/v5 v5.1.0
github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.4.0
github.com/jedib0t/go-pretty/v6 v6.4.9
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.17.0
github.com/stackitcloud/stackit-sdk-go/core v0.7.3
github.com/stackitcloud/stackit-sdk-go/services/dns v0.6.0
github.com/stackitcloud/stackit-sdk-go/services/postgresql v0.7.0
github.com/zalando/go-keyring v0.2.3
golang.org/x/oauth2 v0.14.0
)

require (
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
github.com/alessio/shellescape v1.4.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/danieljoos/wincred v1.2.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/sagikazarmark/locafero v0.3.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.10.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apimachinery v0.28.4 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
)
546 changes: 546 additions & 0 deletions go.sum

Large diffs are not rendered by default.

102 changes: 102 additions & 0 deletions internal/cmd/auth/activate-service-account/activate_service_account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package activateserviceaccount

import (
"fmt"

"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"

"github.com/spf13/cobra"
sdkAuth "github.com/stackitcloud/stackit-sdk-go/core/auth"
sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config"
)

const (
serviceAccountTokenFlag = "service-account-token"
serviceAccountKeyPathFlag = "service-account-key-path"
privateKeyPathFlag = "private-key-path"
tokenCustomEndpointFlag = "token-custom-endpoint"
jwksCustomEndpointFlag = "jwks-custom-endpoint"
)

type flagModel struct {
ServiceAccountToken string
ServiceAccountKeyPath string
PrivateKeyPath string
TokenCustomEndpoint string
JwksCustomEndpoint string
}

var Cmd = &cobra.Command{
Use: "activate-service-account",
Short: "Activate service account authentication",
Long: "Activate authentication using service account credentials.\nFor more details on how to configure your service account, check the Authentication section on our documentation (LINK HERE README)",
Example: `$ stackit auth activate-service-account --service-account-key-path path/to/service_account_key.json --private-key-path path/to/private_key.pem`,
RunE: func(cmd *cobra.Command, args []string) error {
model := parseFlags(cmd)

err := storeFlags(model)
if err != nil {
return err
}

cfg := &sdkConfig.Configuration{
Token: model.ServiceAccountToken,
ServiceAccountKeyPath: model.ServiceAccountKeyPath,
PrivateKeyPath: model.PrivateKeyPath,
TokenCustomUrl: model.TokenCustomEndpoint,
JWKSCustomUrl: model.JwksCustomEndpoint,
}

// Setup authentication based on the provided credentials and the environment
// Initializes the authentication flow
rt, err := sdkAuth.SetupAuth(cfg)
if err != nil {
return fmt.Errorf("set up authentication: %w", err)
}

// Authenticates the service account and stores credentials
email, err := auth.AuthenticateServiceAccount(rt)
if err != nil {
return fmt.Errorf("authenticate service account: %w", err)
}

fmt.Printf("You have been successfully authenticated to the STACKIT CLI!\nService account email: %s\n", email)

return nil
},
}

func init() {
configureFlags(Cmd)
}

func configureFlags(cmd *cobra.Command) {
cmd.Flags().String(serviceAccountTokenFlag, "", "Service account long-lived access token")
cmd.Flags().String(serviceAccountKeyPathFlag, "", "Service account key path")
cmd.Flags().String(privateKeyPathFlag, "", "RSA private key path")
cmd.Flags().String(tokenCustomEndpointFlag, "", "Custom endpoint for the token API, which is used to request access tokens when the service-account authentication is activated")
cmd.Flags().String(jwksCustomEndpointFlag, "", "Custom endpoint for the jwks API, which is used to get the json web key sets (jwks) to validate tokens when the service-account authentication is activated")
}

func parseFlags(cmd *cobra.Command) *flagModel {
return &flagModel{
ServiceAccountToken: utils.FlagToStringValue(cmd, serviceAccountTokenFlag),
ServiceAccountKeyPath: utils.FlagToStringValue(cmd, serviceAccountKeyPathFlag),
PrivateKeyPath: utils.FlagToStringValue(cmd, privateKeyPathFlag),
TokenCustomEndpoint: utils.FlagToStringValue(cmd, tokenCustomEndpointFlag),
JwksCustomEndpoint: utils.FlagToStringValue(cmd, jwksCustomEndpointFlag),
}
}

func storeFlags(model *flagModel) error {
err := auth.SetAuthField(auth.TOKEN_CUSTOM_ENDPOINT, model.TokenCustomEndpoint)
if err != nil {
return fmt.Errorf("set %s: %w", auth.TOKEN_CUSTOM_ENDPOINT, err)
}
err = auth.SetAuthField(auth.JWKS_CUSTOM_ENDPOINT, model.JwksCustomEndpoint)
if err != nil {
return fmt.Errorf("set %s: %w", auth.JWKS_CUSTOM_ENDPOINT, err)
}
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package activateserviceaccount

import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/spf13/cobra"
)

func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
serviceAccountTokenFlag: "token",
serviceAccountKeyPathFlag: "sa_key",
privateKeyPathFlag: "private_key",
tokenCustomEndpointFlag: "token_url",
jwksCustomEndpointFlag: "jwks_url",
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}

func fixtureFlagModel(mods ...func(model *flagModel)) *flagModel {
model := &flagModel{
ServiceAccountToken: "token",
ServiceAccountKeyPath: "sa_key",
PrivateKeyPath: "private_key",
TokenCustomEndpoint: "token_url",
JwksCustomEndpoint: "jwks_url",
}
for _, mod := range mods {
mod(model)
}
return model
}

func TestParseFlags(t *testing.T) {
tests := []struct {
description string
flagValues map[string]string
isValid bool
expectedModel *flagModel
}{
{
description: "base",
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureFlagModel(),
},
{
description: "no values",
flagValues: map[string]string{},
isValid: true,
expectedModel: &flagModel{
ServiceAccountToken: "",
ServiceAccountKeyPath: "",
PrivateKeyPath: "",
TokenCustomEndpoint: "",
JwksCustomEndpoint: "",
},
},
{
description: "zero values",
flagValues: map[string]string{
serviceAccountTokenFlag: "",
serviceAccountKeyPathFlag: "",
privateKeyPathFlag: "",
tokenCustomEndpointFlag: "",
jwksCustomEndpointFlag: "",
},
isValid: true,
expectedModel: &flagModel{
ServiceAccountToken: "",
ServiceAccountKeyPath: "",
PrivateKeyPath: "",
TokenCustomEndpoint: "",
JwksCustomEndpoint: "",
},
},
{
description: "invalid_flag",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues["test_flag"] = "test"
}),
isValid: false,
},
}

for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
cmd := &cobra.Command{}

configureFlags(cmd)

for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}

model := parseFlags(cmd)

if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
21 changes: 21 additions & 0 deletions internal/cmd/auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package auth

import (
activateserviceaccount "github.com/stackitcloud/stackit-cli/internal/cmd/auth/activate-service-account"
"github.com/stackitcloud/stackit-cli/internal/cmd/auth/login"

"github.com/spf13/cobra"
)

var Cmd = &cobra.Command{
Use: "auth",
Short: "Provides authentication functionality",
Long: "Provides authentication functionality",
Example: `$ stackit auth login`,
}

func init() {
// Add all direct child commands
Cmd.AddCommand(login.Cmd)
Cmd.AddCommand(activateserviceaccount.Cmd)
}
29 changes: 29 additions & 0 deletions internal/cmd/auth/login/login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package login

import (
"fmt"
"os"

"github.com/stackitcloud/stackit-cli/internal/pkg/auth"

"github.com/spf13/cobra"
)

var Cmd = &cobra.Command{
Use: "login",
Short: "Login to the provider",
Long: "Login to the provider",
Example: `$ stackit auth login`,
Run: func(cmd *cobra.Command, args []string) {
err := auth.AuthorizeUser()
if err != nil {
fmt.Printf("Authorization failed: %s\n", err)
os.Exit(1)
}

fmt.Println("Successfully logged into STACKIT CLI.")
},
}

func init() {
}
24 changes: 24 additions & 0 deletions internal/cmd/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package config

import (
"fmt"

"github.com/stackitcloud/stackit-cli/internal/cmd/config/inspect"
"github.com/stackitcloud/stackit-cli/internal/cmd/config/set"
"github.com/stackitcloud/stackit-cli/internal/cmd/config/unset"

"github.com/spf13/cobra"
)

var Cmd = &cobra.Command{
Use: "config",
Short: "CLI configuration options",
Long: "CLI configuration options",
Example: fmt.Sprintf("%s\n%s\n%s", set.Cmd.Example, inspect.Cmd.Example, unset.Cmd.Example),
}

func init() {
Cmd.AddCommand(inspect.Cmd)
Cmd.AddCommand(set.Cmd)
Cmd.AddCommand(unset.Cmd)
}
32 changes: 32 additions & 0 deletions internal/cmd/config/inspect/inspect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package inspect

import (
"encoding/json"
"fmt"

"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var Cmd = &cobra.Command{
Use: "inspect",
Short: "Inspect the current CLI configuration values",
Long: "Inspect the current CLI configuration values",
Example: `$ stackit config inspect`,
RunE: func(cmd *cobra.Command, args []string) error {
err := viper.ReadInConfig()
if err != nil {
return fmt.Errorf("read config file: %w", err)
}

configData := viper.AllSettings()

configJSON, err := json.MarshalIndent(configData, "", " ")
if err != nil {
return fmt.Errorf("marshal config: %w", err)
}
fmt.Println(string(configJSON))

return nil
},
}
Loading

0 comments on commit 99f924e

Please sign in to comment.