Skip to content

Commit

Permalink
cli: add commands for managing version tags (#283)
Browse files Browse the repository at this point in the history
These changes add commands for managing version tags:

- `esc env version tag` creates, describes, or updates a version tag
- `esc env version rm` deletes a version tag
- `esc env version ls` lists version tags
  • Loading branch information
pgavlin authored Apr 19, 2024
1 parent a981d06 commit 951bd68
Show file tree
Hide file tree
Showing 10 changed files with 574 additions and 1 deletion.
3 changes: 3 additions & 0 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
- Add support for listing the revisions to an environment.
[#277](https://github.com/pulumi/esc/pull/277)

- Add support for managing version tags.
[#283](https://github.com/pulumi/esc/pull/283)

### Bug Fixes

- Ensure that redacted output is flushed in `esc run`
Expand Down
120 changes: 120 additions & 0 deletions cmd/esc/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"hash/fnv"
"io"
"io/fs"
"net/http"
"os"
"os/exec"
"path"
Expand All @@ -23,12 +24,14 @@ import (
"time"

"github.com/gofrs/uuid"
"github.com/pgavlin/fx"
"github.com/pulumi/esc"
"github.com/pulumi/esc/cmd/esc/cli/client"
"github.com/pulumi/esc/eval"
"github.com/pulumi/esc/schema"
"github.com/pulumi/esc/syntax"
"github.com/pulumi/pulumi/pkg/v3/backend/display"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
Expand All @@ -37,6 +40,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"gopkg.in/yaml.v3"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
Expand Down Expand Up @@ -641,6 +645,122 @@ func (c *testPulumiClient) ListEnvironmentRevisions(
return resp, nil
}

// CreateEnvironmentRevisionTag creates a new revision tag with the given name.
func (c *testPulumiClient) CreateEnvironmentRevisionTag(
ctx context.Context,
orgName string,
envName string,
tagName string,
revision *int,
) error {
env, _, err := c.getEnvironment(orgName, envName, "")
if err != nil {
return err
}

rev := len(env.revisions)
if revision != nil {
rev = *revision
}
if rev < 1 || rev > len(env.revisions) {
return errors.New("not found")
}

if _, ok := env.tags[tagName]; ok {
return errors.New("already exists")
}

env.tags[tagName] = rev
return nil
}

// GetEnvironmentRevisionTag returns a description of the given revision tag.
func (c *testPulumiClient) GetEnvironmentRevisionTag(
ctx context.Context,
orgName string,
envName string,
tagName string,
) (*client.EnvironmentRevisionTag, error) {
env, _, err := c.getEnvironment(orgName, envName, "")
if err != nil {
return nil, err
}

rev, ok := env.tags[tagName]
if !ok {
return nil, &apitype.ErrorResponse{Code: http.StatusNotFound}
}
return &client.EnvironmentRevisionTag{Name: tagName, Revision: rev}, nil
}

// UpdateEnvironmentRevisionTag updates the revision tag with the given name.
func (c *testPulumiClient) UpdateEnvironmentRevisionTag(
ctx context.Context,
orgName string,
envName string,
tagName string,
revision *int,
) error {
env, _, err := c.getEnvironment(orgName, envName, "")
if err != nil {
return err
}

rev := len(env.revisions)
if revision != nil {
rev = *revision
}
if rev < 1 || rev > len(env.revisions) {
return errors.New("not found")
}

if _, ok := env.tags[tagName]; !ok {
return &apitype.ErrorResponse{Code: http.StatusNotFound}
}

env.tags[tagName] = rev
return nil
}

// DeleteEnvironmentRevisionTag deletes the revision tag with the given name.
func (c *testPulumiClient) DeleteEnvironmentRevisionTag(
ctx context.Context,
orgName string,
envName string,
tagName string,
) error {
env, _, err := c.getEnvironment(orgName, envName, "")
if err != nil {
return err
}

if _, ok := env.tags[tagName]; !ok {
return errors.New("not found")
}

delete(env.tags, tagName)
return nil
}

// ListEnvironmentRevisionTags lists the revision tags for the given environment.
func (c *testPulumiClient) ListEnvironmentRevisionTags(
ctx context.Context,
orgName string,
envName string,
options client.ListEnvironmentRevisionTagsOptions,
) ([]client.EnvironmentRevisionTag, error) {
env, _, err := c.getEnvironment(orgName, envName, "")
if err != nil {
return nil, err
}

names := maps.Keys(env.tags)
slices.Sort(names)
return fx.ToSlice(fx.FMap(fx.IterSlice(names), func(name string) (client.EnvironmentRevisionTag, bool) {
return client.EnvironmentRevisionTag{Name: name, Revision: env.tags[name]}, name > options.After
})), nil
}

type testExec struct {
fs testFS
environ map[string]string
Expand Down
100 changes: 100 additions & 0 deletions cmd/esc/cli/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,26 @@ type Client interface {
envName string,
options ListEnvironmentRevisionsOptions,
) ([]EnvironmentRevision, error)

// CreateEnvironmentRevisionTag creates a new revision tag with the given name.
CreateEnvironmentRevisionTag(ctx context.Context, orgName, envName, tagName string, revision *int) error

// GetEnvironmentRevisionTag returns a description of the given revision tag.
GetEnvironmentRevisionTag(ctx context.Context, orgName, envName, tagName string) (*EnvironmentRevisionTag, error)

// UpdateEnvironmentRevisionTag updates the revision tag with the given name.
UpdateEnvironmentRevisionTag(ctx context.Context, orgName, envName, tagName string, revision *int) error

// DeleteEnvironmentRevisionTag deletes the revision tag with the given name.
DeleteEnvironmentRevisionTag(ctx context.Context, orgName, envName, tagName string) error

// ListEnvironmentRevisionTags lists the revision tags for the given environment.
ListEnvironmentRevisionTags(
ctx context.Context,
orgName string,
envName string,
options ListEnvironmentRevisionTagsOptions,
) ([]EnvironmentRevisionTag, error)
}

type client struct {
Expand Down Expand Up @@ -537,6 +557,80 @@ func (pc *client) ListEnvironmentRevisions(
return resp, nil
}

// CreateEnvironmentRevisionTag creates a new revision tag with the given name.
func (pc *client) CreateEnvironmentRevisionTag(
ctx context.Context,
orgName string,
envName string,
tagName string,
revision *int,
) error {
req := CreateEnvironmentRevisionTagRequest{Revision: revision}
path := fmt.Sprintf("/api/preview/environments/%v/%v/tags/%v", orgName, envName, tagName)
return pc.restCall(ctx, http.MethodPost, path, nil, &req, nil)
}

// GetEnvironmentRevisionTag returns a description of the given revision tag.
func (pc *client) GetEnvironmentRevisionTag(
ctx context.Context,
orgName string,
envName string,
tagName string,
) (*EnvironmentRevisionTag, error) {
var resp EnvironmentRevisionTag
path := fmt.Sprintf("/api/preview/environments/%v/%v/tags/%v", orgName, envName, tagName)
err := pc.restCall(ctx, http.MethodGet, path, nil, nil, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}

// UpdateEnvironmentRevisionTag updates the revision tag with the given name.
func (pc *client) UpdateEnvironmentRevisionTag(
ctx context.Context,
orgName string,
envName string,
tagName string,
revision *int,
) error {
req := UpdateEnvironmentRevisionTagRequest{Revision: revision}
path := fmt.Sprintf("/api/preview/environments/%v/%v/tags/%v", orgName, envName, tagName)
return pc.restCall(ctx, http.MethodPatch, path, nil, &req, nil)
}

// DeleteEnvironmentRevisionTag deletes the revision tag with the given name.
func (pc *client) DeleteEnvironmentRevisionTag(
ctx context.Context,
orgName string,
envName string,
tagName string,
) error {
path := fmt.Sprintf("/api/preview/environments/%v/%v/tags/%v", orgName, envName, tagName)
return pc.restCall(ctx, http.MethodDelete, path, nil, nil, nil)
}

type ListEnvironmentRevisionTagsOptions struct {
After string `url:"after"`
Count *int `url:"count"`
}

// ListEnvironmentRevisionTags lists the revision tags for the given environment.
func (pc *client) ListEnvironmentRevisionTags(
ctx context.Context,
orgName string,
envName string,
options ListEnvironmentRevisionTagsOptions,
) ([]EnvironmentRevisionTag, error) {
var resp ListEnvironmentRevisionTagsResponse
path := fmt.Sprintf("/api/preview/environments/%v/%v/tags", orgName, envName)
err := pc.restCall(ctx, http.MethodGet, path, options, nil, &resp)
if err != nil {
return nil, err
}
return resp.Tags, nil
}

type httpCallOptions struct {
// RetryPolicy defines the policy for retrying requests by httpClient.Do.
//
Expand Down Expand Up @@ -762,3 +856,9 @@ func cleanPath(p string) string {

return np
}

// IsNotFound returns true if the indicated error is a "not found" error.
func IsNotFound(err error) bool {
resp, ok := err.(*apitype.ErrorResponse)
return ok && resp.Code == http.StatusNotFound
}
1 change: 1 addition & 0 deletions cmd/esc/cli/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func newEnvCmd(esc *escCommand) *cobra.Command {
cmd.AddCommand(newEnvGetCmd(env))
cmd.AddCommand(newEnvSetCmd(env))
cmd.AddCommand(newEnvLogCmd(env))
cmd.AddCommand(newEnvVersionCmd(env))
cmd.AddCommand(newEnvLsCmd(env))
cmd.AddCommand(newEnvRmCmd(env))
cmd.AddCommand(newEnvOpenCmd(env))
Expand Down
28 changes: 28 additions & 0 deletions cmd/esc/cli/env_version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright 2023, Pulumi Corporation.

package cli

import (
"github.com/spf13/cobra"
)

func newEnvVersionCmd(env *envCommand) *cobra.Command {
cmd := &cobra.Command{
Use: "version",
Short: "Manage version tags",
Long: "Manage version tags\n" +
"\n" +
"This command creates, inspects, updates, deletes, and lists version tags.\n" +
"A version tag is a name that refers to a specific revision of an environment.\n" +
"Once created, version tags can be updated to refer to new reversions\n" +
"of an environment. Version tags can be used to refer to a particular logical\n" +
"version of an environment rather than a specific revision.\n",
SilenceUsage: true,
}

cmd.AddCommand(newEnvVersionTagCmd(env))
cmd.AddCommand(newEnvVersionRmCmd(env))
cmd.AddCommand(newEnvVersionLsCmd(env))

return cmd
}
77 changes: 77 additions & 0 deletions cmd/esc/cli/env_version_ls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2023, Pulumi Corporation.

package cli

import (
"context"
"fmt"
"io"

"github.com/spf13/cobra"

"github.com/pulumi/esc/cmd/esc/cli/client"
"github.com/pulumi/esc/cmd/esc/cli/style"
)

func newEnvVersionLsCmd(env *envCommand) *cobra.Command {
var pagerFlag string
var utc bool

cmd := &cobra.Command{
Use: "ls [<org-name>/]<environment-name>",
Short: "List version tags.",
Long: "List version tags\n" +
"\n" +
"This command lists the version tags for an environment.\n",
SilenceUsage: true,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()

if err := env.esc.getCachedClient(ctx); err != nil {
return err
}

orgName, envName, revisionOrTag, args, err := env.getEnvName(args)
if err != nil {
return err
}
if revisionOrTag != "" {
return fmt.Errorf("the ls command does not accept revisions or tags")
}
_ = args

st := style.NewStylist(style.Profile(env.esc.stdout))

after := ""
return env.esc.pager.Run(pagerFlag, env.esc.stdout, env.esc.stderr, func(ctx context.Context, stdout io.Writer) error {
count := 500
for {
options := client.ListEnvironmentRevisionTagsOptions{
After: after,
Count: &count,
}
tags, err := env.esc.client.ListEnvironmentRevisionTags(ctx, orgName, envName, options)
if err != nil {
return err
}
if len(tags) == 0 {
break
}
after = tags[len(tags)-1].Name

for _, t := range tags {
printRevisionTag(stdout, st, t, utc)
fmt.Fprintf(stdout, "\n")
}
}
return nil
})
},
}

cmd.Flags().StringVar(&pagerFlag, "pager", "", "the command to use to page through the environment's version tags")
cmd.Flags().BoolVar(&utc, "utc", false, "display times in UTC")

return cmd
}
Loading

0 comments on commit 951bd68

Please sign in to comment.