github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/backend/httpstate/policypack.go (about)

     1  package httpstate
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"os"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"github.com/pulumi/pulumi/pkg/v3/backend"
    16  	"github.com/pulumi/pulumi/pkg/v3/backend/httpstate/client"
    17  	"github.com/pulumi/pulumi/pkg/v3/engine"
    18  	resourceanalyzer "github.com/pulumi/pulumi/pkg/v3/resource/analyzer"
    19  	"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
    20  	"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
    21  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/archive"
    22  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
    23  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
    24  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/result"
    25  	"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
    26  	"github.com/pulumi/pulumi/sdk/v3/nodejs/npm"
    27  	"github.com/pulumi/pulumi/sdk/v3/python"
    28  )
    29  
    30  type cloudRequiredPolicy struct {
    31  	apitype.RequiredPolicy
    32  	client  *client.Client
    33  	orgName string
    34  }
    35  
    36  var _ engine.RequiredPolicy = (*cloudRequiredPolicy)(nil)
    37  
    38  func newCloudRequiredPolicy(client *client.Client,
    39  	policy apitype.RequiredPolicy, orgName string) *cloudRequiredPolicy {
    40  
    41  	return &cloudRequiredPolicy{
    42  		client:         client,
    43  		RequiredPolicy: policy,
    44  		orgName:        orgName,
    45  	}
    46  }
    47  
    48  func (rp *cloudRequiredPolicy) Name() string    { return rp.RequiredPolicy.Name }
    49  func (rp *cloudRequiredPolicy) Version() string { return strconv.Itoa(rp.RequiredPolicy.Version) }
    50  func (rp *cloudRequiredPolicy) OrgName() string { return rp.orgName }
    51  
    52  func (rp *cloudRequiredPolicy) Install(ctx context.Context) (string, error) {
    53  	policy := rp.RequiredPolicy
    54  
    55  	// If version tag is empty, we use the version tag. This is to support older version of
    56  	// pulumi/policy that do not have a version tag.
    57  	version := policy.VersionTag
    58  	if version == "" {
    59  		version = strconv.Itoa(policy.Version)
    60  	}
    61  	policyPackPath, installed, err := workspace.GetPolicyPath(rp.OrgName(),
    62  		strings.Replace(policy.Name, tokens.QNameDelimiter, "_", -1), version)
    63  	if err != nil {
    64  		// Failed to get a sensible PolicyPack path.
    65  		return "", err
    66  	} else if installed {
    67  		// We've already downloaded and installed the PolicyPack. Return.
    68  		return policyPackPath, nil
    69  	}
    70  
    71  	fmt.Printf("Installing policy pack %s %s...\n", policy.Name, version)
    72  
    73  	// PolicyPack has not been downloaded and installed. Do this now.
    74  	policyPackTarball, err := rp.client.DownloadPolicyPack(ctx, policy.PackLocation)
    75  	if err != nil {
    76  		return "", err
    77  	}
    78  
    79  	return policyPackPath, installRequiredPolicy(ctx, policyPackPath, policyPackTarball)
    80  }
    81  
    82  func (rp *cloudRequiredPolicy) Config() map[string]*json.RawMessage { return rp.RequiredPolicy.Config }
    83  
    84  func newCloudBackendPolicyPackReference(
    85  	cloudConsoleURL, orgName string, name tokens.QName) *cloudBackendPolicyPackReference {
    86  
    87  	return &cloudBackendPolicyPackReference{
    88  		orgName:         orgName,
    89  		name:            name,
    90  		cloudConsoleURL: cloudConsoleURL,
    91  	}
    92  }
    93  
    94  // cloudBackendPolicyPackReference is a reference to a PolicyPack implemented by the Pulumi service.
    95  type cloudBackendPolicyPackReference struct {
    96  	// name of the PolicyPack.
    97  	name tokens.QName
    98  	// orgName that administrates the PolicyPack.
    99  	orgName string
   100  
   101  	// versionTag of the Policy Pack. This is typically the version specified in
   102  	// a package.json, setup.py, or similar file.
   103  	versionTag string
   104  
   105  	// cloudConsoleURL is the root URL of where the Policy Pack can be found in the console. The
   106  	// version must be appended to the returned URL.
   107  	cloudConsoleURL string
   108  }
   109  
   110  var _ backend.PolicyPackReference = (*cloudBackendPolicyPackReference)(nil)
   111  
   112  func (pr *cloudBackendPolicyPackReference) String() string {
   113  	return fmt.Sprintf("%s/%s", pr.orgName, pr.name)
   114  }
   115  
   116  func (pr *cloudBackendPolicyPackReference) OrgName() string {
   117  	return pr.orgName
   118  }
   119  
   120  func (pr *cloudBackendPolicyPackReference) Name() tokens.QName {
   121  	return pr.name
   122  }
   123  
   124  func (pr *cloudBackendPolicyPackReference) CloudConsoleURL() string {
   125  	return fmt.Sprintf("%s/%s/policypacks/%s", pr.cloudConsoleURL, pr.orgName, pr.Name())
   126  }
   127  
   128  // cloudPolicyPack is a the Pulumi service implementation of the PolicyPack interface.
   129  type cloudPolicyPack struct {
   130  	// ref uniquely identifies the PolicyPack in the Pulumi service.
   131  	ref *cloudBackendPolicyPackReference
   132  	// b is a pointer to the backend that this PolicyPack belongs to.
   133  	b *cloudBackend
   134  	// cl is the client used to interact with the backend.
   135  	cl *client.Client
   136  }
   137  
   138  var _ backend.PolicyPack = (*cloudPolicyPack)(nil)
   139  
   140  func (pack *cloudPolicyPack) Ref() backend.PolicyPackReference {
   141  	return pack.ref
   142  }
   143  
   144  func (pack *cloudPolicyPack) Backend() backend.Backend {
   145  	return pack.b
   146  }
   147  
   148  func (pack *cloudPolicyPack) Publish(
   149  	ctx context.Context, op backend.PublishOperation) result.Result {
   150  
   151  	//
   152  	// Get PolicyPack metadata from the plugin.
   153  	//
   154  
   155  	fmt.Println("Obtaining policy metadata from policy plugin")
   156  
   157  	abs, err := filepath.Abs(op.PlugCtx.Pwd)
   158  	if err != nil {
   159  		return result.FromError(err)
   160  	}
   161  
   162  	analyzer, err := op.PlugCtx.Host.PolicyAnalyzer(tokens.QName(abs), op.PlugCtx.Pwd, nil /*opts*/)
   163  	if err != nil {
   164  		return result.FromError(err)
   165  	}
   166  
   167  	analyzerInfo, err := analyzer.GetAnalyzerInfo()
   168  	if err != nil {
   169  		return result.FromError(err)
   170  	}
   171  
   172  	// Update the name and version tag from the metadata.
   173  	pack.ref.name = tokens.QName(analyzerInfo.Name)
   174  	pack.ref.versionTag = analyzerInfo.Version
   175  
   176  	fmt.Println("Compressing policy pack")
   177  
   178  	var packTarball []byte
   179  
   180  	// TODO[pulumi/pulumi#1334]: move to the language plugins so we don't have to hard code here.
   181  	runtime := op.PolicyPack.Runtime.Name()
   182  	if strings.EqualFold(runtime, "nodejs") {
   183  		packTarball, err = npm.Pack(ctx, op.PlugCtx.Pwd, os.Stderr)
   184  		if err != nil {
   185  			return result.FromError(fmt.Errorf("could not publish policies because of error running npm pack: %w", err))
   186  		}
   187  	} else {
   188  		// npm pack puts all the files in a "package" subdirectory inside the .tgz it produces, so we'll do
   189  		// the same for other runtimes. That way, after unpacking, we can look for the PulumiPolicy.yaml inside the
   190  		// package directory to determine the runtime of the policy pack.
   191  		packTarball, err = archive.TGZ(op.PlugCtx.Pwd, "package", true /*useDefaultExcludes*/)
   192  		if err != nil {
   193  			return result.FromError(fmt.Errorf("could not publish policies because of error creating the .tgz: %w", err))
   194  		}
   195  	}
   196  
   197  	//
   198  	// Publish.
   199  	//
   200  
   201  	fmt.Println("Uploading policy pack to Pulumi service")
   202  
   203  	publishedVersion, err := pack.cl.PublishPolicyPack(ctx, pack.ref.orgName, analyzerInfo, bytes.NewReader(packTarball))
   204  	if err != nil {
   205  		return result.FromError(err)
   206  	}
   207  
   208  	fmt.Printf("\nPermalink: %s/%s\n", pack.ref.CloudConsoleURL(), publishedVersion)
   209  
   210  	return nil
   211  }
   212  
   213  func (pack *cloudPolicyPack) Enable(ctx context.Context, policyGroup string, op backend.PolicyPackOperation) error {
   214  	if op.VersionTag == nil {
   215  		return pack.cl.ApplyPolicyPack(ctx, pack.ref.orgName, policyGroup, string(pack.ref.name),
   216  			"" /* versionTag */, op.Config)
   217  	}
   218  	return pack.cl.ApplyPolicyPack(ctx, pack.ref.orgName, policyGroup, string(pack.ref.name), *op.VersionTag, op.Config)
   219  }
   220  
   221  func (pack *cloudPolicyPack) Validate(ctx context.Context, op backend.PolicyPackOperation) error {
   222  	schema, err := pack.cl.GetPolicyPackSchema(ctx, pack.ref.orgName, string(pack.ref.name), *op.VersionTag)
   223  	if err != nil {
   224  		return err
   225  	}
   226  	err = resourceanalyzer.ValidatePolicyPackConfig(schema.ConfigSchema, op.Config)
   227  	if err != nil {
   228  		return err
   229  	}
   230  	return nil
   231  }
   232  
   233  func (pack *cloudPolicyPack) Disable(ctx context.Context, policyGroup string, op backend.PolicyPackOperation) error {
   234  	if op.VersionTag == nil {
   235  		return pack.cl.DisablePolicyPack(ctx, pack.ref.orgName, policyGroup, string(pack.ref.name), "" /* versionTag */)
   236  	}
   237  	return pack.cl.DisablePolicyPack(ctx, pack.ref.orgName, policyGroup, string(pack.ref.name), *op.VersionTag)
   238  }
   239  
   240  func (pack *cloudPolicyPack) Remove(ctx context.Context, op backend.PolicyPackOperation) error {
   241  	if op.VersionTag == nil {
   242  		return pack.cl.RemovePolicyPack(ctx, pack.ref.orgName, string(pack.ref.name))
   243  	}
   244  	return pack.cl.RemovePolicyPackByVersion(ctx, pack.ref.orgName, string(pack.ref.name), *op.VersionTag)
   245  }
   246  
   247  const packageDir = "package"
   248  
   249  func installRequiredPolicy(ctx context.Context, finalDir string, tgz io.ReadCloser) error {
   250  	// If part of the directory tree is missing, ioutil.TempDir will return an error, so make sure
   251  	// the path we're going to create the temporary folder in actually exists.
   252  	if err := os.MkdirAll(filepath.Dir(finalDir), 0700); err != nil {
   253  		return fmt.Errorf("creating plugin root: %w", err)
   254  	}
   255  
   256  	tempDir, err := ioutil.TempDir(filepath.Dir(finalDir), fmt.Sprintf("%s.tmp", filepath.Base(finalDir)))
   257  	if err != nil {
   258  		return fmt.Errorf("creating plugin directory %s: %w", tempDir, err)
   259  	}
   260  
   261  	// The policy pack files are actually in a directory called `package`.
   262  	tempPackageDir := filepath.Join(tempDir, packageDir)
   263  	if err := os.MkdirAll(tempPackageDir, 0700); err != nil {
   264  		return fmt.Errorf("creating plugin root: %w", err)
   265  	}
   266  
   267  	// If we early out of this function, try to remove the temp folder we created.
   268  	defer func() {
   269  		contract.IgnoreError(os.RemoveAll(tempDir))
   270  	}()
   271  
   272  	// Uncompress the policy pack.
   273  	err = archive.ExtractTGZ(tgz, tempDir)
   274  	if err != nil {
   275  		return fmt.Errorf("failed to extract tarball: %w", err)
   276  	}
   277  
   278  	logging.V(7).Infof("Unpacking policy pack %q %q\n", tempDir, finalDir)
   279  
   280  	// If two calls to `plugin install` for the same plugin are racing, the second one will be
   281  	// unable to rename the directory. That's OK, just ignore the error. The temp directory created
   282  	// as part of the install will be cleaned up when we exit by the defer above.
   283  	if err := os.Rename(tempPackageDir, finalDir); err != nil && !os.IsExist(err) {
   284  		return fmt.Errorf("moving plugin: %w", err)
   285  	}
   286  
   287  	projPath := filepath.Join(finalDir, "PulumiPolicy.yaml")
   288  	proj, err := workspace.LoadPolicyPack(projPath)
   289  	if err != nil {
   290  		return fmt.Errorf("failed to load policy project at %s: %w", finalDir, err)
   291  	}
   292  
   293  	// TODO[pulumi/pulumi#1334]: move to the language plugins so we don't have to hard code here.
   294  	if strings.EqualFold(proj.Runtime.Name(), "nodejs") {
   295  		if err := completeNodeJSInstall(ctx, finalDir); err != nil {
   296  			return err
   297  		}
   298  	} else if strings.EqualFold(proj.Runtime.Name(), "python") {
   299  		if err := completePythonInstall(ctx, finalDir, projPath, proj); err != nil {
   300  			return err
   301  		}
   302  	}
   303  
   304  	fmt.Println("Finished installing policy pack")
   305  	fmt.Println()
   306  
   307  	return nil
   308  }
   309  
   310  func completeNodeJSInstall(ctx context.Context, finalDir string) error {
   311  	if bin, err := npm.Install(ctx, finalDir, false /*production*/, nil, os.Stderr); err != nil {
   312  		return fmt.Errorf("failed to install dependencies of policy pack; you may need to re-run `%s install` "+
   313  			"in %q before this policy pack works"+": %w", bin, finalDir, err)
   314  
   315  	}
   316  
   317  	return nil
   318  }
   319  
   320  func completePythonInstall(ctx context.Context, finalDir, projPath string, proj *workspace.PolicyPackProject) error {
   321  	const venvDir = "venv"
   322  	if err := python.InstallDependencies(ctx, finalDir, venvDir, false /*showOutput*/); err != nil {
   323  		return err
   324  	}
   325  
   326  	// Save project with venv info.
   327  	proj.Runtime.SetOption("virtualenv", venvDir)
   328  	if err := proj.Save(projPath); err != nil {
   329  		return fmt.Errorf("saving project at %s: %w", projPath, err)
   330  	}
   331  
   332  	return nil
   333  }