Add commands for managing stack tags (#2333)
Adds `pulumi stack tag` commands for managing stack tags.
This commit is contained in:
parent
3e65bc6517
commit
5d3d8c01dd
|
@ -2,6 +2,8 @@
|
|||
|
||||
### Improvements
|
||||
|
||||
- Added `pulumi stack tag` commands for managing stack tags stored in the cloud backend.
|
||||
|
||||
- Link directly to /account/tokens when prompting for an access token.
|
||||
|
||||
## 0.16.9 (Released December 24th, 2018)
|
||||
|
|
|
@ -173,6 +173,7 @@ func newStackCmd() *cobra.Command {
|
|||
cmd.AddCommand(newStackOutputCmd())
|
||||
cmd.AddCommand(newStackRmCmd())
|
||||
cmd.AddCommand(newStackSelectCmd())
|
||||
cmd.AddCommand(newStackTagCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
203
cmd/stack_tag.go
Normal file
203
cmd/stack_tag.go
Normal file
|
@ -0,0 +1,203 @@
|
|||
// Copyright 2016-2018, Pulumi Corporation.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/apitype"
|
||||
"github.com/pulumi/pulumi/pkg/backend"
|
||||
"github.com/pulumi/pulumi/pkg/backend/display"
|
||||
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
||||
)
|
||||
|
||||
func newStackTagCmd() *cobra.Command {
|
||||
var stack string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "tag",
|
||||
Short: "Manage stack tags",
|
||||
Long: "Manage stack tags\n" +
|
||||
"\n" +
|
||||
"Stacks have associated metadata in the form of tags. Each tag consists of a name\n" +
|
||||
"and value. The `get`, `ls`, `rm`, and `set` commands can be used to manage tags.\n" +
|
||||
"Some tags are automatically assigned based on the environment each time a stack\n" +
|
||||
"is updated.\n",
|
||||
Args: cmdutil.NoArgs,
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringVarP(
|
||||
&stack, "stack", "s", "", "The name of the stack to operate on. Defaults to the current stack")
|
||||
|
||||
cmd.AddCommand(newStackTagGetCmd(&stack))
|
||||
cmd.AddCommand(newStackTagLsCmd(&stack))
|
||||
cmd.AddCommand(newStackTagRmCmd(&stack))
|
||||
cmd.AddCommand(newStackTagSetCmd(&stack))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newStackTagGetCmd(stack *string) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "get <name>",
|
||||
Short: "Get a single stack tag value",
|
||||
Args: cmdutil.SpecificArgs([]string{"name"}),
|
||||
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
opts := display.Options{
|
||||
Color: cmdutil.GetGlobalColorization(),
|
||||
}
|
||||
s, err := requireStack(*stack, false, opts, true /*setCurrent*/)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tags, err := backend.GetStackTags(commandContext(), s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if value, ok := tags[name]; ok {
|
||||
fmt.Printf("%v\n", value)
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.Errorf(
|
||||
"stack tag '%s' not found for stack '%s'", name, s.Ref())
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func newStackTagLsCmd(stack *string) *cobra.Command {
|
||||
var jsonOut bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls",
|
||||
Short: "List all stack tags",
|
||||
Args: cmdutil.NoArgs,
|
||||
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
|
||||
opts := display.Options{
|
||||
Color: cmdutil.GetGlobalColorization(),
|
||||
}
|
||||
s, err := requireStack(*stack, false, opts, true /*setCurrent*/)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tags, err := backend.GetStackTags(commandContext(), s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if jsonOut {
|
||||
return printJSON(tags)
|
||||
}
|
||||
|
||||
printStackTags(tags)
|
||||
return nil
|
||||
}),
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().BoolVarP(
|
||||
&jsonOut, "json", "j", false, "Emit stack tags as JSON")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func printStackTags(tags map[apitype.StackTagName]string) {
|
||||
var names []string
|
||||
for n := range tags {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
rows := []cmdutil.TableRow{}
|
||||
for _, name := range names {
|
||||
rows = append(rows, cmdutil.TableRow{Columns: []string{name, tags[name]}})
|
||||
}
|
||||
|
||||
cmdutil.PrintTable(cmdutil.Table{
|
||||
Headers: []string{"NAME", "VALUE"},
|
||||
Rows: rows,
|
||||
})
|
||||
}
|
||||
|
||||
func newStackTagRmCmd(stack *string) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "rm <name>",
|
||||
Short: "Remove a stack tag",
|
||||
Args: cmdutil.SpecificArgs([]string{"name"}),
|
||||
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
opts := display.Options{
|
||||
Color: cmdutil.GetGlobalColorization(),
|
||||
}
|
||||
s, err := requireStack(*stack, false, opts, true /*setCurrent*/)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := commandContext()
|
||||
|
||||
tags, err := backend.GetStackTags(ctx, s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delete(tags, name)
|
||||
|
||||
return backend.UpdateStackTags(ctx, s, tags)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func newStackTagSetCmd(stack *string) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "set <name> <value>",
|
||||
Short: "Set a stack tag",
|
||||
Args: cmdutil.SpecificArgs([]string{"name", "value"}),
|
||||
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
value := args[1]
|
||||
|
||||
opts := display.Options{
|
||||
Color: cmdutil.GetGlobalColorization(),
|
||||
}
|
||||
s, err := requireStack(*stack, false, opts, true /*setCurrent*/)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := commandContext()
|
||||
|
||||
tags, err := backend.GetStackTags(ctx, s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tags == nil {
|
||||
tags = make(map[apitype.StackTagName]string)
|
||||
}
|
||||
tags[name] = value
|
||||
|
||||
return backend.UpdateStackTags(ctx, s, tags)
|
||||
}),
|
||||
}
|
||||
}
|
|
@ -115,6 +115,11 @@ type Backend interface {
|
|||
// Get the configuration from the most recent deployment of the stack.
|
||||
GetLatestConfiguration(ctx context.Context, stackRef StackReference) (config.Map, error)
|
||||
|
||||
// GetStackTags fetches the stack's existing tags.
|
||||
GetStackTags(ctx context.Context, stackRef StackReference) (map[apitype.StackTagName]string, error)
|
||||
// UpdateStackTags updates the stacks's tags, replacing all existing tags.
|
||||
UpdateStackTags(ctx context.Context, stackRef StackReference, tags map[apitype.StackTagName]string) error
|
||||
|
||||
// ExportDeployment exports the deployment for the given stack as an opaque JSON message.
|
||||
ExportDeployment(ctx context.Context, stackRef StackReference) (*apitype.UntypedDeployment, error)
|
||||
// ImportDeployment imports the given deployment into the indicated stack.
|
||||
|
|
|
@ -146,7 +146,7 @@ func (b *localBackend) CreateStack(ctx context.Context, stackRef backend.StackRe
|
|||
return nil, &backend.StackAlreadyExistsError{StackName: string(stackName)}
|
||||
}
|
||||
|
||||
tags, err := backend.GetStackTags()
|
||||
tags, err := backend.GetEnvironmentTagsForCurrentStack()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting stack tags")
|
||||
}
|
||||
|
@ -538,3 +538,19 @@ func (b *localBackend) getLocalStacks() ([]tokens.QName, error) {
|
|||
|
||||
return stacks, nil
|
||||
}
|
||||
|
||||
// GetStackTags fetches the stack's existing tags.
|
||||
func (b *localBackend) GetStackTags(ctx context.Context,
|
||||
stackRef backend.StackReference) (map[apitype.StackTagName]string, error) {
|
||||
|
||||
// The local backend does not currently persist tags.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UpdateStackTags updates the stacks's tags, replacing all existing tags.
|
||||
func (b *localBackend) UpdateStackTags(ctx context.Context,
|
||||
stackRef backend.StackReference, tags map[apitype.StackTagName]string) error {
|
||||
|
||||
// The local backend does not currently persist tags.
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -509,7 +509,7 @@ func (b *cloudBackend) CreateStack(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
tags, err := backend.GetStackTags()
|
||||
tags, err := backend.GetEnvironmentTagsForCurrentStack()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error determining initial tags")
|
||||
}
|
||||
|
@ -648,10 +648,12 @@ func (b *cloudBackend) Destroy(ctx context.Context, stackRef backend.StackRefere
|
|||
}
|
||||
|
||||
func (b *cloudBackend) createAndStartUpdate(
|
||||
ctx context.Context, action apitype.UpdateKind, stackRef backend.StackReference,
|
||||
ctx context.Context, action apitype.UpdateKind, stack backend.Stack,
|
||||
op backend.UpdateOperation, dryRun bool) (client.UpdateIdentifier, int, string, error) {
|
||||
|
||||
stack, err := b.getCloudStackIdentifier(stackRef)
|
||||
stackRef := stack.Ref()
|
||||
|
||||
stackID, err := b.getCloudStackIdentifier(stackRef)
|
||||
if err != nil {
|
||||
return client.UpdateIdentifier{}, 0, "", err
|
||||
}
|
||||
|
@ -672,14 +674,14 @@ func (b *cloudBackend) createAndStartUpdate(
|
|||
Environment: op.M.Environment,
|
||||
}
|
||||
update, err := b.client.CreateUpdate(
|
||||
ctx, action, stack, op.Proj, workspaceStack.Config, metadata, op.Opts.Engine, dryRun)
|
||||
ctx, action, stackID, op.Proj, workspaceStack.Config, metadata, op.Opts.Engine, dryRun)
|
||||
if err != nil {
|
||||
return client.UpdateIdentifier{}, 0, "", err
|
||||
}
|
||||
|
||||
// Start the update. We use this opportunity to pass new tags to the service, to pick up any
|
||||
// metadata changes.
|
||||
tags, err := backend.GetStackTags()
|
||||
tags, err := backend.GetMergedStackTags(ctx, stack)
|
||||
if err != nil {
|
||||
return client.UpdateIdentifier{}, 0, "", errors.Wrap(err, "getting stack tags")
|
||||
}
|
||||
|
@ -704,7 +706,7 @@ func (b *cloudBackend) apply(ctx context.Context, kind apitype.UpdateKind, stack
|
|||
colors.SpecHeadline+"%s (%s):"+colors.Reset+"\n"), actionLabel, stack.Ref())
|
||||
|
||||
// Create an update object to persist results.
|
||||
update, version, token, err := b.createAndStartUpdate(ctx, kind, stack.Ref(), op, opts.DryRun)
|
||||
update, version, token, err := b.createAndStartUpdate(ctx, kind, stack, op, opts.DryRun)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1155,3 +1157,30 @@ func IsValidAccessToken(ctx context.Context, cloudURL, accessToken string) (bool
|
|||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GetStackTags fetches the stack's existing tags.
|
||||
func (b *cloudBackend) GetStackTags(ctx context.Context,
|
||||
stackRef backend.StackReference) (map[apitype.StackTagName]string, error) {
|
||||
|
||||
stack, err := b.GetStack(ctx, stackRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if stack == nil {
|
||||
return nil, errors.New("stack not found")
|
||||
}
|
||||
|
||||
return stack.(Stack).Tags(), nil
|
||||
}
|
||||
|
||||
// UpdateStackTags updates the stacks's tags, replacing all existing tags.
|
||||
func (b *cloudBackend) UpdateStackTags(ctx context.Context,
|
||||
stackRef backend.StackReference, tags map[apitype.StackTagName]string) error {
|
||||
|
||||
stack, err := b.getCloudStackIdentifier(stackRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.client.UpdateStackTags(ctx, stack, tags)
|
||||
}
|
||||
|
|
|
@ -516,3 +516,15 @@ func (pc *Client) RecordEngineEvent(
|
|||
nil, event, nil,
|
||||
updateAccessToken(token), callOpts)
|
||||
}
|
||||
|
||||
// UpdateStackTags updates the stacks's tags, replacing all existing tags.
|
||||
func (pc *Client) UpdateStackTags(
|
||||
ctx context.Context, stack StackIdentifier, tags map[apitype.StackTagName]string) error {
|
||||
|
||||
// Validate stack tags.
|
||||
if err := backend.ValidateStackTags(tags); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return pc.restCall(ctx, "PATCH", getStackPath(stack, "tags"), nil, tags, nil)
|
||||
}
|
||||
|
|
|
@ -17,10 +17,11 @@ package backend
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pulumi/pulumi/pkg/apitype"
|
||||
"github.com/pulumi/pulumi/pkg/engine"
|
||||
|
@ -107,9 +108,46 @@ func ImportStackDeployment(ctx context.Context, s Stack, deployment *apitype.Unt
|
|||
return s.Backend().ImportDeployment(ctx, s.Ref(), deployment)
|
||||
}
|
||||
|
||||
// GetStackTags returns the set of tags for the "current" stack, based on the environment
|
||||
// GetStackTags fetches the stack's existing tags.
|
||||
func GetStackTags(ctx context.Context, s Stack) (map[apitype.StackTagName]string, error) {
|
||||
return s.Backend().GetStackTags(ctx, s.Ref())
|
||||
}
|
||||
|
||||
// UpdateStackTags updates the stacks's tags, replacing all existing tags.
|
||||
func UpdateStackTags(ctx context.Context, s Stack, tags map[apitype.StackTagName]string) error {
|
||||
return s.Backend().UpdateStackTags(ctx, s.Ref(), tags)
|
||||
}
|
||||
|
||||
// GetMergedStackTags returns the stack's existing tags merged with fresh tags from the environment
|
||||
// and Pulumi.yaml file.
|
||||
func GetStackTags() (map[apitype.StackTagName]string, error) {
|
||||
func GetMergedStackTags(ctx context.Context, s Stack) (map[apitype.StackTagName]string, error) {
|
||||
// Get the stack's existing tags.
|
||||
tags, err := GetStackTags(ctx, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tags == nil {
|
||||
tags = make(map[apitype.StackTagName]string)
|
||||
}
|
||||
|
||||
// Get latest environment tags for the current stack.
|
||||
envTags, err := GetEnvironmentTagsForCurrentStack()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add each new environment tag to the existing tags, overwriting existing tags with the
|
||||
// latest values.
|
||||
for k, v := range envTags {
|
||||
tags[k] = v
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// GetEnvironmentTagsForCurrentStack returns the set of tags for the "current" stack, based on the environment
|
||||
// and Pulumi.yaml file.
|
||||
func GetEnvironmentTagsForCurrentStack() (map[apitype.StackTagName]string, error) {
|
||||
tags := make(map[apitype.StackTagName]string)
|
||||
|
||||
// Tags based on Pulumi.yaml.
|
||||
|
@ -184,21 +222,11 @@ func validateStackName(s string) error {
|
|||
return errors.New("a stack name may only contain alphanumeric, hyphens, underscores, or periods")
|
||||
}
|
||||
|
||||
// ValidateStackProperties validates the stack name and its tags to confirm they adhear to various
|
||||
// naming and length restrictions.
|
||||
func ValidateStackProperties(stack string, tags map[apitype.StackTagName]string) error {
|
||||
const maxStackName = 100 // Derived from the regex in validateStackName.
|
||||
if len(stack) > maxStackName {
|
||||
return errors.Errorf("stack name too long (max length %d characters)", maxStackName)
|
||||
}
|
||||
if err := validateStackName(stack); err != nil {
|
||||
return errors.Wrapf(err, "invalid stack name")
|
||||
}
|
||||
|
||||
// Ensure tag values won't be rejected by the Pulumi Service. We do not validate that their
|
||||
// values make sense, e.g. ProjectRuntimeTag is a supported runtime.
|
||||
// ValidateStackTags validates the tag names and values.
|
||||
func ValidateStackTags(tags map[apitype.StackTagName]string) error {
|
||||
const maxTagName = 40
|
||||
const maxTagValue = 256
|
||||
|
||||
for t, v := range tags {
|
||||
if len(t) == 0 {
|
||||
return errors.Errorf("invalid stack tag %q", t)
|
||||
|
@ -213,3 +241,19 @@ func ValidateStackProperties(stack string, tags map[apitype.StackTagName]string)
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateStackProperties validates the stack name and its tags to confirm they adhear to various
|
||||
// naming and length restrictions.
|
||||
func ValidateStackProperties(stack string, tags map[apitype.StackTagName]string) error {
|
||||
const maxStackName = 100 // Derived from the regex in validateStackName.
|
||||
if len(stack) > maxStackName {
|
||||
return errors.Errorf("stack name too long (max length %d characters)", maxStackName)
|
||||
}
|
||||
if err := validateStackName(stack); err != nil {
|
||||
return errors.Wrapf(err, "invalid stack name")
|
||||
}
|
||||
|
||||
// Ensure tag values won't be rejected by the Pulumi Service. We do not validate that their
|
||||
// values make sense, e.g. ProjectRuntimeTag is a supported runtime.
|
||||
return ValidateStackTags(tags)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue