Skip to content

Commit

Permalink
Surface rotation results in eval (#448)
Browse files Browse the repository at this point in the history
  • Loading branch information
seanyeh authored Feb 19, 2025
1 parent 1ce6683 commit bad3c7d
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 15 deletions.
41 changes: 30 additions & 11 deletions eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func RotateEnvironment(
environments EnvironmentLoader,
execContext *esc.ExecContext,
paths []resource.PropertyPath,
) (*esc.Environment, []*Patch, syntax.Diagnostics) {
) (*esc.Environment, *RotationResult, syntax.Diagnostics) {
rotateDocPaths := make(map[string]bool, len(paths))
for _, path := range paths {
rotateDocPaths["values."+path.String()] = true
Expand All @@ -138,7 +138,7 @@ func evalEnvironment(
execContext *esc.ExecContext,
showSecrets bool,
rotatePaths map[string]bool,
) (*esc.Environment, []*Patch, syntax.Diagnostics) {
) (*esc.Environment, *RotationResult, syntax.Diagnostics) {
if env == nil || (len(env.Values.GetEntries()) == 0 && len(env.Imports.GetElements()) == 0) {
return nil, nil, nil
}
Expand All @@ -165,7 +165,7 @@ func evalEnvironment(
Properties: v.export(name).Value.(map[string]esc.Value),
Schema: s,
ExecutionContext: executionContext,
}, ec.patchOutputs, diags
}, &ec.rotationResult, diags
}

type imported struct {
Expand Down Expand Up @@ -194,7 +194,7 @@ type evalContext struct {
base *value // the base value

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
rotationResult RotationResult // result of secret rotations

diags syntax.Diagnostics // diagnostics generated during evaluation
}
Expand Down Expand Up @@ -1103,15 +1103,23 @@ func (e *evalContext) evaluateBuiltinRotate(x *expr, repr *rotateExpr) *value {
}
v.schema = x.schema

docPath := x.repr.syntax().Syntax().Syntax().Path()

inputs, inputsOK := e.evaluateTypedExpr(repr.inputs, repr.inputSchema)
state, stateOK := e.evaluateTypedExpr(repr.state, repr.stateSchema)
if !inputsOK || inputs.containsObservableUnknowns(e.rotating) || !stateOK || state.containsUnknowns() || e.validating || err != nil {
if e.shouldRotate(docPath) {
e.rotationResult = append(e.rotationResult, &Rotation{
Path: docPath,
Status: RotationNotEvaluated,
})
}

v.unknown = true
return v
}

// if rotating, invoke prior to open
docPath := x.repr.syntax().Syntax().Syntax().Path()
if e.shouldRotate(docPath) {
newState, err := rotator.Rotate(
e.ctx,
Expand All @@ -1120,19 +1128,30 @@ func (e *evalContext) evaluateBuiltinRotate(x *expr, repr *rotateExpr) *value {
e.execContext,
)
if err != nil {
diag := ast.ExprError(repr.syntax(), err.Error())
e.rotationResult = append(e.rotationResult, &Rotation{
Path: docPath,
Status: RotationFailed,
Diags: []*syntax.Diagnostic{diag},
})

e.errorf(repr.syntax(), "rotate: %s", err.Error())
v.unknown = true
return v
}

// todo: validate newState conforms to state schema

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

// todo: validate newState conforms to state schema

// pass the updated state to open, as if it were already persisted
state = unexport(newState, x)
}
Expand Down
10 changes: 8 additions & 2 deletions eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,9 +404,11 @@ func TestEval(t *testing.T) {
var rotated *esc.Environment
var patches []*Patch
var rotateDiags syntax.Diagnostics
var rotationResult *RotationResult
if doRotate {
rotated, patches, rotateDiags = RotateEnvironment(context.Background(), environmentName, env, rot128{}, testProviders{},
rotated, rotationResult, rotateDiags = RotateEnvironment(context.Background(), environmentName, env, rot128{}, testProviders{},
&testEnvironments{basePath}, execContext, rotatePaths)
patches = rotationResult.Patches()
}

var checkJSON any
Expand Down Expand Up @@ -475,8 +477,12 @@ func TestEval(t *testing.T) {

var rotated *esc.Environment
if doRotate {
rotated_, patches, diags := RotateEnvironment(context.Background(), environmentName, env, rot128{}, testProviders{},
rotated_, rotationResult, diags := RotateEnvironment(context.Background(), environmentName, env, rot128{}, testProviders{},
&testEnvironments{basePath}, execContext, rotatePaths)
var patches []*Patch
if rotationResult != nil {
patches = rotationResult.Patches()
}

sortEnvironmentDiagnostics(diags)
require.Equal(t, expected.RotateDiags, diags)
Expand Down
5 changes: 3 additions & 2 deletions eval/rotate_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package eval
import (
"context"
"fmt"

"github.com/pulumi/esc"
)

Expand Down Expand Up @@ -46,10 +47,10 @@ values:

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

// writeback state patches
updated, _ := ApplyValuePatches([]byte(def), patches)
updated, _ := ApplyValuePatches([]byte(def), rotationResult.Patches())

// encrypt secret values
encryptedYaml, _ := EncryptSecrets(context.Background(), "<stdin>", updated, rot128{})
Expand Down
47 changes: 47 additions & 0 deletions eval/rotation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2025, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package eval

import "github.com/pulumi/esc/syntax"

type RotationStatus string

const (
RotationSucceeded RotationStatus = "succeeded"
RotationFailed RotationStatus = "failed"
RotationNotEvaluated RotationStatus = "not-evaluated"
)

// A RotationResult stores the result of secret rotations
type RotationResult []*Rotation

// A Rotation stores secret rotation information and diagnostics
type Rotation struct {
Path string // document path where the rotation was defined
Status RotationStatus // status of the rotation
Diags syntax.Diagnostics // diagnostics from the rotation
Patch *Patch // updated rotation state generated during evaluation, to be written back to the environment definition
}

func (r *RotationResult) Patches() []*Patch {
var patches []*Patch
for _, rotation := range *r {
if rotation.Patch != nil {
patches = append(patches, rotation.Patch)
}
}

return patches
}

0 comments on commit bad3c7d

Please sign in to comment.