pulumi/pkg/resource/stack/deployment.go
Joe Duffy bc2cf55463
Implement resource protection (#751)
This change implements resource protection, as per pulumi/pulumi#689.
The overall idea is that a resource can be marked as "protect: true",
which will prevent deletion of that resource for any reason whatsoever
(straight deletion, replacement, etc).  This is expressed in the
program.  To "unprotect" a resource, one must perform an update setting
"protect: false", and then afterwards, they can delete the resource.

For example:

    let res = new MyResource("precious", { .. }, { protect: true });

Afterwards, the resource will display in the CLI with a lock icon, and
any attempts to remove it will fail in the usual ways (in planning or,
worst case, during an actual update).

This was done by adding a new ResourceOptions bag parameter to the
base Resource types.  This is unfortunately a breaking change, but now
is the right time to take this one.  We had been adding new settings
one by one -- like parent and dependsOn -- and this new approach will
set us up to add any number of additional settings down the road,
without needing to worry about breaking anything ever again.

This is related to protected stacks, as described in
pulumi/pulumi-service#399.  Most likely this will serve as a foundational
building block that enables the coarser grained policy management.
2017-12-20 14:31:07 -08:00

242 lines
8.8 KiB
Go

// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
package stack
import (
"reflect"
"time"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/deploy"
"github.com/pulumi/pulumi/pkg/resource/plugin"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/contract"
)
// Deployment is a serializable, flattened LumiGL graph structure, representing a deploy. It is similar
// to the actual Snapshot structure, except that it flattens and rearranges a few data structures for serializability.
// Over time, we also expect this to gather more information about deploys themselves.
type Deployment struct {
Manifest Manifest `json:"manifest" yaml:"manifest"` // the deployment's manifest.
Resources []Resource `json:"resources,omitempty" yaml:"resources,omitempty"` // an array of resources.
}
// Manifest captures meta-information about this checkpoint file, such as versions of binaries, etc.
type Manifest struct {
Time time.Time `json:"time" yaml:"time"` // the time of the deploy.
Magic string `json:"magic" yaml:"magic"` // a magic cookie.
Version string `json:"version" yaml:"version"` // the version of the Pulumi CLI.
Plugins []PluginInfo `json:"plugins,omitempty" yaml:"plugins,omitempty"` // the plugin binary versions.
}
// PluginInfo captures the version and information about a plugin.
type PluginInfo struct {
Name string `json:"name" yaml:"name"`
Type plugin.Type `json:"type" yaml:"type"`
Version string `json:"version" yaml:"version"`
}
// Resource is a serializable vertex within a LumiGL graph, specifically for resource snapshots.
// nolint: lll
type Resource struct {
URN resource.URN `json:"urn" yaml:"urn"` // the URN for this resource.
Custom bool `json:"custom" yaml:"custom"` // if this is a custom resource managed by a plugin.
Delete bool `json:"delete,omitempty" yaml:"delete,omitempty"` // if this should be deleted during the next update.
ID resource.ID `json:"id,omitempty" yaml:"id,omitempty"` // the provider ID for this resource, if any.
Type tokens.Type `json:"type" yaml:"type"` // this resource's full type token.
Inputs map[string]interface{} `json:"inputs,omitempty" yaml:"inputs,omitempty"` // the input properties from the provider (or the program for ressources with defaults).
Defaults map[string]interface{} `json:"defaults,omitempty" yaml:"defaults,omitempty"` // the default property values from the provider (DEPRECATED, see #637).
Outputs map[string]interface{} `json:"outputs,omitempty" yaml:"outputs,omitempty"` // the output properties from the resource provider.
Parent resource.URN `json:"parent,omitempty" yaml:"parent,omitempty"` // an optional parent URN if this is a child resource.
Protect bool `json:"protect,omitempty" yaml:"protect,omitempty"` // true if this resource is protected, and cannot be deleted.
}
// SerializeDeployment serializes an entire snapshot as a deploy record.
func SerializeDeployment(snap *deploy.Snapshot) *Deployment {
// Capture the version information into a manifest.
manifest := Manifest{
Time: snap.Manifest.Time,
Magic: snap.Manifest.Magic,
Version: snap.Manifest.Version,
}
for _, plug := range snap.Manifest.Plugins {
manifest.Plugins = append(manifest.Plugins, PluginInfo{
Name: plug.Name,
Type: plug.Type,
Version: plug.Version,
})
}
// Serialize all vertices and only include a vertex section if non-empty.
var resources []Resource
for _, res := range snap.Resources {
resources = append(resources, SerializeResource(res))
}
return &Deployment{
Manifest: manifest,
Resources: resources,
}
}
// SerializeResource turns a resource into a structure suitable for serialization.
func SerializeResource(res *resource.State) Resource {
contract.Assert(res != nil)
contract.Assertf(string(res.URN) != "", "Unexpected empty resource resource.URN")
// Serialize all input and output properties recursively, and add them if non-empty.
var inputs map[string]interface{}
if inp := res.Inputs; inp != nil {
inputs = SerializeProperties(inp)
}
var outputs map[string]interface{}
if outp := res.Outputs; outp != nil {
outputs = SerializeProperties(outp)
}
return Resource{
URN: res.URN,
Custom: res.Custom,
Delete: res.Delete,
ID: res.ID,
Type: res.Type,
Parent: res.Parent,
Inputs: inputs,
Outputs: outputs,
Protect: res.Protect,
}
}
// SerializeProperties serializes a resource property bag so that it's suitable for serialization.
func SerializeProperties(props resource.PropertyMap) map[string]interface{} {
dst := make(map[string]interface{})
for _, k := range props.StableKeys() {
if v := SerializePropertyValue(props[k]); v != nil {
dst[string(k)] = v
}
}
return dst
}
// SerializePropertyValue serializes a resource property value so that it's suitable for serialization.
func SerializePropertyValue(prop resource.PropertyValue) interface{} {
// Skip nulls and "outputs"; the former needn't be serialized, and the latter happens if there is an output
// that hasn't materialized (either because we're serializing inputs or the provider didn't give us the value).
if prop.IsComputed() || !prop.HasValue() {
return nil
}
// For arrays, make sure to recurse.
if prop.IsArray() {
srcarr := prop.ArrayValue()
dstarr := make([]interface{}, len(srcarr))
for i, elem := range prop.ArrayValue() {
dstarr[i] = SerializePropertyValue(elem)
}
return dstarr
}
// Also for objects, recurse and use naked properties.
if prop.IsObject() {
return SerializeProperties(prop.ObjectValue())
}
// For assets, we need to serialize them a little carefully, so we can recover them afterwards.
if prop.IsAsset() {
return prop.AssetValue().Serialize()
} else if prop.IsArchive() {
return prop.ArchiveValue().Serialize()
}
// All others are returned as-is.
return prop.V
}
// DeserializeResource turns a serialized resource back into its usual form.
func DeserializeResource(res Resource) (*resource.State, error) {
// Deserialize the resource properties, if they exist.
inputs, err := DeserializeProperties(res.Inputs)
if err != nil {
return nil, err
}
defaults, err := DeserializeProperties(res.Defaults)
if err != nil {
return nil, err
}
outputs, err := DeserializeProperties(res.Outputs)
if err != nil {
return nil, err
}
// If this is an old checkpoint that still had defaults, merge the inputs into the defaults.
//
// NOTE: we will remove support for defaults entirely in the future. See #637.
if inputs != nil && defaults != nil {
inputs = defaults.Merge(inputs)
}
return resource.NewState(
res.Type, res.URN, res.Custom, res.Delete, res.ID, inputs, outputs, res.Parent, res.Protect), nil
}
// DeserializeProperties deserializes an entire map of deploy properties into a resource property map.
func DeserializeProperties(props map[string]interface{}) (resource.PropertyMap, error) {
result := make(resource.PropertyMap)
for k, prop := range props {
desprop, err := DeserializePropertyValue(prop)
if err != nil {
return nil, err
}
result[resource.PropertyKey(k)] = desprop
}
return result, nil
}
// DeserializePropertyValue deserializes a single deploy property into a resource property value.
func DeserializePropertyValue(v interface{}) (resource.PropertyValue, error) {
if v != nil {
switch w := v.(type) {
case bool:
return resource.NewBoolProperty(w), nil
case float64:
return resource.NewNumberProperty(w), nil
case string:
return resource.NewStringProperty(w), nil
case []interface{}:
var arr []resource.PropertyValue
for _, elem := range w {
ev, err := DeserializePropertyValue(elem)
if err != nil {
return resource.PropertyValue{}, err
}
arr = append(arr, ev)
}
return resource.NewArrayProperty(arr), nil
case map[string]interface{}:
obj, err := DeserializeProperties(w)
if err != nil {
return resource.PropertyValue{}, err
}
// This could be an asset or archive; if so, recover its type.
objmap := obj.Mappable()
asset, isasset, err := resource.DeserializeAsset(objmap)
if err != nil {
return resource.PropertyValue{}, err
} else if isasset {
return resource.NewAssetProperty(asset), nil
}
archive, isarchive, err := resource.DeserializeArchive(objmap)
if err != nil {
return resource.PropertyValue{}, err
} else if isarchive {
return resource.NewArchiveProperty(archive), nil
}
// Otherwise, it's just a weakly typed object map.
return resource.NewObjectProperty(obj), nil
default:
contract.Failf("Unrecognized property type: %v", reflect.ValueOf(v))
}
}
return resource.NewNullProperty(), nil
}