Add PolicyPack abstraction with Publish verb

This commit will implement the core business logic of `pulumi policy
publish` -- code to boot an analyzer, ask it for metadata about the
policies it contains, pack the code, and transmit all of this to the
Pulumi service.
This commit is contained in:
Alex Clemmer 2019-06-23 21:39:22 -07:00
parent 9e9f7f07d3
commit c93a860574
7 changed files with 251 additions and 0 deletions

View file

@ -20,6 +20,8 @@ import (
"fmt"
"time"
"github.com/pulumi/pulumi/pkg/diag"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/apitype"
@ -64,6 +66,20 @@ type StackReference interface {
Name() tokens.QName
}
// PolicyPackReference is an opaque type that refers to a PolicyPack managedby a backend. The CLI
// uses the ParsePolicyPackReference method to turn a string like "myOrg/mySecurityRules" into a
// PolicyPackReference that can be used to interact with the PolicyPack via the backend.
// PolicyPackReferences are specific to a given backend and different back ends may interpret the
// string passed to ParsePolicyPackReference differently.
type PolicyPackReference interface {
// fmt.Stringer's String() method returns a string of the stack identity, suitable for display in the CLI
fmt.Stringer
// OrgName is the name of the organization that is managing the PolicyPack.
OrgName() string
// Name is the name of the PolicyPack being referenced.
Name() tokens.QName
}
// StackSummary provides a basic description of a stack, without the ability to inspect its resources or make changes.
type StackSummary interface {
Name() StackReference
@ -82,6 +98,9 @@ type Backend interface {
// URL returns a URL at which information about this backend may be seen.
URL() string
// GetPolicyPack returns a PolicyPack object tied to this backend, or nil if it cannot be found.
GetPolicyPack(ctx context.Context, policyPack string, d diag.Sink) (PolicyPack, error)
// ParseStackReference takes a string representation and parses it to a reference which may be used for other
// methods in this backend.
ParseStackReference(s string) (StackReference, error)

View file

@ -198,6 +198,12 @@ func (b *localBackend) StateDir() string {
return workspace.BookkeepingDir
}
func (b *localBackend) GetPolicyPack(ctx context.Context, policyPack string,
d diag.Sink) (backend.PolicyPack, error) {
return nil, fmt.Errorf("File state backend does not support resource policy")
}
func (b *localBackend) ParseStackReference(stackRefName string) (backend.StackReference, error) {
return localBackendReference{name: tokens.QName(stackRefName)}, nil
}
@ -333,6 +339,14 @@ func (b *localBackend) GetLatestConfiguration(ctx context.Context,
return hist[0].Config, nil
}
func (b *localBackend) PackPolicies(
ctx context.Context, policyPackRef backend.PolicyPackReference,
cancellationScopes backend.CancellationScopeSource,
callerEventsOpt chan<- engine.Event) result.Result {
return result.Error("File state backend does not support resource policy")
}
func (b *localBackend) Preview(ctx context.Context, stackRef backend.StackReference,
op backend.UpdateOperation) (engine.ResourceChanges, result.Result) {
// Get the stack.

View file

@ -354,6 +354,43 @@ func (b *cloudBackend) CurrentUser() (string, error) {
func (b *cloudBackend) CloudURL() string { return b.url }
func (b *cloudBackend) parsePolicyPackReference(s string) (backend.PolicyPackReference, error) {
split := strings.Split(s, "/")
var orgName string
var policyPackName string
switch len(split) {
case 2:
orgName = split[0]
policyPackName = split[1]
default:
return nil, errors.Errorf("could not parse policy pack name '%s'; must be of the form "+
"<orgName>/<policyPackName>", s)
}
return newCloudBackendPolicyPackReference(orgName, tokens.QName(policyPackName)), nil
}
func (b *cloudBackend) GetPolicyPack(ctx context.Context, policyPack string,
d diag.Sink) (backend.PolicyPack, error) {
policyPackRef, err := b.parsePolicyPackReference(policyPack)
if err != nil {
return nil, err
}
apiToken, err := workspace.GetAccessToken(b.CloudURL())
if err != nil {
return nil, err
}
return &cloudPolicyPack{
ref: newCloudBackendPolicyPackReference(
policyPackRef.OrgName(), tokens.QName(policyPackRef.Name())),
b: b,
cl: client.NewClient(b.CloudURL(), apiToken, d)}, nil
}
func (b *cloudBackend) ParseStackReference(s string) (backend.StackReference, error) {
split := strings.Split(s, "/")
var owner string

View file

@ -106,4 +106,7 @@ func init() {
addEndpoint("POST", "/api/stacks/{orgName}/{projectName}/{stackName}/{updateKind}/{updateID}/complete", "completeUpdate")
addEndpoint("POST", "/api/stacks/{orgName}/{projectName}/{stackName}/{updateKind}/{updateID}/events", "postEngineEvent")
addEndpoint("POST", "/api/stacks/{orgName}/{projectName}/{stackName}/{updateKind}/{updateID}/renew_lease", "renewLease")
// APIs for managing `PolicyPack`s.
addEndpoint("POST", "/api/orgs/{orgName}/policypacks", "publishPolicyPack")
}

View file

@ -15,6 +15,7 @@
package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
@ -22,6 +23,8 @@ import (
"path"
"time"
"github.com/pulumi/pulumi/pkg/resource/plugin"
"github.com/blang/semver"
"github.com/pkg/errors"
@ -87,6 +90,12 @@ func getStackPath(stack StackIdentifier, components ...string) string {
return path.Join(append([]string{prefix}, components...)...)
}
// publishPolicyPackPath returns the API path to for the given organization with the given
// components joined with path separators and appended to the organization root.
func publishPolicyPackPath(orgName string) string {
return fmt.Sprintf("/api/orgs/%s/policypacks", orgName)
}
// getUpdatePath returns the API path to for the given stack with the given components joined with path separators
// and appended to the update root.
func getUpdatePath(update UpdateIdentifier, components ...string) string {
@ -415,6 +424,45 @@ func (pc *Client) StartUpdate(ctx context.Context, update UpdateIdentifier,
return resp.Version, resp.Token, nil
}
// PublishPolicyPack publishes a `PolicyPack` to the Pulumi service.
func (pc *Client) PublishPolicyPack(ctx context.Context, orgName string,
analyzerInfo plugin.AnalyzerInfo, dirArchive *bytes.Buffer) error {
//
// Step 1 of 2: Send POST containing policy metadata to service. This begins process of creating
// publishing the PolicyPack.
//
req := apitype.CreatePolicyPackRequest{
Name: analyzerInfo.Name,
DisplayName: analyzerInfo.DisplayName,
Policies: analyzerInfo.Policies,
}
var resp apitype.CreatePolicyPackResponse
err := pc.restCall(ctx, "POST", publishPolicyPackPath(orgName), nil, req, &resp)
if err != nil {
return errors.Wrapf(err, "HTTP POST to publish policy pack failed")
}
//
// Step 2 or 2: Upload the compressed PolicyPack directory to the presigned S3 URL. The
// PolicyPack is now published.
//
putS3Req, err := http.NewRequest(http.MethodPut, resp.UploadURI, dirArchive)
if err != nil {
return errors.Wrapf(err, "Failed to upload compressed PolicyPack")
}
_, err = http.DefaultClient.Do(putS3Req)
if err != nil {
return errors.Wrapf(err, "Failed to upload compressed PolicyPack")
}
return nil
}
// GetUpdateEvents returns all events, taking an optional continuation token from a previous call.
func (pc *Client) GetUpdateEvents(ctx context.Context, update UpdateIdentifier,
continuationToken *string) (apitype.UpdateResults, error) {

View file

@ -0,0 +1,91 @@
package httpstate
import (
"context"
"fmt"
"github.com/pulumi/pulumi/pkg/util/archive"
"github.com/pulumi/pulumi/pkg/backend"
"github.com/pulumi/pulumi/pkg/backend/httpstate/client"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/result"
)
func newCloudBackendPolicyPackReference(
orgName string, name tokens.QName) *cloudBackendPolicyPackReference {
return &cloudBackendPolicyPackReference{orgName: orgName, name: name}
}
// cloudBackendPolicyPackReference is a reference to a PolicyPack implemented by the Pulumi service.
type cloudBackendPolicyPackReference struct {
// name of the PolicyPack.
name tokens.QName
// orgName that administrates the PolicyPack.
orgName string
}
var _ backend.PolicyPackReference = (*cloudBackendPolicyPackReference)(nil)
func (pr *cloudBackendPolicyPackReference) String() string {
return fmt.Sprintf("%s/%s", pr.orgName, pr.name)
}
func (pr *cloudBackendPolicyPackReference) OrgName() string {
return pr.orgName
}
func (pr *cloudBackendPolicyPackReference) Name() tokens.QName {
return pr.name
}
// cloudPolicyPack is a the Pulumi service implementation of the PolicyPack interface.
type cloudPolicyPack struct {
// ref uniquely identifies the PolicyPack in the Pulumi service.
ref *cloudBackendPolicyPackReference
// b is a pointer to the backend that this PolicyPack belongs to.
b *cloudBackend
// cl is the client used to interact with the backend.
cl *client.Client
}
var _ backend.PolicyPack = (*cloudPolicyPack)(nil)
func (pack *cloudPolicyPack) Ref() backend.PolicyPackReference {
return pack.ref
}
func (pack *cloudPolicyPack) Backend() backend.Backend {
return pack.b
}
func (pack *cloudPolicyPack) Publish(
ctx context.Context, op backend.PublishOperation) result.Result {
//
// Get PolicyPack metadata from the plugin.
//
analyzer, err := op.PlugCtx.Host.Analyzer(pack.Ref().Name())
if err != nil {
return result.FromError(err)
}
analyzerInfo, err := analyzer.GetAnalyzerInfo()
if err != nil {
return result.FromError(err)
}
dirArchive, err := archive.Process(op.Root, false)
if err != nil {
return result.FromError(err)
}
err = pack.cl.PublishPolicyPack(ctx, pack.ref.orgName, analyzerInfo, dirArchive)
if err != nil {
return result.FromError(err)
}
return nil
}

39
pkg/backend/policypack.go Normal file
View file

@ -0,0 +1,39 @@
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package backend
import (
"context"
"github.com/pulumi/pulumi/pkg/resource/plugin"
"github.com/pulumi/pulumi/pkg/util/result"
)
// PublishOperation publishes a PolicyPack to the backend.
type PublishOperation struct {
Root string
PlugCtx *plugin.Context
Scopes CancellationScopeSource
}
// PolicyPack is a set of policies associated with a particular backend implementation.
type PolicyPack interface {
// Ref returns a reference to this PolicyPack.
Ref() PolicyPackReference
// Backend returns the backend this PolicyPack is managed by.
Backend() Backend
// Publish the PolicyPack to the service.
Publish(ctx context.Context, op PublishOperation) result.Result
}