Skip to content

Commit

Permalink
support rotating by path (#437)
Browse files Browse the repository at this point in the history
Extends #432 with support for specifying a subset of paths to rotate.

Closes pulumi/pulumi-service#24986
  • Loading branch information
nyobe committed Feb 5, 2025
1 parent 65c34a5 commit 37bede5
Show file tree
Hide file tree
Showing 8 changed files with 6,477 additions and 28 deletions.
61 changes: 42 additions & 19 deletions eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/pulumi/esc/schema"
"github.com/pulumi/esc/syntax"
"github.com/pulumi/esc/syntax/encoding"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"golang.org/x/exp/maps"
)

Expand Down Expand Up @@ -85,7 +86,7 @@ func EvalEnvironment(
environments EnvironmentLoader,
execContext *esc.ExecContext,
) (*esc.Environment, syntax.Diagnostics) {
opened, _, diags := evalEnvironment(ctx, false, false, name, env, decrypter, providers, environments, execContext, true)
opened, _, diags := evalEnvironment(ctx, false, false, name, env, decrypter, providers, environments, execContext, true, nil)
return opened, diags
}

Expand All @@ -101,7 +102,7 @@ func CheckEnvironment(
execContext *esc.ExecContext,
showSecrets bool,
) (*esc.Environment, syntax.Diagnostics) {
checked, _, diags := evalEnvironment(ctx, true, false, name, env, decrypter, providers, environments, execContext, showSecrets)
checked, _, diags := evalEnvironment(ctx, true, false, name, env, decrypter, providers, environments, execContext, showSecrets, nil)
return checked, diags
}

Expand All @@ -115,8 +116,13 @@ func RotateEnvironment(
providers ProviderLoader,
environments EnvironmentLoader,
execContext *esc.ExecContext,
paths []resource.PropertyPath,
) (*esc.Environment, []*Patch, syntax.Diagnostics) {
return evalEnvironment(ctx, false, true, name, env, decrypter, providers, environments, execContext, true)
rotateDocPaths := make(map[string]bool, len(paths))
for _, path := range paths {
rotateDocPaths["values."+path.String()] = true
}
return evalEnvironment(ctx, false, true, name, env, decrypter, providers, environments, execContext, true, rotateDocPaths)
}

