From ad4348569ff02bcc4d62cbc0ae18c3c95b83fdae Mon Sep 17 00:00:00 2001 From: dschaller Date: Wed, 4 Sep 2024 17:10:56 -0700 Subject: [PATCH] [env] add clone command --- cmd/esc/cli/cli_test.go | 33 ++++ cmd/esc/cli/client/apitype.go | 10 ++ cmd/esc/cli/client/client.go | 13 ++ cmd/esc/cli/env.go | 1 + cmd/esc/cli/env_clone.go | 97 +++++++++++ cmd/esc/cli/testdata/env-clone-conflict.yaml | 38 ++++ cmd/esc/cli/testdata/env-clone-not-found.yaml | 38 ++++ cmd/esc/cli/testdata/env-clone-ok.yaml | 164 ++++++++++++++++++ 8 files changed, 394 insertions(+) create mode 100644 cmd/esc/cli/env_clone.go create mode 100644 cmd/esc/cli/testdata/env-clone-conflict.yaml create mode 100644 cmd/esc/cli/testdata/env-clone-not-found.yaml create mode 100644 cmd/esc/cli/testdata/env-clone-ok.yaml diff --git a/cmd/esc/cli/cli_test.go b/cmd/esc/cli/cli_test.go index 9d24b813..2dc5b4f1 100644 --- a/cmd/esc/cli/cli_test.go +++ b/cmd/esc/cli/cli_test.go @@ -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, diff --git a/cmd/esc/cli/client/apitype.go b/cmd/esc/cli/client/apitype.go index 9ad8b2a1..ad7423c1 100644 --- a/cmd/esc/cli/client/apitype.go +++ b/cmd/esc/cli/client/apitype.go @@ -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"` diff --git a/cmd/esc/cli/client/client.go b/cmd/esc/cli/client/client.go index 8f125c90..48e193d0 100644 --- a/cmd/esc/cli/client/client.go +++ b/cmd/esc/cli/client/client.go @@ -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" }. @@ -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, diff --git a/cmd/esc/cli/env.go b/cmd/esc/cli/env.go index 192b0c99..847b0ea9 100644 --- a/cmd/esc/cli/env.go +++ b/cmd/esc/cli/env.go @@ -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)) diff --git a/cmd/esc/cli/env_clone.go b/cmd/esc/cli/env_clone.go new file mode 100644 index 00000000..03f9ec9d --- /dev/null +++ b/cmd/esc/cli/env_clone.go @@ -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 [/]/ [/]", + 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 +} diff --git a/cmd/esc/cli/testdata/env-clone-conflict.yaml b/cmd/esc/cli/testdata/env-clone-conflict.yaml new file mode 100644 index 00000000..f0ff2afd --- /dev/null +++ b/cmd/esc/cli/testdata/env-clone-conflict.yaml @@ -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 diff --git a/cmd/esc/cli/testdata/env-clone-not-found.yaml b/cmd/esc/cli/testdata/env-clone-not-found.yaml new file mode 100644 index 00000000..68f9fdca --- /dev/null +++ b/cmd/esc/cli/testdata/env-clone-not-found.yaml @@ -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 diff --git a/cmd/esc/cli/testdata/env-clone-ok.yaml b/cmd/esc/cli/testdata/env-clone-ok.yaml new file mode 100644 index 00000000..4e0554f5 --- /dev/null +++ b/cmd/esc/cli/testdata/env-clone-ok.yaml @@ -0,0 +1,164 @@ +run: | + esc env clone default/src dest + esc env get default/dest + esc env version history default/dest --utc + esc env tag ls default/dest --utc + esc env clone default/src project/env + esc env get project/env + esc env clone default/src project/preserve --history --access --revTags --envTags + esc env version history project/preserve --utc + esc env tag ls project/preserve --utc +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 dest + Environment test-user/default/src cloned into test-user/default/dest. + > esc env get default/dest + # Value + ```json + { + "array": [ + "hello", + "world" + ], + "boolean": true, + "null": null, + "number": 42, + "object": { + "hello": "world" + }, + "open": "[unknown]", + "secret": "[secret]", + "string": "esc" + } + ``` + # Definition + ```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 + + ``` + + > esc env version history default/dest --utc + revision 1 + Author: Test Tester + Date: 1970-01-01 01:00:00 +0000 UTC + + > esc env tag ls default/dest --utc + > esc env clone default/src project/env + Environment test-user/default/src cloned into test-user/project/env. + > esc env get project/env + # Value + ```json + { + "array": [ + "hello", + "world" + ], + "boolean": true, + "null": null, + "number": 42, + "object": { + "hello": "world" + }, + "open": "[unknown]", + "secret": "[secret]", + "string": "esc" + } + ``` + # Definition + ```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 + + ``` + + > esc env clone default/src project/preserve --history --access --revTags --envTags + Environment test-user/default/src cloned into test-user/project/preserve. + > esc env version history project/preserve --utc + revision 4 + Author: Test Tester + Date: 1970-01-01 04:00:00 +0000 UTC + + revision 3 (tag: stable) + Author: Test Tester + Date: 1970-01-01 03:00:00 +0000 UTC + + revision 2 + Author: Test Tester + Date: 1970-01-01 02:00:00 +0000 UTC + + revision 1 + Author: Test Tester + Date: 1970-01-01 01:00:00 +0000 UTC + + > esc env tag ls project/preserve --utc + Name: team + Value: pulumi + Last updated at 2024-07-29 12:30:00 +0000 UTC by pulumipus +stderr: | + > esc env clone default/src dest + > esc env get default/dest + > esc env version history default/dest --utc + > esc env tag ls default/dest --utc + > esc env clone default/src project/env + > esc env get project/env + > esc env clone default/src project/preserve --history --access --revTags --envTags + > esc env version history project/preserve --utc + > esc env tag ls project/preserve --utc