Skip to content

Commit

Permalink
Merge pull request #26 from stackitcloud/hs/ske-credentials
Browse files Browse the repository at this point in the history
Implement `ske credential rotate/describe`
  • Loading branch information
hcsa73 authored Nov 27, 2023
2 parents 5fa7923 + 7c1c787 commit b1b6438
Show file tree
Hide file tree
Showing 6 changed files with 596 additions and 0 deletions.
26 changes: 26 additions & 0 deletions internal/cmd/ske/credential/credential.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package credential

import (
"fmt"

"github.com/stackitcloud/stackit-cli/internal/cmd/ske/credential/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/ske/credential/rotate"

"github.com/spf13/cobra"
)

func NewCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "credential",
Short: "Provides functionality for SKE credentials",
Long: "Provides functionality for SKE credentials",
Example: fmt.Sprintf("%s\n%s", describe.NewCmd().Example, rotate.NewCmd().Example),
}
addSubcommands(cmd)
return cmd
}

func addSubcommands(cmd *cobra.Command) {
cmd.AddCommand(describe.NewCmd())
cmd.AddCommand(rotate.NewCmd())
}
109 changes: 109 additions & 0 deletions internal/cmd/ske/credential/describe/describe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package describe

import (
"context"
"encoding/json"
"fmt"

"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"

"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/ske"
)

const (
clusterNameFlag = "cluster-name"
)

type flagModel struct {
*globalflags.GlobalFlagModel
ClusterName string
}

func NewCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "describe",
Short: "Get details of the credential associated to a SKE cluster",
Long: "Get details of the credential associated to a SKE cluster",
Example: `$ stackit ske credential describe --project-id xxx --cluster-name xxx`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseFlags(cmd)
if err != nil {
return err
}

// Configure API client
apiClient, err := client.ConfigureClient(cmd)
if err != nil {
return fmt.Errorf("authentication failed, please run \"stackit auth login\" or \"stackit auth activate-service-account\"")
}

// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("describe SKE credential: %w", err)
}

return outputResult(cmd, model.OutputFormat, resp)
},
}
configureFlags(cmd)
return cmd
}

func configureFlags(cmd *cobra.Command) {
cmd.Flags().String(clusterNameFlag, "", "Cluster name")

err := utils.MarkFlagsRequired(cmd, clusterNameFlag)
cobra.CheckErr(err)
}

func parseFlags(cmd *cobra.Command) (*flagModel, error) {
globalFlags := globalflags.Parse(cmd)
if globalFlags.ProjectId == "" {
return nil, fmt.Errorf("project ID not set")
}

clusterName := utils.FlagToStringValue(cmd, clusterNameFlag)
if clusterName == "" {
return nil, fmt.Errorf("cluster name can't be empty")
}

return &flagModel{
GlobalFlagModel: globalFlags,
ClusterName: clusterName,
}, nil
}

func buildRequest(ctx context.Context, model *flagModel, apiClient *ske.APIClient) ske.ApiGetCredentialsRequest {
req := apiClient.GetCredentials(ctx, model.ProjectId, model.ClusterName)
return req
}

func outputResult(cmd *cobra.Command, outputFormat string, credential *ske.CredentialsResponse) error {
switch outputFormat {
case globalflags.TableOutputFormat:
table := tables.NewTable()
table.SetHeader("SERVER", "TOKEN")
table.AddRow(*credential.Server, *credential.Token)
err := table.Render(cmd)
if err != nil {
return fmt.Errorf("render table: %w", err)
}

return nil
default:
details, err := json.MarshalIndent(credential, "", " ")
if err != nil {
return fmt.Errorf("marshal PostgreSQL credential: %w", err)
}
cmd.Println(string(details))

return nil
}
}
183 changes: 183 additions & 0 deletions internal/cmd/ske/credential/describe/describe_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package describe

import (
"context"
"testing"

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

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/ske"
)

var projectIdFlag = globalflags.ProjectIdFlag

type testCtxKey struct{}

var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &ske.APIClient{}
var testProjectId = uuid.NewString()

func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
clusterNameFlag: "cluster",
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}

func fixtureFlagModel(mods ...func(model *flagModel)) *flagModel {
model := &flagModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
},
ClusterName: "cluster",
}
for _, mod := range mods {
mod(model)
}
return model
}

func fixtureRequest(mods ...func(request *ske.ApiGetCredentialsRequest)) ske.ApiGetCredentialsRequest {
request := testClient.GetCredentials(testCtx, testProjectId, "cluster")
for _, mod := range mods {
mod(&request)
}
return request
}

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: false,
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "cluster name missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, clusterNameFlag)
}),
isValid: false,
},
{
description: "cluster name invalid",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[clusterNameFlag] = ""
}),
isValid: false,
},
}

for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
cmd := &cobra.Command{}
configureFlags(cmd)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}

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

err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}

model, err := parseFlags(cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}

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

func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *flagModel
expectedRequest ske.ApiGetCredentialsRequest
}{
{
description: "base",
model: fixtureFlagModel(),
expectedRequest: fixtureRequest(),
},
}

for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)

diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
Loading

0 comments on commit b1b6438

Please sign in to comment.