// evalEnvironment evaluates an environment and exports the result of evaluation.
Expand All @@ -131,12 +137,13 @@ func evalEnvironment(
envs EnvironmentLoader,
execContext *esc.ExecContext,
showSecrets bool,
rotatePaths map[string]bool,
) (*esc.Environment, []*Patch, syntax.Diagnostics) {
if env == nil || (len(env.Values.GetEntries()) == 0 && len(env.Imports.GetElements()) == 0) {
return nil, nil, nil
}

ec := newEvalContext(ctx, validating, rotating, name, env, decrypter, providers, envs, map[string]*imported{}, execContext, showSecrets)
ec := newEvalContext(ctx, validating, rotating, name, env, decrypter, providers, envs, map[string]*imported{}, execContext, showSecrets, rotatePaths)
v, diags := ec.evaluate()

s := schema.Never().Schema()
Expand Down Expand Up @@ -185,7 +192,8 @@ type evalContext struct {
root *expr // the root expression
base *value // the base value

patchOutputs []*Patch // updated rotation state generated during evaluation, to be written back to the environment definition
rotateDocPaths map[string]bool // the subset of document paths to invoke rotation for when rotating. if empty, all rotators will be invoked.
patchOutputs []*Patch // updated rotation state generated during evaluation, to be written back to the environment definition

diags syntax.Diagnostics // diagnostics generated during evaluation
}
Expand All @@ -202,19 +210,21 @@ func newEvalContext(
imports map[string]*imported,
execContext *esc.ExecContext,
showSecrets bool,
rotateDocPaths map[string]bool,
) *evalContext {
return &evalContext{
ctx: ctx,
validating: validating,
rotating: rotating,
showSecrets: showSecrets,
name: name,
env: env,
decrypter: decrypter,
providers: providers,
environments: environments,
imports: imports,
execContext: execContext.CopyForEnv(name),
ctx: ctx,
validating: validating,
rotating: rotating,
showSecrets: showSecrets,
name: name,
env: env,
decrypter: decrypter,
providers: providers,
environments: environments,
imports: imports,
execContext: execContext.CopyForEnv(name),
rotateDocPaths: rotateDocPaths,
}
}

Expand Down Expand Up @@ -504,7 +514,7 @@ func (e *evalContext) evaluateImport(myImports map[string]*value, decl *ast.Impo
}

// we only want to rotate the root environment, so set rotating flag to false when evaluating imports
imp := newEvalContext(e.ctx, e.validating, false, name, env, dec, e.providers, e.environments, e.imports, e.execContext, e.showSecrets)
imp := newEvalContext(e.ctx, e.validating, false, name, env, dec, e.providers, e.environments, e.imports, e.execContext, e.showSecrets, nil)
v, diags := imp.evaluate()
e.diags.Extend(diags...)

Expand Down Expand Up @@ -1021,7 +1031,8 @@ func (e *evalContext) evaluateBuiltinRotate(x *expr, repr *rotateExpr) *value {
}

// if rotating, invoke prior to open
if e.rotating {
docPath := x.repr.syntax().Syntax().Syntax().Path()
if e.shouldRotate(docPath) {
newState, err := rotator.Rotate(
e.ctx,
inputs.export("").Value.(map[string]esc.Value),
Expand All @@ -1038,7 +1049,7 @@ func (e *evalContext) evaluateBuiltinRotate(x *expr, repr *rotateExpr) *value {

e.patchOutputs = append(e.patchOutputs, &Patch{
// rotation output is written back to the fn's `state` input
DocPath: util.JoinKey(x.path, repr.node.Name().GetValue()) + ".state",
DocPath: util.JoinKey(docPath, repr.node.Name().GetValue()) + ".state",
Replacement: newState,
})

Expand All @@ -1060,6 +1071,18 @@ func (e *evalContext) evaluateBuiltinRotate(x *expr, repr *rotateExpr) *value {
return unexport(output, x)
}

// shouldRotate returns true if the rotator at this path should be invoked.
func (e *evalContext) shouldRotate(docPath string) bool {
if !e.rotating {
return false
}
if len(e.rotateDocPaths) == 0 {
// we're rotating the full environment
return true
}
return e.rotateDocPaths[docPath]
}

// evaluateBuiltinJoin evaluates a call to the fn::join builtin.
func (e *evalContext) evaluateBuiltinJoin(x *expr, repr *joinExpr) *value {
v := &value{def: x, schema: x.schema}
Expand Down
19 changes: 14 additions & 5 deletions eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/pulumi/esc"
"github.com/pulumi/esc/schema"
"github.com/pulumi/esc/syntax"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -325,9 +326,10 @@ func normalize[T any](t *testing.T, v T) T {

func TestEval(t *testing.T) {
type testOverrides struct {
ShowSecrets bool `json:"showSecrets,omitempty"`
RootEnvironment string `json:"rootEnvironment,omitempty"`
Rotate bool `json:"rotate,omitempty"`
ShowSecrets bool `json:"showSecrets,omitempty"`
RootEnvironment string `json:"rootEnvironment,omitempty"`
Rotate bool `json:"rotate,omitempty"`
RotatePaths []string `json:"rotatePaths,omitempty"`
}

type expectedData struct {
Expand Down Expand Up @@ -385,7 +387,14 @@ func TestEval(t *testing.T) {
environmentName = overrides.RootEnvironment
}
showSecrets := overrides.ShowSecrets

doRotate := overrides.Rotate
rotatePaths := make([]resource.PropertyPath, len(overrides.RotatePaths))
for i := range overrides.RotatePaths {
propertyPath, err := resource.ParsePropertyPath(overrides.RotatePaths[i])
require.NoError(t, err)
rotatePaths[i] = propertyPath
}

if accept() {
env, loadDiags, err := LoadYAMLBytes(environmentName, envBytes)
Expand All @@ -405,7 +414,7 @@ func TestEval(t *testing.T) {
var rotateDiags syntax.Diagnostics
if doRotate {
rotated, patches, rotateDiags = RotateEnvironment(context.Background(), environmentName, env, rot128{}, testProviders{},
&testEnvironments{basePath}, execContext)
&testEnvironments{basePath}, execContext, rotatePaths)
}

var checkJSON any
Expand Down Expand Up @@ -475,7 +484,7 @@ func TestEval(t *testing.T) {
var rotated *esc.Environment
if doRotate {
rotated_, patches, diags := RotateEnvironment(context.Background(), environmentName, env, rot128{}, testProviders{},
&testEnvironments{basePath}, execContext)
&testEnvironments{basePath}, execContext, rotatePaths)

sortEnvironmentDiagnostics(diags)
require.Equal(t, expected.RotateDiags, diags)
Expand Down
2 changes: 1 addition & 1 deletion eval/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func ApplyValuePatches(source []byte, patches []*Patch) ([]byte, error) {
}

for _, patch := range patches {
path, err := resource.ParsePropertyPath("values." + patch.DocPath)
path, err := resource.ParsePropertyPath(patch.DocPath)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion eval/rotate_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ values:

// rotate the environment
execContext, _ := esc.NewExecContext(nil)
_, patches, _ := RotateEnvironment(context.Background(), "<stdin>", env, rot128{}, testProviders{}, &testEnvironments{}, execContext)
_, patches, _ := RotateEnvironment(context.Background(), "<stdin>", env, rot128{}, testProviders{}, &testEnvironments{}, execContext, nil)

// writeback state patches
updated, _ := ApplyValuePatches([]byte(def), patches)
Expand Down
34 changes: 34 additions & 0 deletions eval/testdata/eval/rotate-paths/env.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
values:
examples:
not-rotated:
fn::rotate::swap:
inputs: { }
state:
a: a
b: b

subscript-path:
- fn::rotate::swap:
inputs: {}
state:
a: a
b: b

deeply:
nested:
- quoted "property":
path:
fn::rotate::swap:
inputs: {}
state:
a: a
b: b

embedded-in-another-fn:
fn::open::test:
some-input:
fn::rotate::swap:
inputs: {}
state:
a: a
b: b
Loading

0 comments on commit 37bede5

Please sign in to comment.