Skip to content

Commit

Permalink
cli: esc env log
Browse files Browse the repository at this point in the history
These changes add a command to the CLI that displays the revision
history of an environment. This command, `esc env log`, is modeled after
`git log`. If a specific version of an environment is given, the history
begins at the corresponding revision.

The most complicated part of these changes is the pager support. If a
system pager is available, the output is sent to that pager. Like `git`,
the pager defaults to `less`, but can be changed via the `--pager` flag
or the `PAGER` environment variables. If no pager is available, however,
we use a built-in pager. This is primarily intended to address platforms
where `less` or other pagers are not present (e.g. stock Windows).
  • Loading branch information
pgavlin committed Mar 29, 2024
1 parent 57acb56 commit 8beb8d8
Show file tree
Hide file tree
Showing 17 changed files with 730 additions and 27 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
### Improvements



- Add support for getting or opening environments at specific revisions/tags.
[#275](https://github.com/pulumi/esc/pull/275)

- Add support for listing the revisions to an environment.
[#277](https://github.com/pulumi/esc/pull/277)

### Bug Fixes

59 changes: 52 additions & 7 deletions cmd/esc/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,10 +347,10 @@ func (c *testPulumiClient) getEnvironment(orgName, envName, revisionOrTag string

var revision int
if revisionOrTag == "" || revisionOrTag == "latest" {
revision = len(env.revisions) - 1
revision = len(env.revisions)
} else if revisionOrTag[0] >= '0' && revisionOrTag[0] <= '9' {
rev, err := strconv.ParseInt(revisionOrTag, 10, 0)
if err != nil || rev < 0 || rev >= int64(len(env.revisions)) {
if err != nil || rev < 1 || rev > int64(len(env.revisions)) {
return nil, nil, errors.New("not found")
}
revision = int(rev)
Expand All @@ -362,7 +362,7 @@ func (c *testPulumiClient) getEnvironment(orgName, envName, revisionOrTag string
revision = rev
}

return env, env.revisions[revision], nil
return env, env.revisions[revision-1], nil
}

func (c *testPulumiClient) checkEnvironment(ctx context.Context, orgName, envName string, yaml []byte) (*esc.Environment, []client.EnvironmentDiagnostic, error) {
Expand Down Expand Up @@ -436,6 +436,15 @@ func (c *testPulumiClient) GetPulumiAccountDetails(ctx context.Context) (string,
return c.user, nil, nil, nil
}

func (c *testPulumiClient) GetRevisionNumber(ctx context.Context, orgName, envName, revisionOrTag string) (int, error) {
_, rev, err := c.getEnvironment(orgName, envName, revisionOrTag)
if err != nil {
return 0, err
}
return rev.number, nil

}

func (c *testPulumiClient) ListEnvironments(
ctx context.Context,
orgName string,
Expand Down Expand Up @@ -532,7 +541,7 @@ func (c *testPulumiClient) UpdateEnvironment(
return nil, err
}

revisionNumber := len(env.revisions)
revisionNumber := len(env.revisions) + 1
env.revisions = append(env.revisions, &testEnvironmentRevision{
number: revisionNumber,
yaml: yaml,
Expand Down Expand Up @@ -597,6 +606,35 @@ func (c *testPulumiClient) GetOpenProperty(ctx context.Context, orgName, envName
return nil, errors.New("NYI")
}

func (c *testPulumiClient) ListEnvironmentRevisions(
ctx context.Context,
orgName string,
envName string,
options client.ListEnvironmentRevisionsOptions,
) ([]client.EnvironmentRevision, error) {
env, _, err := c.getEnvironment(orgName, envName, "")
if err != nil {
return nil, err
}

before := len(env.revisions) + 1
if options.Before != nil && *options.Before != 0 {
before = *options.Before
}

var resp []client.EnvironmentRevision
for i := before - 1; i > 0; i-- {
resp = append(resp, client.EnvironmentRevision{
Number: i,
Created: time.Unix(0, 0).Add(time.Duration(i) * time.Hour),
CreatorLogin: "Test Tester",
CreatorName: "test-tester",
})
}

return resp, nil
}

type testExec struct {
fs testFS
environ map[string]string
Expand Down Expand Up @@ -659,6 +697,7 @@ func (c *testExec) runScript(script string, cmd *exec.Cmd) error {
fs: c.fs,
environ: environ,
exec: c,
pager: testPager(0),
newClient: func(_, backendURL, accessToken string, insecure bool) client.Client {
return c.client
},
Expand Down Expand Up @@ -715,6 +754,12 @@ func (c *testExec) runScript(script string, cmd *exec.Cmd) error {
return runner.Run(context.Background(), file)
}

type testPager int

func (testPager) Run(pager string, stdout, stderr io.Writer, f func(context.Context, io.Writer) error) error {
return f(context.Background(), stdout)
}

type cliTestcaseProcess struct {
FS map[string]string `yaml:"fs,omitempty"`
Environ map[string]string `yaml:"environ,omitempty"`
Expand Down Expand Up @@ -786,15 +831,15 @@ func loadTestcase(path string) (*cliTestcaseYAML, *cliTestcase, error) {
revisions = cliTestcaseRevisions{Revisions: []cliTestcaseRevision{{YAML: env}}}
}

envRevisions := []*testEnvironmentRevision{{}}
envRevisions := []*testEnvironmentRevision{{number: 1}}
tags := map[string]int{}
for _, rev := range revisions.Revisions {
bytes, err := yaml.Marshal(rev.YAML)
if err != nil {
return nil, nil, err
}

revisionNumber := len(envRevisions)
revisionNumber := len(envRevisions) + 1
envRevisions = append(envRevisions, &testEnvironmentRevision{
number: revisionNumber,
yaml: bytes,
Expand All @@ -807,7 +852,7 @@ func loadTestcase(path string) (*cliTestcaseYAML, *cliTestcase, error) {
tags[rev.Tag] = revisionNumber
}
}
tags["latest"] = len(envRevisions) - 1
tags["latest"] = len(envRevisions)

environments[k] = &testEnvironment{
revisions: envRevisions,
Expand Down
54 changes: 54 additions & 0 deletions cmd/esc/cli/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"net/http"
"path"
"runtime"
"strconv"
"strings"
"time"

Expand All @@ -49,6 +50,9 @@ type Client interface {
// GetPulumiAccountDetails returns the user implied by the API token associated with this client.
GetPulumiAccountDetails(ctx context.Context) (string, []string, *workspace.TokenInformation, error)

// GetRevisionNumber returns the revision number for revisionOrTag.
GetRevisionNumber(ctx context.Context, orgName, envName, revisionOrTag string) (int, error)

// ListEnvironments lists all environments in the given org that are accessible to the calling user.
//
// Each call to ListEnvironments returns a page of results and a continuation token. If there are no
Expand Down Expand Up @@ -145,6 +149,16 @@ type Client interface {
// environmentVariables["AWS_ACCESS_KEY_ID"]
//
GetOpenProperty(ctx context.Context, orgName, envName, openEnvID, property string) (*esc.Value, error)

// ListEnvironmentRevisions returns a list of revisions to the named environments in reverse order by
// revision number. The revision at which to start and the number of revisions to return are
// configurable via the options parameter.
ListEnvironmentRevisions(
ctx context.Context,
orgName string,
envName string,
options ListEnvironmentRevisionsOptions,
) ([]EnvironmentRevision, error)
}

type client struct {
Expand Down Expand Up @@ -284,6 +298,26 @@ func (pc *client) resolveEnvironmentPath(ctx context.Context, orgName, envName,
return fmt.Sprintf("/api/preview/environments/%v/%v/revisions/%v", orgName, envName, resp.Revision), nil
}

func (pc *client) GetRevisionNumber(ctx context.Context, orgName, envName, revisionOrTag string) (int, error) {
if revisionOrTag == "" {
revisionOrTag = "latest"
} else if revisionOrTag[0] >= '0' && revisionOrTag[0] <= '9' {
rev, err := strconv.ParseInt(revisionOrTag, 10, 0)
if err != nil {
return 0, fmt.Errorf("invalid revision number %q", revisionOrTag)
}
return int(rev), nil
}

path := fmt.Sprintf("/api/preview/environments/%v/%v/tags/%v", orgName, envName, revisionOrTag)

var resp EnvironmentRevisionTag
if err := pc.restCall(ctx, http.MethodGet, path, nil, nil, &resp); err != nil {
return 0, fmt.Errorf("resolving tag %q: %w", revisionOrTag, err)
}
return resp.Revision, nil
}

func (pc *client) ListEnvironments(
ctx context.Context,
orgName string,
Expand Down Expand Up @@ -483,6 +517,26 @@ func (pc *client) GetOpenProperty(ctx context.Context, orgName, envName, openSes
return &resp, nil
}

type ListEnvironmentRevisionsOptions struct {
Before *int `url:"before"`
Count *int `url:"count"`
}

func (pc *client) ListEnvironmentRevisions(
ctx context.Context,
orgName string,
envName string,
options ListEnvironmentRevisionsOptions,
) ([]EnvironmentRevision, error) {
var resp []EnvironmentRevision
path := fmt.Sprintf("/api/preview/environments/%v/%v/revisions", orgName, envName)
err := pc.restCall(ctx, http.MethodGet, path, options, nil, &resp)
if err != nil {
return nil, err
}
return resp, nil
}

type httpCallOptions struct {
// RetryPolicy defines the policy for retrying requests by httpClient.Do.
//
Expand Down
1 change: 1 addition & 0 deletions cmd/esc/cli/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func newEnvCmd(esc *escCommand) *cobra.Command {
cmd.AddCommand(newEnvEditCmd(env))
cmd.AddCommand(newEnvGetCmd(env))
cmd.AddCommand(newEnvSetCmd(env))
cmd.AddCommand(newEnvLogCmd(env))
cmd.AddCommand(newEnvLsCmd(env))
cmd.AddCommand(newEnvRmCmd(env))
cmd.AddCommand(newEnvOpenCmd(env))
Expand Down
95 changes: 95 additions & 0 deletions cmd/esc/cli/env_log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2023, Pulumi Corporation.

package cli

import (
"context"
"fmt"
"io"

"github.com/spf13/cobra"

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

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

cmd := &cobra.Command{
Use: "log [<org-name>/]<environment-name>[:<revision-or-tag>]",
Short: "Show revision logs.",
Long: "Show revision logs\n" +
"\n" +
"This command shows the revision logs for an environment. If a revision\n" +
"or tag is present, the logs will start at the given revision.\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
}
_ = args

before := 0
if revisionOrTag != "" {
rev, err := env.esc.client.GetRevisionNumber(ctx, orgName, envName, revisionOrTag)
if err != nil {
return err
}
before = rev + 1
}

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.ListEnvironmentRevisionsOptions{
Before: &before,
Count: &count,
}
revisions, err := env.esc.client.ListEnvironmentRevisions(ctx, orgName, envName, options)
if err != nil {
return err
}
if len(revisions) == 0 {
break
}
before = revisions[len(revisions)-1].Number

for _, r := range revisions {
fmt.Fprintf(stdout, "revision %v\n", r.Number)
if r.CreatorLogin == "" {
fmt.Fprintf(stdout, "Author: <unknown>\n")
} else {
fmt.Fprintf(stdout, "Author: %v <%v>\n", r.CreatorName, r.CreatorLogin)
}

stamp := r.Created
if utc {
stamp = stamp.UTC()
} else {
stamp = stamp.Local()
}

fmt.Fprintf(stdout, "Date: %v\n", stamp)
fmt.Fprintf(stdout, "\n")
}
}

return nil
})
},
}

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

return cmd
}
3 changes: 3 additions & 0 deletions cmd/esc/cli/esc.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type Options struct {
fs escFS
environ environ
exec cmdExec
pager pager

newClient func(userAgent, backendURL, accessToken string, insecure bool) client.Client
}
Expand All @@ -44,6 +45,7 @@ type escCommand struct {
fs escFS
environ environ
exec cmdExec
pager pager

stdin io.Reader
stdout io.Writer
Expand All @@ -68,6 +70,7 @@ func newESC(opts *Options) *escCommand {
fs: fs,
environ: valueOrDefault(opts.environ, newEnviron()),
exec: valueOrDefault(opts.exec, newCmdExec()),
pager: valueOrDefault(opts.pager, newPager()),
stdin: valueOrDefault(opts.Stdin, io.Reader(os.Stdin)),
stdout: valueOrDefault(opts.Stdout, io.Writer(os.Stdout)),
stderr: valueOrDefault(opts.Stderr, io.Writer(os.Stderr)),
Expand Down
Loading

0 comments on commit 8beb8d8

Please sign in to comment.