Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,8 @@ stackctl stack rollback my-app --target-log <log-id>

`stack get` shows `Following` and `Followers` (k8s-stack-manager v0.8.0 or later). `stack preview -q` prints the names of the charts whose values differ from the last deploy. `stack preview` writes the values-drift warning to stderr, so the YAML on stdout stays valid.

`stack action list` and `stack action run` see the actions that apply to the stack: a server can limit an action to some templates, template categories, definitions or clusters (`applies_to`). An action of another stack gives "unknown action" with the actions of this stack. When the server could not check where an action applies, `stack action list` ends with "Some actions are not shown: the server could not check where they apply. Try again.", and `-o json` has `"scope_incomplete": true`.

`stack action run`:

- Checks the declared parameters before the request: a `bool` takes `true` or `false`, an `enum` takes one of its options, and a required parameter needs a value.
Expand Down Expand Up @@ -654,6 +656,7 @@ These groups have no section above. Run `stackctl <group> --help` for the flags.
| `--follow` and `stack watch` with an API key, `bulk delete` with clean first, `orphaned delete --confirm`, `user set-role` | v0.8.0 or later |
| `definition link-template` | v0.11.0 or later |
| `--stop-at`, `stack extend --until`, the TTL limit of `server info` and of the local check (an older server ignores `--stop-at` and `--until`: stackctl shows the result and exits with code 1; it uses 30 days as the limit) | v0.12.0 or later |
| Actions limited to some stacks (`applies_to`) and the `scope_incomplete` hint of `stack action list` (an older server lists every action for every stack) | the release after v0.14.0 (omattsson/k8s-stack-manager#720) |

## Output Formats

Expand Down
29 changes: 24 additions & 5 deletions cli/cmd/stack_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@ Examples:
return printer.PrintYAML(list)
}
if len(list.Actions) == 0 {
printer.PrintMessage("The server has no custom actions.")
printer.PrintMessage("This stack has no custom actions.")
if list.ScopeIncomplete {
printer.PrintMessage(msgActionsIncomplete)
}
return nil
}
rows := make([][]string, len(list.Actions))
Expand All @@ -109,10 +112,20 @@ Examples:
client.SanitizeServerText(parameterSummary(a.Parameters)),
}
}
return printer.PrintTable([]string{"NAME", "LABEL", "CAN RUN", "JOB LOG", "PARAMETERS"}, rows)
if err := printer.PrintTable([]string{"NAME", "LABEL", "CAN RUN", "JOB LOG", "PARAMETERS"}, rows); err != nil {
return err
}
if list.ScopeIncomplete {
printer.PrintMessage(msgActionsIncomplete)
}
return nil
},
}

// msgActionsIncomplete tells that the server left out actions it could not
// check for the stack (scope_incomplete).
const msgActionsIncomplete = "Some actions are not shown: the server could not check where they apply. Try again."

