Skip to content

Commit

Permalink
[env] add clone command
Browse files Browse the repository at this point in the history
  • Loading branch information
dschaller committed Sep 5, 2024
1 parent 4f71a47 commit ad43485
Show file tree
Hide file tree
Showing 8 changed files with 394 additions and 0 deletions.
33 changes: 33 additions & 0 deletions cmd/esc/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,39 @@ func (c *testPulumiClient) CreateEnvironmentWithProject(ctx context.Context, org
return nil
}

func (c *testPulumiClient) CloneEnvironment(
ctx context.Context,
orgName, projectName, envName string,
destEnv client.CloneEnvironmentRequest,
) error {
srcEnvName := path.Join(orgName, projectName, envName)
srcEnv, ok := c.environments[srcEnvName]
if !ok {
return errors.New("source env not found")
}
if destEnv.Project == "" {
destEnv.Project = projectName
}
destEnvName := path.Join(orgName, destEnv.Project, destEnv.Name)
if _, ok := c.environments[destEnvName]; ok {
return errors.New("already exists")
}
testDestEnv := &testEnvironment{
revisions: []*testEnvironmentRevision{srcEnv.revisions[len(srcEnv.revisions)-1]},
}
if destEnv.PreserveHistory {
testDestEnv.revisions = srcEnv.revisions
}
if destEnv.PreserveEnvironmentTags {
testDestEnv.tags = srcEnv.tags
}
if destEnv.PreserveRevisionTags {
testDestEnv.revisionTags = srcEnv.revisionTags
}
c.environments[destEnvName] = testDestEnv
return nil
}

