Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

provider rotation #432

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions analysis/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ func (testProviders) LoadProvider(ctx context.Context, name string) (esc.Provide
return nil, fmt.Errorf("unknown provider %q", name)
}

type testRotators struct{}

func (testRotators) LoadRotator(ctx context.Context, name string) (esc.Rotator, error) {
return nil, fmt.Errorf("unknown provider %q", name)
}

type testEnvironments struct{}

func (testEnvironments) LoadEnvironment(ctx context.Context, name string) ([]byte, eval.Decrypter, error) {
Expand Down
4 changes: 2 additions & 2 deletions analysis/describe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestDescribe(t *testing.T) {
execContext, err := esc.NewExecContext(make(map[string]esc.Value))
require.NoError(t, err)

env, diags := eval.CheckEnvironment(context.Background(), "def", syntax, nil, testProviders{}, testEnvironments{}, execContext, false)
env, diags := eval.CheckEnvironment(context.Background(), "def", syntax, nil, testProviders{}, testRotators{}, testEnvironments{}, execContext, false)
require.Empty(t, diags)

analysis := New(*env, map[string]*schema.Schema{"test": testProviderSchema})
Expand Down Expand Up @@ -108,7 +108,7 @@ func TestDescribeOpen(t *testing.T) {
execContext, err := esc.NewExecContext(make(map[string]esc.Value))
require.NoError(t, err)

env, diags := eval.CheckEnvironment(context.Background(), "def", syntax, nil, testProviders{}, testEnvironments{}, execContext, false)
env, diags := eval.CheckEnvironment(context.Background(), "def", syntax, nil, testProviders{}, testRotators{}, testEnvironments{}, execContext, false)
require.Empty(t, diags)

analysis := New(*env, map[string]*schema.Schema{"test": testProviderSchema})
Expand Down
2 changes: 1 addition & 1 deletion analysis/traversal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestExpressionAt(t *testing.T) {
execContext, err := esc.NewExecContext(make(map[string]esc.Value))
require.NoError(t, err)

env, diags := eval.CheckEnvironment(context.Background(), "def", syntax, nil, testProviders{}, testEnvironments{}, execContext, false)
env, diags := eval.CheckEnvironment(context.Background(), "def", syntax, nil, testProviders{}, testRotators{}, testEnvironments{}, execContext, false)
require.Empty(t, diags)

analysis := New(*env, map[string]*schema.Schema{"test": testProviderSchema})
Expand Down
114 changes: 114 additions & 0 deletions ast/expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"encoding/json"
"fmt"
"reflect"
"slices"
"strings"

"github.com/hashicorp/hcl/v2"
Expand Down Expand Up @@ -434,6 +435,41 @@ func Open(provider string, inputs *ObjectExpr) *OpenExpr {
}
}

// RotateExpr is a type of OpenExpr that supports a rotate operation.
type RotateExpr struct {
builtinNode

Provider *StringExpr
Inputs Expr
State *ObjectExpr
}

func RotateSyntax(node *syntax.ObjectNode, name *StringExpr, args Expr, provider *StringExpr, inputs Expr, state *ObjectExpr) *RotateExpr {
return &RotateExpr{
builtinNode: builtin(node, name, args),
Provider: provider,
Inputs: inputs,
State: state,
}
}

func Rotate(provider string, inputs, state *ObjectExpr) *RotateExpr {
name, providerX := String("fn::rotate"), String(provider)

entries := []ObjectProperty{
{Key: String("provider"), Value: providerX},
{Key: String("inputs"), Value: inputs},
{Key: String("state"), Value: state},
}

return &RotateExpr{
builtinNode: builtin(nil, name, Object(entries...)),
Provider: providerX,
Inputs: inputs,
State: state,
}
}

// ToJSON returns the underlying structure as a json string.
type ToJSONExpr struct {
builtinNode
Expand Down Expand Up @@ -607,6 +643,8 @@ func tryParseFunction(node *syntax.ObjectNode) (Expr, syntax.Diagnostics, bool)
parse = parseJoin
case "fn::open":
parse = parseOpen
case "fn::rotate":
parse = parseRotate
case "fn::secret":
parse = parseSecret
case "fn::toBase64":
Expand All @@ -620,6 +658,10 @@ func tryParseFunction(node *syntax.ObjectNode) (Expr, syntax.Diagnostics, bool)
parse = parseShortOpen
break
}
if strings.HasPrefix(kvp.Key.Value(), "fn::rotate::") {
parse = parseShortRotate
break
}

if strings.HasPrefix(strings.ToLower(kvp.Key.Value()), "fn::") {
diags = append(diags, syntax.Error(kvp.Key.Syntax().Range(),
Expand Down Expand Up @@ -696,6 +738,78 @@ func parseShortOpen(node *syntax.ObjectNode, name *StringExpr, args Expr) (Expr,
return OpenSyntax(node, name, args, provider, args), nil
}

func parseRotate(node *syntax.ObjectNode, name *StringExpr, args Expr) (Expr, syntax.Diagnostics) {
obj, ok := args.(*ObjectExpr)
if !ok {
diags := syntax.Diagnostics{ExprError(args, "the argument to fn::rotate must be an object containing 'provider', 'inputs' and 'state'")}
return RotateSyntax(node, name, args, nil, nil, nil), diags
}

var providerExpr, inputs, stateExpr Expr
var diags syntax.Diagnostics

for i := 0; i < len(obj.Entries); i++ {
kvp := obj.Entries[i]
key := kvp.Key
switch key.GetValue() {
case "provider":
providerExpr = kvp.Value
case "inputs":
inputs = kvp.Value
case "state":
stateExpr = kvp.Value
}
}

provider, ok := providerExpr.(*StringExpr)
if !ok {
if providerExpr == nil {
diags.Extend(ExprError(obj, "missing provider name ('provider')"))
} else {
diags.Extend(ExprError(providerExpr, "provider name must be a string literal"))
}
}

if inputs == nil {
diags.Extend(ExprError(obj, "missing provider inputs ('inputs')"))
}

state, ok := stateExpr.(*ObjectExpr)
if !ok && state != nil {
diags.Extend(ExprError(stateExpr, "rotation state must be an object literal"))
}

return RotateSyntax(node, name, obj, provider, inputs, state), diags
}

func parseShortRotate(node *syntax.ObjectNode, name *StringExpr, args Expr) (Expr, syntax.Diagnostics) {
kvp := node.Index(0)
provider := StringSyntaxValue(name.Syntax().(*syntax.StringNode), strings.TrimPrefix(kvp.Key.Value(), "fn::rotate::"))

inputs, ok := args.(*ObjectExpr)
if !ok {
diags := syntax.Diagnostics{ExprError(args, "provider inputs must be an object")}
return RotateSyntax(node, name, args, nil, nil, nil), diags
}

// hoist 'state' key out of inputs
var stateExpr Expr
if i := slices.IndexFunc(inputs.Entries, func(kvp ObjectProperty) bool {
return kvp.Key.GetValue() == "state"
}); i != -1 {
stateExpr = inputs.Entries[i].Value
inputs.Entries = slices.Delete(inputs.Entries, i, i+1)
}

state, ok := stateExpr.(*ObjectExpr)
if !ok && state != nil {
diags := syntax.Diagnostics{ExprError(stateExpr, "rotation state must be an object literal")}
return RotateSyntax(node, name, args, nil, nil, nil), diags
}

return RotateSyntax(node, name, args, provider, inputs, state), nil
}

func parseJoin(node *syntax.ObjectNode, name *StringExpr, args Expr) (Expr, syntax.Diagnostics) {
list, ok := args.(*ArrayExpr)
if !ok || len(list.Elements) != 2 {
Expand Down
12 changes: 10 additions & 2 deletions cmd/esc/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,12 @@ func (testProviders) LoadProvider(ctx context.Context, name string) (esc.Provide
return nil, fmt.Errorf("unknown provider %q", name)
}

type testRotators struct{}

func (testRotators) LoadRotator(ctx context.Context, name string) (esc.Rotator, error) {
return nil, fmt.Errorf("unknown rotator %q", name)
}

type rot128 struct{}

func (rot128) Encrypt(_ context.Context, plaintext []byte) ([]byte, error) {
Expand Down Expand Up @@ -395,6 +401,7 @@ func (c *testPulumiClient) checkEnvironment(ctx context.Context, orgName, envNam
}

providers := &testProviders{}
rotators := &testRotators{}
envLoader := &testEnvironments{orgName: orgName, environments: c.environments}

execContext, err := esc.NewExecContext(make(map[string]esc.Value))
Expand All @@ -407,7 +414,7 @@ func (c *testPulumiClient) checkEnvironment(ctx context.Context, orgName, envNam
showSecrets = opts[0].ShowSecrets
}

checked, checkDiags := eval.CheckEnvironment(ctx, envName, environment, rot128{}, providers, envLoader, execContext, showSecrets)
checked, checkDiags := eval.CheckEnvironment(ctx, envName, environment, rot128{}, providers, rotators, envLoader, execContext, showSecrets)
diags.Extend(checkDiags...)
return checked, mapDiags(diags), nil
}
Expand All @@ -427,14 +434,15 @@ func (c *testPulumiClient) openEnvironment(ctx context.Context, orgName, name st
}

providers := &testProviders{}
rotators := &testRotators{}
envLoader := &testEnvironments{orgName: orgName, environments: c.environments}

execContext, err := esc.NewExecContext(make(map[string]esc.Value))
if err != nil {
return "", nil, fmt.Errorf("initializing the ESC exec context: %w", err)
}

openEnv, evalDiags := eval.EvalEnvironment(ctx, name, decl, rot128{}, providers, envLoader, execContext)
openEnv, evalDiags := eval.EvalEnvironment(ctx, name, decl, rot128{}, providers, rotators, envLoader, execContext)
diags.Extend(evalDiags...)

if diags.HasErrors() {
Expand Down
Loading
Loading