PaC: Support Config/getProject/getStack/isDryRun (#3612)

Add support for using `Config`, `getProject()`, `getStack()`, and
`isDryRun()` from Policy Packs.
This commit is contained in:
Justin Van Patten 2019-12-16 22:51:02 +00:00 committed by GitHub
parent 35f16f3e42
commit 10a960ea4b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 132 additions and 42 deletions

View file

@ -10,6 +10,9 @@ CHANGELOG
pulumi login gs://my-bucket
```
- Support for using `Config`, `getProject()`, `getStack()`, and `isDryRun()` from Policy Packs.
[#3612](https://github.com/pulumi/pulumi/pull/3612)
## 1.7.1 (2019-12-13)
- Fix [SxS issue](https://github.com/pulumi/pulumi/issues/3652) introduced in 1.7.0 when assigning

View file

@ -124,7 +124,7 @@ func (pack *cloudPolicyPack) Publish(
return result.FromError(err)
}
analyzer, err := op.PlugCtx.Host.PolicyAnalyzer(tokens.QName(abs), op.PlugCtx.Pwd)
analyzer, err := op.PlugCtx.Host.PolicyAnalyzer(tokens.QName(abs), op.PlugCtx.Pwd, nil /*opts*/)
if err != nil {
return result.FromError(err)
}

View file

@ -16,10 +16,12 @@ package engine
import (
"context"
"path/filepath"
"sync"
"time"
"github.com/blang/semver"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/diag"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/deploy"
@ -184,19 +186,37 @@ func installPlugins(
return allPlugins, defaultProviderVersions, nil
}
func installAndLoadPolicyPlugins(plugctx *plugin.Context, policies []RequiredPolicy) error {
func installAndLoadPolicyPlugins(plugctx *plugin.Context, policies []RequiredPolicy, localPolicyPackPaths []string,
opts *plugin.PolicyAnalyzerOptions) error {
// Install and load required policy packs.
for _, policy := range policies {
policyPath, err := policy.Install(context.Background())
if err != nil {
return err
}
_, err = plugctx.Host.PolicyAnalyzer(tokens.QName(policy.Name()), policyPath)
_, err = plugctx.Host.PolicyAnalyzer(tokens.QName(policy.Name()), policyPath, opts)
if err != nil {
return err
}
}
// Load local policy packs.
for _, path := range localPolicyPackPaths {
abs, err := filepath.Abs(path)
if err != nil {
return err
}
analyzer, err := plugctx.Host.PolicyAnalyzer(tokens.QName(abs), path, opts)
if err != nil {
return err
} else if analyzer == nil {
return errors.Errorf("analyzer could not be loaded from path %q", path)
}
}
return nil
}
@ -226,7 +246,19 @@ func newUpdateSource(
// Step 2: Install and load policy plugins.
//
if err := installAndLoadPolicyPlugins(plugctx, opts.RequiredPolicies); err != nil {
// Decrypt the configuration.
config, err := target.Config.Decrypt(target.Decrypter)
if err != nil {
return nil, err
}
analyzerOpts := plugin.PolicyAnalyzerOptions{
Project: proj.Name.String(),
Stack: target.Name.String(),
Config: config,
DryRun: dryRun,
}
if err := installAndLoadPolicyPlugins(plugctx, opts.RequiredPolicies, opts.LocalPolicyPackPaths,
&analyzerOpts); err != nil {
return nil, err
}

View file

@ -180,7 +180,8 @@ func (host *pluginHost) GetRequiredPlugins(info plugin.ProgInfo,
return nil, nil
}
func (host *pluginHost) PolicyAnalyzer(name tokens.QName, path string) (plugin.Analyzer, error) {
func (host *pluginHost) PolicyAnalyzer(name tokens.QName, path string,
opts *plugin.PolicyAnalyzerOptions) (plugin.Analyzer, error) {
return nil, errors.New("unsupported")
}

View file

@ -54,7 +54,8 @@ func (host *testPluginHost) LogStatus(sev diag.Severity, urn resource.URN, msg s
func (host *testPluginHost) Analyzer(nm tokens.QName) (plugin.Analyzer, error) {
return nil, errors.New("unsupported")
}
func (host *testPluginHost) PolicyAnalyzer(name tokens.QName, path string) (plugin.Analyzer, error) {
func (host *testPluginHost) PolicyAnalyzer(name tokens.QName, path string,
opts *plugin.PolicyAnalyzerOptions) (plugin.Analyzer, error) {
return nil, errors.New("unsupported")
}
func (host *testPluginHost) ListAnalyzers() []plugin.Analyzer {

View file

@ -15,7 +15,6 @@
package deploy
import (
"path/filepath"
"strings"
"github.com/pkg/errors"
@ -314,22 +313,6 @@ func (sg *stepGenerator) generateSteps(event RegisterResourceEvent) ([]Step, res
new.Inputs = inputs
}
// Load all policy packs into the plugin host.
for _, path := range sg.plan.localPolicyPackPaths {
abs, err := filepath.Abs(path)
if err != nil {
return nil, result.FromError(err)
}
var analyzer plugin.Analyzer
analyzer, err = sg.plan.ctx.Host.PolicyAnalyzer(tokens.QName(abs), path)
if err != nil {
return nil, result.FromError(err)
} else if analyzer == nil {
return nil, result.Errorf("analyzer could not be loaded from path %q", path)
}
}
// Send the resource off to any Analyzers before being operated on.
analyzers := sg.plan.ctx.Host.ListAnalyzers()
for _, analyzer := range analyzers {

View file

@ -15,7 +15,9 @@
package plugin
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/blang/semver"
@ -59,7 +61,7 @@ func NewAnalyzer(host Host, ctx *Context, name tokens.QName) (Analyzer, error) {
}
plug, err := newPlugin(ctx, ctx.Pwd, path, fmt.Sprintf("%v (analyzer)", name),
[]string{host.ServerAddr(), ctx.Pwd})
[]string{host.ServerAddr(), ctx.Pwd}, nil /*env*/)
if err != nil {
return nil, err
}
@ -77,7 +79,7 @@ const policyAnalyzerName = "policy"
// NewPolicyAnalyzer boots the nodejs analyzer plugin located at `policyPackpath`
func NewPolicyAnalyzer(
host Host, ctx *Context, name tokens.QName, policyPackPath string) (Analyzer, error) {
host Host, ctx *Context, name tokens.QName, policyPackPath string, opts *PolicyAnalyzerOptions) (Analyzer, error) {
// Load the policy-booting analyzer plugin (i.e., `pulumi-analyzer-${policyAnalyzerName}`).
_, pluginPath, err := workspace.GetPluginPath(
@ -91,6 +93,12 @@ func NewPolicyAnalyzer(
"does not support resource policies", string(name))
}
// Create the environment variables from the options.
env, err := constructEnv(opts)
if err != nil {
return nil, err
}
// The `pulumi-analyzer-policy` plugin is a script that looks for the '@pulumi/pulumi/cmd/run-policy-pack'
// node module and runs it with node. To allow non-node Pulumi programs (e.g. Python, .NET, Go, etc.) to
// run node policy packs, we must set the plugin's pwd to the policy pack directory instead of the Pulumi
@ -98,7 +106,7 @@ func NewPolicyAnalyzer(
// node_modules is used.
pwd := policyPackPath
plug, err := newPlugin(ctx, pwd, pluginPath, fmt.Sprintf("%v (analyzer)", name),
[]string{host.ServerAddr(), "."})
[]string{host.ServerAddr(), "."}, env)
if err != nil {
if err == errRunPolicyModuleNotFound {
return nil, fmt.Errorf("it looks like the policy pack's dependencies are not installed; "+
@ -305,3 +313,49 @@ func convertDiagnostics(protoDiagnostics []*pulumirpc.AnalyzeDiagnostic) ([]Anal
return diagnostics, nil
}
// constructEnv creates a slice of key/value pairs to be used as the environment for the policy pack process. Each entry
// is of the form "key=value". Config is passed as an environment variable (including unecrypted secrets), similar to
// how config is passed to each language runtime plugin.
func constructEnv(opts *PolicyAnalyzerOptions) ([]string, error) {
env := os.Environ()
maybeAppendEnv := func(k, v string) {
if v != "" {
env = append(env, k+"="+v)
}
}
config, err := constructConfig(opts)
if err != nil {
return nil, err
}
maybeAppendEnv("PULUMI_CONFIG", config)
if opts != nil {
maybeAppendEnv("PULUMI_NODEJS_PROJECT", opts.Project)
maybeAppendEnv("PULUMI_NODEJS_STACK", opts.Stack)
maybeAppendEnv("PULUMI_NODEJS_DRY_RUN", fmt.Sprintf("%v", opts.DryRun))
}
return env, nil
}
// constructConfig JSON-serializes the configuration data.
func constructConfig(opts *PolicyAnalyzerOptions) (string, error) {
if opts == nil || opts.Config == nil {
return "", nil
}
config := make(map[string]string)
for k, v := range opts.Config {
config[k.String()] = v
}
configJSON, err := json.Marshal(config)
if err != nil {
return "", err
}
return string(configJSON), nil
}

View file

@ -23,6 +23,7 @@ import (
"github.com/pulumi/pulumi/pkg/diag"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/config"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/cmdutil"
"github.com/pulumi/pulumi/pkg/util/contract"
@ -53,7 +54,7 @@ type Host interface {
// because policy analyzers generally do not need to be "discovered" -- the engine is given a
// set of policies that are required to be run during an update, so they tend to be in a
// well-known place.
PolicyAnalyzer(name tokens.QName, path string) (Analyzer, error)
PolicyAnalyzer(name tokens.QName, path string, opts *PolicyAnalyzerOptions) (Analyzer, error)
// ListAnalyzers returns a list of all analyzer plugins known to the plugin host.
ListAnalyzers() []Analyzer
@ -117,6 +118,14 @@ func NewDefaultHost(ctx *Context, config ConfigSource, runtimeOptions map[string
return host, nil
}
// PolicyAnalyzerOptions includes a bag of options to pass along to a policy analyzer.
type PolicyAnalyzerOptions struct {
Project string
Stack string
Config map[config.Key]string
DryRun bool
}
type pluginLoadRequest struct {
load func() error
result chan<- error
@ -209,7 +218,7 @@ func (host *defaultHost) Analyzer(name tokens.QName) (Analyzer, error) {
return plugin.(Analyzer), nil
}
func (host *defaultHost) PolicyAnalyzer(name tokens.QName, path string) (Analyzer, error) {
func (host *defaultHost) PolicyAnalyzer(name tokens.QName, path string, opts *PolicyAnalyzerOptions) (Analyzer, error) {
plugin, err := host.loadPlugin(func() (interface{}, error) {
// First see if we already loaded this plugin.
if plug, has := host.analyzerPlugins[name]; has {
@ -218,7 +227,7 @@ func (host *defaultHost) PolicyAnalyzer(name tokens.QName, path string) (Analyze
}
// If not, try to load and bind to a plugin.
plug, err := NewPolicyAnalyzer(host, host.ctx, name, path)
plug, err := NewPolicyAnalyzer(host, host.ctx, name, path, opts)
if err == nil && plug != nil {
info, infoerr := plug.GetPluginInfo()
if infoerr != nil {

View file

@ -61,7 +61,7 @@ func NewLanguageRuntime(host Host, ctx *Context, runtime string,
}
args = append(args, host.ServerAddr())
plug, err := newPlugin(ctx, ctx.Pwd, path, runtime, args)
plug, err := newPlugin(ctx, ctx.Pwd, path, runtime, args, nil /*env*/)
if err != nil {
return nil, err
}

View file

@ -43,8 +43,11 @@ type plugin struct {
stdoutDone <-chan bool
stderrDone <-chan bool
Bin string
Args []string
Bin string
Args []string
// Env specifies the environment of the plugin in the same format as go's os/exec.Cmd.Env
// https://golang.org/pkg/os/exec/#Cmd (each entry is of the form "key=value").
Env []string
Conn *grpc.ClientConn
Proc *os.Process
Stdin io.WriteCloser
@ -67,7 +70,7 @@ var nextStreamID int32
// the stack's Pulumi SDK did not have the required modules. i.e. is too old.
var errRunPolicyModuleNotFound = errors.New("pulumi SDK does not support policy as code")
func newPlugin(ctx *Context, pwd, bin, prefix string, args []string) (*plugin, error) {
func newPlugin(ctx *Context, pwd, bin, prefix string, args, env []string) (*plugin, error) {
if logging.V(9) {
var argstr string
for i, arg := range args {
@ -80,7 +83,7 @@ func newPlugin(ctx *Context, pwd, bin, prefix string, args []string) (*plugin, e
}
// Try to execute the binary.
plug, err := execPlugin(bin, args, pwd)
plug, err := execPlugin(bin, args, pwd, env)
if err != nil {
return nil, errors.Wrapf(err, "failed to load plugin %s", bin)
}
@ -231,7 +234,7 @@ func newPlugin(ctx *Context, pwd, bin, prefix string, args []string) (*plugin, e
return plug, nil
}
func execPlugin(bin string, pluginArgs []string, pwd string) (*plugin, error) {
func execPlugin(bin string, pluginArgs []string, pwd string, env []string) (*plugin, error) {
var args []string
// Flow the logging information if set.
if logging.LogFlow {
@ -251,6 +254,9 @@ func execPlugin(bin string, pluginArgs []string, pwd string) (*plugin, error) {
cmd := exec.Command(bin, args...)
cmdutil.RegisterProcessGroup(cmd)
cmd.Dir = pwd
if len(env) > 0 {
cmd.Env = env
}
in, _ := cmd.StdinPipe()
out, _ := cmd.StdoutPipe()
err, _ := cmd.StderrPipe()
@ -261,6 +267,7 @@ func execPlugin(bin string, pluginArgs []string, pwd string) (*plugin, error) {
return &plugin{
Bin: bin,
Args: args,
Env: env,
Proc: cmd.Process,
Stdin: in,
Stdout: out,

View file

@ -77,7 +77,8 @@ func NewProvider(host Host, ctx *Context, pkg tokens.Package, version *semver.Ve
})
}
plug, err := newPlugin(ctx, ctx.Pwd, path, fmt.Sprintf("%v (resource)", pkg), []string{host.ServerAddr()})
plug, err := newPlugin(ctx, ctx.Pwd, path, fmt.Sprintf("%v (resource)", pkg),
[]string{host.ServerAddr()}, nil /*env*/)
if err != nil {
return nil, err
}

View file

@ -232,7 +232,7 @@ func (host *goLanguageHost) constructEnv(req *pulumirpc.RunRequest) ([]string, e
return env, nil
}
// constructConfig json-serializes the configuration data given as part of a RunRequest.
// constructConfig JSON-serializes the configuration data given as part of a RunRequest.
func (host *goLanguageHost) constructConfig(req *pulumirpc.RunRequest) (string, error) {
configMap := req.GetConfig()
if configMap == nil {

View file

@ -571,7 +571,7 @@ func (host *nodeLanguageHost) constructArguments(req *pulumirpc.RunRequest, addr
return args
}
// constructConfig json-serializes the configuration data given as part of
// constructConfig JSON-serializes the configuration data given as part of
// a RunRequest.
func (host *nodeLanguageHost) constructConfig(req *pulumirpc.RunRequest) (string, error) {
configMap := req.GetConfig()

View file

@ -18,8 +18,7 @@ import * as path from "path";
import * as tsnode from "ts-node";
import { ResourceError, RunError } from "../../errors";
import * as log from "../../log";
import { disconnectSync } from "../../runtime/settings";
import { runInPulumiStack } from "../../runtime/stack";
import * as runtime from "../../runtime";
// Keep track if we already logged the information about an unhandled error to the user.. If
// so, we end with a different exit code. The language host recognizes this and will not print
@ -226,7 +225,7 @@ export function run(opts: RunOpts): Promise<Record<string, any> | undefined> | P
// @ts-ignore 'unhandledRejection' will almost always invoke uncaughtHandler with an Error. so
// just suppress the TS strictness here.
process.on("unhandledRejection", uncaughtHandler);
process.on("exit", disconnectSync);
process.on("exit", runtime.disconnectSync);
opts.programStarted();
@ -270,5 +269,5 @@ export function run(opts: RunOpts): Promise<Record<string, any> | undefined> | P
}
};
return opts.runInStack ? runInPulumiStack(runProgram) : runProgram();
return opts.runInStack ? runtime.runInPulumiStack(runProgram) : runProgram();
}