Repository: pulumi/esc
Pull request #441 (2025-02-05 17:51:54)

Separate open environment from rotate
The Rotate API no longer does an open, so this adjusts the esc CLI accordingly


Patch:
diff --git a/cmd/esc/cli/client/client.go b/cmd/esc/cli/client/client.go
index 131af48..ccaf115 100644
--- a/cmd/esc/cli/client/client.go
+++ b/cmd/esc/cli/client/client.go
@@ -157,19 +157,13 @@ type Client interface {
 
 	// RotateEnvironment will rotate credentials in an environment.
 	// If rotationPaths is non-empty, will only rotate credentials at those paths.
-	// It also evaluates the environment projectName/envName in org orgName and returns the ID of the opened
-	// environment. The opened environment will be available for the indicated duration, after which it
-	// will expire.
-	//
-	// If the environment contains errors, the open will fail with diagnostics.
 	RotateEnvironment(
 		ctx context.Context,
 		orgName string,
 		projectName string,
 		envName string,
-		duration time.Duration,
 		rotationPaths []string,
-	) (string, []EnvironmentDiagnostic, error)
+	) ([]EnvironmentDiagnostic, error)
 
 	// CheckYAMLEnvironment checks the given environment YAML for errors within the context of org orgName.
 	//
@@ -637,9 +631,8 @@ func (pc *client) RotateEnvironment(
 	orgName string,
 	projectName string,
 	envName string,
-	duration time.Duration,
 	rotationPaths []string,
-) (string, []EnvironmentDiagnostic, error) {
+) ([]EnvironmentDiagnostic, error) {
 	path := fmt.Sprintf("/api/esc/environments/%v/%v/%v/rotate", orgName, projectName, envName)
 
 	reqObj := struct {
@@ -648,26 +641,21 @@ func (pc *client) RotateEnvironment(
 		Paths: rotationPaths,
 	}
 
-	queryObj := struct {
-		Duration string `url:"duration"`
-	}{
-		Duration: duration.String(),
-	}
 	var resp struct {
 		ID string `json:"id"`
 	}
 	var errResp EnvironmentErrorResponse
-	err := pc.restCallWithOptions(ctx, http.MethodPost, path, queryObj, reqObj, &resp, httpCallOptions{
+	err := pc.restCallWithOptions(ctx, http.MethodPost, path, nil, reqObj, &resp, httpCallOptions{
 		ErrorResponse: &errResp,
 	})
 	if err != nil {
 		var diags *EnvironmentErrorResponse
 		if errors.As(err, &diags) && diags.Code == http.StatusBadRequest && len(diags.Diagnostics) != 0 {
-			return "", diags.Diagnostics, nil
+			return diags.Diagnostics, nil
 		}
-		return "", nil, err
+		return nil, err
 	}
-	return resp.ID, nil, nil
+	return nil, nil
 }
 
 func (pc *client) CheckYAMLEnvironment(
diff --git a/cmd/esc/cli/env_rotate.go b/cmd/esc/cli/env_rotate.go
index 5ea4398..c2e6e5f 100644
--- a/cmd/esc/cli/env_rotate.go
+++ b/cmd/esc/cli/env_rotate.go
@@ -5,27 +5,18 @@ package cli
 import (
 	"context"
 	"fmt"
-	"time"
 
-	"github.com/pulumi/esc"
-	"github.com/pulumi/esc/cmd/esc/cli/client"
 	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
 	"github.com/spf13/cobra"
 )
 
 func newEnvRotateCmd(envcmd *envCommand) *cobra.Command {
-	var duration time.Duration
-	var format string
-
 	cmd := &cobra.Command{
 		Use:   "rotate [<org-name>/][<project-name>/]<environment-name> [path(s) to rotate]",
-		Short: "Rotate secrets and open the environment",
-		Long: "Rotate secrets and open the environment\n" +
-			"\n" +
-			"Optionally accepts any number of Property Paths as additional arguments. If given any paths, will only rotate secrets at those paths.\n" +
+		Short: "Rotate secrets in an environment",
+		Long: "Rotate secrets in an environment\n" +
 			"\n" +
-			"This command opens the environment with the given name. The result is written to\n" +
-			"stdout as JSON.\n",
+			"Optionally accepts any number of Property Paths as additional arguments. If given any paths, will only rotate secrets at those paths.\n",
 		SilenceUsage: true,
 		Hidden:       true,
 		RunE: func(cmd *cobra.Command, args []string) error {
@@ -53,14 +44,7 @@ func newEnvRotateCmd(envcmd *envCommand) *cobra.Command {
 				rotationPaths = append(rotationPaths, arg)
 			}
 
-			switch format {
-			case "detailed", "json", "yaml", "string", "dotenv", "shell":
-				// OK
-			default:
-				return fmt.Errorf("unknown output format %q", format)
-			}
-
-			env, diags, err := envcmd.rotateEnvironment(ctx, ref, duration, rotationPaths)
+			diags, err := envcmd.esc.client.RotateEnvironment(ctx, ref.orgName, ref.projectName, ref.envName, rotationPaths)
 			if err != nil {
 				return err
 			}
@@ -68,33 +52,10 @@ func newEnvRotateCmd(envcmd *envCommand) *cobra.Command {
 				return envcmd.writePropertyEnvironmentDiagnostics(envcmd.esc.stderr, diags)
 			}
 
-			return envcmd.renderValue(envcmd.esc.stdout, env, resource.PropertyPath{}, format, false, true)
+			fmt.Fprintf(envcmd.esc.stdout, "Environment '%s' rotated.\n", args[0])
+			return nil
 		},
 	}
 
-	cmd.Flags().DurationVarP(
-		&duration, "lifetime", "l", 2*time.Hour,
-		"the lifetime of the opened environment in the form HhMm (e.g. 2h, 1h30m, 15m)")
-	cmd.Flags().StringVarP(
-		&format, "format", "f", "json",
-		"the output format to use. May be 'dotenv', 'json', 'yaml', 'detailed', or 'shell'")
-
 	return cmd
 }
-
-func (env *envCommand) rotateEnvironment(
-	ctx context.Context,
-	ref environmentRef,
-	duration time.Duration,
-	rotationPaths []string,
-) (*esc.Environment, []client.EnvironmentDiagnostic, error) {
-	envID, diags, err := env.esc.client.RotateEnvironment(ctx, ref.orgName, ref.projectName, ref.envName, duration, rotationPaths)
-	if err != nil {
-		return nil, nil, err
-	}
-	if len(diags) != 0 {
-		return nil, diags, err
-	}
-	open, err := env.esc.client.GetOpenEnvironmentWithProject(ctx, ref.orgName, ref.projectName, ref.envName, envID)
-	return open, nil, err
-}


Test patch:
diff --git a/cmd/esc/cli/cli_test.go b/cmd/esc/cli/cli_test.go
index 9ca4e6c..730d82c 100644
--- a/cmd/esc/cli/cli_test.go
+++ b/cmd/esc/cli/cli_test.go
@@ -682,10 +682,9 @@ func (c *testPulumiClient) RotateEnvironment(
 	orgName string,
 	projectName string,
 	envName string,
-	duration time.Duration,
 	rotationPaths []string,
-) (string, []client.EnvironmentDiagnostic, error) {
-	return c.OpenEnvironment(ctx, orgName, projectName, envName, "", duration)
+) ([]client.EnvironmentDiagnostic, error) {
+	return []client.EnvironmentDiagnostic{}, nil
 }
 
 func (c *testPulumiClient) CheckYAMLEnvironment(
diff --git a/cmd/esc/cli/client/client_test.go b/cmd/esc/cli/client/client_test.go
index d0fdb0f..5cf7915 100644
--- a/cmd/esc/cli/client/client_test.go
+++ b/cmd/esc/cli/client/client_test.go
@@ -507,24 +507,19 @@ func TestOpenEnvironment(t *testing.T) {
 func TestRotateEnvironment(t *testing.T) {
 	rotationPaths := []string{"a.b", "c"}
 	t.Run("OK", func(t *testing.T) {
-		const expectedID = "open-id"
-		duration := 2 * time.Hour
 		expectedBody := "{\"Paths\":[\"a.b\",\"c\"]}"
 
 		client := newTestClient(t, http.MethodPost, "/api/esc/environments/test-org/test-project/test-env/rotate", func(w http.ResponseWriter, r *http.Request) {
-			assert.Equal(t, duration.String(), r.URL.Query().Get("duration"))
-
 			body, err := io.ReadAll(r.Body)
 			require.NoError(t, err)
 			assert.Equal(t, expectedBody, string(body))
 
-			err = json.NewEncoder(w).Encode(map[string]any{"id": expectedID})
+			err = json.NewEncoder(w).Encode(map[string]any{})
 			require.NoError(t, err)
 		})
 
-		id, diags, err := client.RotateEnvironment(context.Background(), "test-org", "test-project", "test-env", duration, rotationPaths)
+		diags, err := client.RotateEnvironment(context.Background(), "test-org", "test-project", "test-env", rotationPaths)
 		require.NoError(t, err)
-		assert.Equal(t, expectedID, id)
 		assert.Empty(t, diags)
 	})
 
@@ -559,7 +554,7 @@ func TestRotateEnvironment(t *testing.T) {
 			require.NoError(t, err)
 		})
 
-		_, diags, err := client.RotateEnvironment(context.Background(), "test-org", "test-project", "test-env", 2*time.Hour, rotationPaths)
+		diags, err := client.RotateEnvironment(context.Background(), "test-org", "test-project", "test-env", rotationPaths)
 		require.NoError(t, err)
 		assert.Equal(t, expected, diags)
 	})
@@ -575,7 +570,7 @@ func TestRotateEnvironment(t *testing.T) {
 			require.NoError(t, err)
 		})
 
-		_, _, err := client.RotateEnvironment(context.Background(), "test-org", "test-project", "test-env", 2*time.Hour, rotationPaths)
+		_, err := client.RotateEnvironment(context.Background(), "test-org", "test-project", "test-env", rotationPaths)
 		assert.ErrorContains(t, err, "not found")
 	})
 }