func (c *testPulumiClient) GetEnvironment(
ctx context.Context,
orgName string,
Expand Down
10 changes: 10 additions & 0 deletions cmd/esc/cli/client/apitype.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ func diagsErrorString(envDiags []EnvironmentDiagnostic) string {
return diags.String()
}

type CloneEnvironmentRequest struct {
Project string `json:"project,omitempty"`
Name string `json:"name"`
Version int `json:"version,omitempty"`
PreserveHistory bool `json:"preserveHistory,omitempty"`
PreserveAccess bool `json:"preserveAccess,omitempty"`
PreserveEnvironmentTags bool `json:"preserveEnvironmentTags,omitempty"`
PreserveRevisionTags bool `json:"preserveRevisionTags,omitempty"`
}

type EnvironmentRevisionRetracted struct {
Replacement int `json:"replacement"`
At time.Time `json:"at"`
Expand Down
13 changes: 13 additions & 0 deletions cmd/esc/cli/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,13 @@ type Client interface {

// Deprecated: Use CreateEnvironmentWithProject instead
CreateEnvironment(ctx context.Context, orgName, envName string) error

// CreateEnvironment creates an environment named projectName/envName in orgName.
CreateEnvironmentWithProject(ctx context.Context, orgName, projectName, envName string) error

// CloneEnvironment clones an source environment into a new destination environment.
CloneEnvironment(ctx context.Context, orgName, srcEnvProject, srcEnvName string, destEnv CloneEnvironmentRequest) error

// GetEnvironment returns the YAML + ETag for the environment envName in org orgName. If decrypt is
// true, any { fn::secret: { ciphertext: "..." } } constructs in the definition will be decrypted and
// replaced with { fn::secret: "plaintext" }.
Expand Down Expand Up @@ -461,6 +465,15 @@ func (pc *client) CreateEnvironmentWithProject(ctx context.Context, orgName, pro
return pc.restCall(ctx, http.MethodPost, path, nil, req, nil)
}

func (pc *client) CloneEnvironment(
ctx context.Context,
orgName, srcEnvProject, srcEnvName string,
destEnv CloneEnvironmentRequest,
) error {
path := fmt.Sprintf("/api/esc/environments/%v/%v/%v/clone", orgName, srcEnvProject, srcEnvName)
return pc.restCall(ctx, http.MethodPost, path, nil, destEnv, nil)
}

func (pc *client) GetEnvironment(
ctx context.Context,
orgName string,
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 @@ -51,6 +51,7 @@ func newEnvCmd(esc *escCommand) *cobra.Command {
cmd.PersistentFlags().StringVar(&env.envNameFlag, "env", "", "The name of the environment to operate on.")

cmd.AddCommand(newEnvInitCmd(env))
cmd.AddCommand(newEnvCloneCmd(env))
cmd.AddCommand(newEnvEditCmd(env))
cmd.AddCommand(newEnvGetCmd(env))
cmd.AddCommand(newEnvDiffCmd(env))
Expand Down
97 changes: 97 additions & 0 deletions cmd/esc/cli/env_clone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2023, Pulumi Corporation.

package cli

import (
"context"
"fmt"
"strings"

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

func newEnvCloneCmd(env *envCommand) *cobra.Command {
var (
preserveHistory bool
preserveAccess bool
preserveEnvironmentTags bool
preserveRevisionTags bool
)

cmd := &cobra.Command{
Use: "clone [<org-name>/]<project-name>/<environment-name> [<project-name>/]<environment-name>",
Args: cobra.MaximumNArgs(2),
Short: "Clone an existing environment into a new environment.",
Long: "Clone an existing environment into a new environment.\n" +
"\n" +
"This command clones an existing environment with the given identifier into a new environment.\n" +
"If a project is omitted from the new environment identifier the new environment will be created\n" +
"within the same project as the environment being cloned.\n",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()

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

ref, args, err := env.getNewEnvRef(ctx, args)
if err != nil {
return err
}
if ref.version != "" {
return fmt.Errorf("the clone command does not accept versions")
}

var destProject string
destName := args[0]
destParts := strings.Split(args[0], "/")
if len(destParts) == 2 {
destProject = destParts[0]
destName = destParts[1]
}

destEnv := client.CloneEnvironmentRequest{
Project: destProject,
Name: destName,
PreserveHistory: preserveHistory,
PreserveAccess: preserveAccess,
PreserveEnvironmentTags: preserveEnvironmentTags,
PreserveRevisionTags: preserveRevisionTags,
}
if err := env.esc.client.CloneEnvironment(ctx, ref.orgName, ref.projectName, ref.envName, destEnv); err != nil {
return fmt.Errorf("cloning environment: %w", err)
}

if destProject == "" {
destProject = ref.projectName
}
fmt.Fprintf(
env.esc.stdout,
"Environment %s/%s/%s cloned into %s/%s/%s.\n",
ref.orgName, ref.projectName, ref.envName,
ref.orgName, destProject, destName,
)
return nil
},
}

cmd.Flags().BoolVar(&preserveHistory,
"history", false,
"preserve history of the environment being cloned")

cmd.Flags().BoolVar(&preserveAccess,
"access", false,
"add the newly cloned environment to the same teams that have access to the origin environment")

cmd.Flags().BoolVar(&preserveEnvironmentTags,
"envTags", false,
"preserve any tags on the environment being cloned")

cmd.Flags().BoolVar(&preserveRevisionTags,
"revTags", false,
"preserve any tags on the environment revisions being cloned")

return cmd
}
38 changes: 38 additions & 0 deletions cmd/esc/cli/testdata/env-clone-conflict.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
run: |
esc env clone default/src a
error: exit status 1
environments:
test-user/default/a: {}
test-user/default/src:
revisions:
- yaml:
values: {}
- yaml:
values:
string: hello, world!
tags:
- stable
- yaml:
imports:
- a
- b
values:
# comment
"null": null
boolean: true
number: 42
string: esc
array: [hello, world]
object: {hello: world}
open:
fn::open::test: echo
secret:
fn::secret:
ciphertext: ZXNjeAAAAAHz5ePy5fTB4+Pl8/PL5fnJxPD7
tags:
team: pulumi
stdout: |
> esc env clone default/src a
stderr: |
> esc env clone default/src a
Error: cloning environment: already exists
38 changes: 38 additions & 0 deletions cmd/esc/cli/testdata/env-clone-not-found.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
run: |
esc env clone not/found dest
error: exit status 1
environments:
test-user/default/a: {}
test-user/default/src:
revisions:
- yaml:
values: {}
- yaml:
values:
string: hello, world!
tags:
- stable
- yaml:
imports:
- a
- b
values:
# comment
"null": null
boolean: true
number: 42
string: esc
array: [hello, world]
object: {hello: world}
open:
fn::open::test: echo
secret:
fn::secret:
ciphertext: ZXNjeAAAAAHz5ePy5fTB4+Pl8/PL5fnJxPD7
tags:
team: pulumi
stdout: |
> esc env clone not/found dest
stderr: |
> esc env clone not/found dest
Error: cloning environment: source env not found
Loading

0 comments on commit ad43485

Please sign in to comment.