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 }