PaC: Support Config/getProject/getStack/isDryRun (#3612)
Add support for using `Config`, `getProject()`, `getStack()`, and `isDryRun()` from Policy Packs.
This commit is contained in:
parent
35f16f3e42
commit
10a960ea4b
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue