Add commands for managing stack tags (#2333)

Adds `pulumi stack tag` commands for managing stack tags.
This commit is contained in:
Justin Van Patten 2019-01-04 13:23:47 -08:00 committed by GitHub
parent 3e65bc6517
commit 5d3d8c01dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 335 additions and 23 deletions

View file

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

View file

@ -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
View 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)
}),
}
}

View file

@ -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.

View file

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

View file

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

View file

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

View file

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