// parameterSummary returns the parameters of an action for the table, for
// example "env:enum(se|dk)*=se, dry_run:bool".
func parameterSummary(params []types.ActionParameter) string {
Expand Down Expand Up @@ -265,7 +278,9 @@ func parseParamFlags(raw []string) (map[string]string, error) {
return out, nil
}

// findAction returns the action with the given name.
// findAction returns the action with the given name. The list holds the
// actions that apply to the stack: a server can limit an action to some
// templates or clusters (applies_to).
func findAction(list *types.ActionList, name string) (types.ActionInfo, error) {
names := make([]string, 0, len(list.Actions))
for _, a := range list.Actions {
Expand All @@ -274,10 +289,14 @@ func findAction(list *types.ActionList, name string) (types.ActionInfo, error) {
}
names = append(names, a.Name)
}
hint := ""
if list.ScopeIncomplete {
hint = "; some actions could not be checked for this stack, try again"
}
if len(names) == 0 {
return types.ActionInfo{}, fmt.Errorf("unknown action %q: the server has no custom actions", name)
return types.ActionInfo{}, fmt.Errorf("unknown action %q: this stack has no custom actions%s", name, hint)
}
return types.ActionInfo{}, fmt.Errorf("unknown action %q (actions: %s)", name, strings.Join(names, ", "))
return types.ActionInfo{}, fmt.Errorf("unknown action %q (actions of this stack: %s)%s", name, strings.Join(names, ", "), hint)
}

// buildActionParams checks the given parameters against the declared ones
Expand Down
42 changes: 40 additions & 2 deletions cli/cmd/stack_action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,20 @@ func TestStackActionListCmd(t *testing.T) {
empty := &actionServer{list: `{"instance_id":"42","actions":[]}`}
buf := setupStackTestCmd(t, empty.start(t).URL)
require.NoError(t, stackActionListCmd.RunE(stackActionListCmd, []string{"42"}))
assert.Equal(t, "The server has no custom actions.\n", buf.String())
assert.Equal(t, "This stack has no custom actions.\n", buf.String())
})
t.Run("scope incomplete", func(t *testing.T) {
incomplete := &actionServer{list: strings.Replace(actionListBody, `"instance_id":"42",`, `"instance_id":"42","scope_incomplete":true,`, 1)}
buf := setupStackTestCmd(t, incomplete.start(t).URL)
require.NoError(t, stackActionListCmd.RunE(stackActionListCmd, []string{"42"}))
assert.Regexp(t, `ping\s+Ping`, buf.String())
assert.True(t, strings.HasSuffix(buf.String(), msgActionsIncomplete+"\n"), buf.String())
})
t.Run("no actions, scope incomplete", func(t *testing.T) {
empty := &actionServer{list: `{"instance_id":"42","actions":[],"scope_incomplete":true}`}
buf := setupStackTestCmd(t, empty.start(t).URL)
require.NoError(t, stackActionListCmd.RunE(stackActionListCmd, []string{"42"}))
assert.Equal(t, "This stack has no custom actions.\n"+msgActionsIncomplete+"\n", buf.String())
})
}

Expand Down Expand Up @@ -198,7 +211,7 @@ func TestStackActionRunCmd_UsageErrors(t *testing.T) {
{"required empty", "refresh-db", []string{"note= "}, `parameter "note" is required and cannot be empty`},
{"no equals sign", "refresh-db", []string{"note"}, `--param "note": use key=value`},
{"duplicate", "refresh-db", []string{"note=a", "note=b"}, "--param note is set more than once"},
{"unknown action", "nope", nil, `unknown action "nope" (actions: refresh-db, ping)`},
{"unknown action", "nope", nil, `unknown action "nope" (actions of this stack: refresh-db, ping)`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -443,3 +456,28 @@ func TestParameterSummary(t *testing.T) {
{Name: "c", Type: "bool", Default: false},
}))
}

func TestFindAction_ScopeMessages(t *testing.T) {
t.Parallel()
ping := types.ActionInfo{Name: "ping"}
tests := []struct {
name string
list types.ActionList
wantErr string
}{
{"no actions", types.ActionList{}, `unknown action "nope": this stack has no custom actions`},
{"no actions, scope incomplete", types.ActionList{ScopeIncomplete: true},
`unknown action "nope": this stack has no custom actions; some actions could not be checked for this stack, try again`},
{"other actions, scope incomplete", types.ActionList{Actions: []types.ActionInfo{ping}, ScopeIncomplete: true},
`unknown action "nope" (actions of this stack: ping); some actions could not be checked for this stack, try again`},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
_, err := findAction(&tt.list, "nope")
require.Error(t, err)
assert.Equal(t, tt.wantErr, err.Error())
})
}
}
7 changes: 6 additions & 1 deletion cli/pkg/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,16 @@ type ChannelDeliveryLog struct {
}

// ActionList is the response of GET /api/v1/stack-instances/:id/actions:
// the custom actions of the hooks config, without URLs or secrets.
// the custom actions of the hooks config that apply to the stack, without
// URLs or secrets.
type ActionList struct {
InstanceID string `json:"instance_id" yaml:"instance_id"`
Actions []ActionInfo `json:"actions" yaml:"actions"`
CanInvoke bool `json:"can_invoke" yaml:"can_invoke"`
// ScopeIncomplete is true when the server left out actions limited to
// some templates (applies_to) because it could not read the template
// of the stack. Omitted when false, and by servers without applies_to.
ScopeIncomplete bool `json:"scope_incomplete,omitempty" yaml:"scope_incomplete,omitempty"`
}

// ActionInfo is one custom action with its UI metadata.
Expand Down
Loading
Loading