Skip to content

Commit

Permalink
feat(spacectl/stack): add stack list command (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
jpJuni0r authored Aug 23, 2022
1 parent 630e6d4 commit 418d6c8
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 0 deletions.
170 changes: 170 additions & 0 deletions internal/cmd/stack/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ package stack
import (
"encoding/base64"
"fmt"
"github.com/pkg/errors"
"io/ioutil"
"os"
"regexp"
"strings"

"github.com/shurcooL/graphql"
"github.com/urfave/cli/v2"

"github.com/spacelift-io/spacectl/internal/cmd"
"github.com/spacelift-io/spacectl/internal/cmd/authenticated"
)

Expand All @@ -30,6 +33,46 @@ type ConfigInput struct {
WriteOnly graphql.Boolean `json:"writeOnly"`
}

type configElement struct {
ID string `graphql:"id" json:"id,omitempty"`
Checksum string `graphql:"checksum" json:"checksum,omitempty"`
CreatedAt int64 `graphql:"createdAt" json:"createdAt,omitempty"`
Runtime bool `graphql:"runtime" json:"runtime,omitempty"`
Type ConfigType `graphql:"type" json:"type,omitempty"`
Value *string `graphql:"value" json:"value,omitempty"`
WriteOnly bool `graphql:"writeOnly" json:"writeOnly,omitempty"`
FileMode *string `graphql:"fileMode" json:"fileMode,omitempty"`
}

type listEnvElementOutput struct {
Name string `json:"name"`
Type string `json:"type"`
Value *string `json:"value"`
FileMode *string `json:"fileMode"`
// Context specifies the name of the context.
Context *string `json:"context"`
// Runtime is not printed, it's just used to determine output formatting.
Runtime bool `json:"runtime"`
// WriteOnly is not printed, it's just used to determine output formatting.
WriteOnly bool `json:"writeOnly"`
}

type runtimeConfig struct {
Context *struct {
ID string `graphql:"id" json:"id,omitempty"`
ContextName string `graphql:"contextName" json:"contextName,omitempty"`
} `graphql:"context" json:"context"`
Element configElement `graphql:"element" json:"element,omitempty"`
}

type listEnvQuery struct {
Stack struct {
RuntimeConfig []runtimeConfig `graphql:"runtimeConfig" json:"runtimeConfig"`
} `graphql:"stack(id: $stack)" json:"stack"`
}

type listEnvCommand struct{}

func setVar(cliCtx *cli.Context) error {
if nArgs := cliCtx.NArg(); nArgs != 2 {
return fmt.Errorf("expected two arguments to `environment setenv` but got %d", nArgs)
Expand Down Expand Up @@ -73,6 +116,133 @@ func setVar(cliCtx *cli.Context) error {
return nil
}

func (e *listEnvCommand) listEnv(cliCtx *cli.Context) error {
outputFormat, err := cmd.GetOutputFormat(cliCtx)
if err != nil {
return err
}

stackID := cliCtx.String(flagStackID.Name)

var query listEnvQuery
variables := map[string]interface{}{
"stack": graphql.ID(stackID),
}

if err := authenticated.Client.Query(cliCtx.Context, &query, variables); err != nil {
return err
}

var elements []listEnvElementOutput
for _, config := range query.Stack.RuntimeConfig {
var contextName *string
if config.Context != nil {
contextName = &config.Context.ContextName
}
if element, err := config.Element.toConfigElementOutput(contextName); err == nil {
elements = append(elements, element)
} else {
return err
}
}

switch outputFormat {
case cmd.OutputFormatTable:
return e.showOutputsTable(elements)
case cmd.OutputFormatJSON:
return e.showOutputsJSON(elements)
default:
return fmt.Errorf("unknown output format: %v", outputFormat)
}
}

func (e *listEnvCommand) showOutputsTable(outputs []listEnvElementOutput) error {
tableData := [][]string{{"Name", "Type", "Value", "File Mode", "Context"}}
for _, output := range outputs {
var row []string

row = append(row, output.Name)
row = append(row, output.Type)

var value string
switch {
case output.Runtime:
value = "<computed>"
case output.WriteOnly:
value = "*****"
case output.Type == string(fileTypeConfig):
value = output.trimmedValue()
case output.Value != nil:
value = *output.Value
default:
// keep value as empty string
}
row = append(row, value)

if output.FileMode != nil {
row = append(row, *output.FileMode)
} else {
row = append(row, "")
}
if output.Context != nil {
row = append(row, *output.Context)
} else {
row = append(row, "")
}

tableData = append(tableData, row)
}
return cmd.OutputTable(tableData, true)
}

func (e *listEnvCommand) showOutputsJSON(outputs []listEnvElementOutput) error {
return cmd.OutputJSON(outputs)
}

func (e *configElement) toConfigElementOutput(contextName *string) (listEnvElementOutput, error) {
var value = e.Value

if e.Type == fileTypeConfig && e.Value != nil {
result, err := base64.StdEncoding.DecodeString(*e.Value)

if err != nil {
message := fmt.Sprintf("failed to decode base64-encoded file with id %s", e.ID)
return listEnvElementOutput{}, errors.Wrapf(err, message)
}

stringValue := string(result)
value = &stringValue
}

return listEnvElementOutput{
Name: e.ID,
Type: string(e.Type),
Value: value,
FileMode: e.FileMode,
Context: contextName,
Runtime: e.Runtime,
WriteOnly: e.WriteOnly,
}, nil
}

func (o *listEnvElementOutput) trimmedValue() string {
if o.Value == nil {
return ""
}

lineBreaks := regexp.MustCompile("(\r?\n)|\r")
valueNoNewlines := string(lineBreaks.ReplaceAll([]byte(*o.Value), []byte(" ")))

maxOutputLength := 80
ellipsis := "..."

if len(valueNoNewlines) > maxOutputLength {
return fmt.Sprintf("%s%s", valueNoNewlines[:(maxOutputLength-len(ellipsis))], ellipsis)
}

return valueNoNewlines
}

func mountFile(cliCtx *cli.Context) error {
nArgs := cliCtx.NArg()

Expand Down
34 changes: 34 additions & 0 deletions internal/cmd/stack/environment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package stack

import (
"strings"
"testing"
)

func TestTrimmedValue(t *testing.T) {
limit := 80
type testRow struct {
input string
expected string
}

testTable := []testRow{
{input: "foo", expected: "foo"},
{input: "abc\rdef\nghi\r\njkl", expected: "abc def ghi jkl"},
{input: "a\r\r\nb\n\r\rc", expected: "a b c"},
{input: strings.Repeat("a", limit), expected: strings.Repeat("a", limit)},
{input: strings.Repeat("a", limit+1), expected: strings.Repeat("a", limit-3) + "..."},
}

for _, testCase := range testTable {
o := listEnvElementOutput{
Value: &testCase.input,
}

result := o.trimmedValue()

if result != testCase.expected {
t.Errorf("trimmed value of %s should be %s but was %s", testCase.input, testCase.expected, result)
}
}
}
10 changes: 10 additions & 0 deletions internal/cmd/stack/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@ func Command() *cli.Command {
Before: authenticated.Ensure,
ArgsUsage: "NAME VALUE",
},
{
Name: "list",
Usage: "Lists all the environment variables and mounted files for a stack.",
Flags: []cli.Flag{
flagStackID,
cmd.FlagOutputFormat,
},
Action: (&listEnvCommand{}).listEnv,
Before: authenticated.Ensure,
},
{
Name: "mount",
Usage: "Mount a file from existing file or STDIN.",
Expand Down

0 comments on commit 418d6c8

Please sign in to comment.