// Copyright 2016-2025, 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 schema
import (
"cmp"
"context"
_ "embed"
"errors"
"fmt"
"io"
"math"
"net/url"
"os"
"path"
"regexp"
"slices"
"sort"
"strings"
"github.com/blang/semver"
"github.com/hashicorp/hcl/v2"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/santhosh-tekuri/jsonschema/v5"
"github.com/segmentio/encoding/json"
)
//go:embed pulumi.json
var metaSchema string
var MetaSchema *jsonschema.Schema
func init() {
compiler := jsonschema.NewCompiler()
compiler.LoadURL = func(u string) (io.ReadCloser, error) {
if u == "blob://pulumi.json" {
return io.NopCloser(strings.NewReader(metaSchema)), nil
}
return jsonschema.LoadURL(u)
}
MetaSchema = compiler.MustCompile("blob://pulumi.json")
}
func sortedKeys[K cmp.Ordered, V any](m map[K]V) []K {
keys := slice.Prealloc[K](len(m))
for key := range m {
keys = append(keys, key)
}
slices.Sort(keys)
return keys
}
func memberPath(section, token string, rest ...string) string {
path := fmt.Sprintf("#/%v/%v", section, url.PathEscape(token))
if len(rest) != 0 {
path += "/" + strings.Join(rest, "/")
}
return path
}
func errorf(path, message string, args ...interface{}) *hcl.Diagnostic {
contract.Requiref(path != "", "path", "must not be empty")
summary := path + ": " + fmt.Sprintf(message, args...)
return &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: summary,
}
}
func warningf(path, message string, args ...interface{}) *hcl.Diagnostic {
contract.Requiref(path != "", "path", "must not be empty")
summary := path + ": " + fmt.Sprintf(message, args...)
return &hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: summary,
}
}
func validateSpec(spec PackageSpec) (hcl.Diagnostics, error) {
bytes, err := json.Marshal(spec)
if err != nil {
return nil, err
}
var raw interface{}
if err = json.Unmarshal(bytes, &raw); err != nil {
return nil, err
}
if err = MetaSchema.Validate(raw); err == nil {
return nil, nil
}
validationError, ok := err.(*jsonschema.ValidationError)
if !ok {
return nil, err
}
var diags hcl.Diagnostics
var appendError func(err *jsonschema.ValidationError)
appendError = func(err *jsonschema.ValidationError) {
if err.InstanceLocation != "" && err.Message != "" {
diags = diags.Append(errorf("#"+err.InstanceLocation, "%v", err.Message))
}
for _, err := range err.Causes {
appendError(err)
}
}
appendError(validationError)
return diags, nil
}
// bindSpec converts a serializable PackageSpec into a Package. This function includes a loader parameter which
// works as a singleton -- if it is nil, a new loader is instantiated, else the provided loader is used. This avoids
// breaking downstream consumers of ImportSpec while allowing us to extend schema support to external packages.
//
// A few notes on diagnostics and errors in spec binding:
//
// - Unless an error is *fatal*--i.e. binding is fundamentally unable to proceed (e.g. because a provider for a
// package failed to load)--errors should be communicated as diagnostics. Fatal errors should be communicated as
// error values.
// - Semantic errors during type binding should not be fatal. Instead, they should return an `InvalidType`. The
// invalid type is accepted in any position, and carries diagnostics that explain the semantic error during binding.
// This allows binding to continue and produce as much information as possible for the end user.
// - Diagnostics may be rendered to users by downstream tools, and should be written with schema authors in mind.
// - Diagnostics _must_ contain enough contextual information for a user to be able to understand the source of the
// diagnostic. Until we have line/column information, we use JSON pointers to the offending entities. These pointers
// are passed around using `path` parameters. The `errorf` function is provided as a utility to easily create a
// diagnostic error that is appropriately tagged with a JSON pointer.
func bindSpec(spec PackageSpec, languages map[string]Language, loader Loader,
validate bool,
options ValidationOptions,
) (*Package, hcl.Diagnostics, error) {
var diags hcl.Diagnostics
// Validate the package against the metaschema.
if validate {
validationDiags, err := validateSpec(spec)
diags = diags.Extend(validationDiags)
if err != nil {
return nil, diags, fmt.Errorf("validating spec: %w", err)
}
}
types, pkgDiags, err := newBinder(spec.Info(), packageSpecSource{&spec}, loader, nil)
diags = diags.Extend(pkgDiags)
if err != nil {
return nil, diags, err
}
defer contract.IgnoreClose(types)
diags = diags.Extend(spec.validateTypeTokens())
config, configDiags, err := bindConfig(spec.Config, types, options)
diags = diags.Extend(configDiags)
if err != nil {
return nil, diags, err
}
provider, resources, resourceDiags, err := types.finishResources(sortedKeys(spec.Resources), options)
diags = diags.Extend(resourceDiags)
if err != nil {
return nil, diags, err
}
functions, functionDiags, err := types.finishFunctions(sortedKeys(spec.Functions), options)
diags = diags.Extend(functionDiags)
if err != nil {
return nil, diags, err
}
typeList, typeDiags, err := types.finishTypes(sortedKeys(spec.Types), options)
diags = diags.Extend(typeDiags)
if err != nil {
return nil, diags, err
}
parameterization, parameterizationDiags := bindParameterization(spec.Parameterization)
diags = diags.Extend(parameterizationDiags)
diags = diags.Extend(checkDuplicates(spec.Resources, spec.Functions))
pkg := types.pkg
pkg.Config = config
pkg.Types = typeList
pkg.Provider = provider
pkg.Resources = resources
pkg.Functions = functions
pkg.Parameterization = parameterization
pkg.Dependencies = spec.Dependencies
pkg.resourceTable = types.resourceDefs
pkg.functionTable = types.functionDefs
pkg.typeTable = types.typeDefs
pkg.resourceTypeTable = types.resources
if err := pkg.ImportLanguages(languages); err != nil {
return nil, nil, err
}
return pkg, diags, nil
}
// Create a new binder.
//
// bindTo overrides the PackageReference field contained in generated types.
func newBinder(info PackageInfoSpec, spec specSource, loader Loader,
bindTo PackageReference,
) (*types, hcl.Diagnostics, error) {
var diags hcl.Diagnostics
// Validate that there is a name
if info.Name == "" {
diags = diags.Append(errorf("#/name", "no name provided"))
}
// Parse the version, if any.
var version *semver.Version
if info.Version != "" {
v, err := semver.ParseTolerant(info.Version)
if err != nil {
diags = diags.Append(errorf("#/version", "failed to parse semver: %v", err))
} else {
version = &v
}
}
if info.Meta != nil && info.Meta.SupportPack && info.Version == "" {
diags = diags.Append(errorf("#/version", "version must be provided when package supports packing"))
}
// Parse the module format, if any.
moduleFormat := "(.*)"
if info.Meta != nil && info.Meta.ModuleFormat != "" {
moduleFormat = info.Meta.ModuleFormat
}
moduleFormatRegexp, err := regexp.Compile(moduleFormat)
if err != nil {
diags = diags.Append(errorf("#/meta/moduleFormat", "failed to compile regex: %v", err))
}
language := make(map[string]interface{}, len(info.Language))
for name, v := range info.Language {
language[name] = json.RawMessage(v)
}
supportPack := false
if info.Meta != nil {
supportPack = info.Meta.SupportPack
}
// Parameterized packages must always be built in SupportPack mode.
if info.Parameterization != nil {
supportPack = true
}
parameterization, parameterizationDiagnostics := bindParameterization(info.Parameterization)
diags = diags.Extend(parameterizationDiagnostics)
pkg := &Package{
SupportPack: supportPack,
moduleFormat: moduleFormatRegexp,
Name: info.Name,
DisplayName: info.DisplayName,
Version: version,
Description: info.Description,
Keywords: info.Keywords,
Homepage: info.Homepage,
License: info.License,
Attribution: info.Attribution,
Repository: info.Repository,
PluginDownloadURL: info.PluginDownloadURL,
Publisher: info.Publisher,
Namespace: info.Namespace,
Dependencies: info.Dependencies,
AllowedPackageNames: info.AllowedPackageNames,
LogoURL: info.LogoURL,
Language: language,
Parameterization: parameterization,
}
// We want to use the same loader instance for all referenced packages, so only instantiate the loader if the
// reference is nil.
var loadCtx io.Closer
if loader == nil {
cwd, err := os.Getwd()
if err != nil {
return nil, nil, err
}
ctx, err := plugin.NewContext(context.TODO(), nil, nil, nil, nil, cwd, nil, false, nil)
if err != nil {
return nil, nil, err
}
loader, loadCtx = NewPluginLoader(ctx.Host), ctx
}
// Create a type binder.
types := &types{
pkg: pkg,
spec: spec,
loader: loader,
loadCtx: loadCtx,
typeDefs: map[string]Type{},
functionDefs: map[string]*Function{},
resourceDefs: map[string]*Resource{},
resources: map[string]*ResourceType{},
arrays: map[Type]*ArrayType{},
maps: map[Type]*MapType{},
unions: map[string]*UnionType{},
tokens: map[string]*TokenType{},
inputs: map[Type]*InputType{},
optionals: map[Type]*OptionalType{},
bindToReference: bindTo,
}
return types, diags, nil
}
// Options that affect the validation of the packgae schema.
type ValidationOptions struct {
AllowDanglingReferences bool
}
// BindSpec converts a serializable PackageSpec into a Package. Any semantic errors encountered during binding are
// contained in the returned diagnostics. The returned error is only non-nil if a fatal error was encountered.
func BindSpec(spec PackageSpec, loader Loader, options ValidationOptions) (*Package, hcl.Diagnostics, error) {
return bindSpec(spec, nil, loader, true, options)
}
// ImportSpec converts a serializable PackageSpec into a Package. Unlike BindSpec, ImportSpec does not validate its
// input against the Pulumi package metaschema. ImportSpec should only be used to load packages that are assumed to be
// well-formed (e.g. packages referenced for program code generation or by a root package being used for SDK
// generation). BindSpec should be used to load and validate a package spec prior to generating its SDKs.
func ImportSpec(spec PackageSpec, languages map[string]Language, options ValidationOptions) (*Package, error) {
// Call the internal implementation that includes a loader parameter.
pkg, diags, err := bindSpec(spec, languages, nil, false, options)
if err != nil {
return nil, err
}
if diags.HasErrors() {
return nil, diags
}
return pkg, nil
}
// ImportPartialSpec converts a serializable PartialPackageSpec into a PartialPackage. Unlike a typical Package, a
// PartialPackage loads and binds its members on-demand rather than at import time. This is useful when the entire
// contents of a package are not needed (e.g. for referenced packages).
func ImportPartialSpec(spec PartialPackageSpec, languages map[string]Language, loader Loader) (*PartialPackage, error) {
pkg := &PartialPackage{
spec: &spec,
languages: languages,
}
types, diags, err := newBinder(spec.PackageInfoSpec, partialPackageSpecSource{&spec}, loader, pkg)
if err != nil {
return nil, err
}
if diags.HasErrors() {
return nil, diags
}
pkg.types = types
return pkg, nil
}
type specSource interface {
GetTypeDefSpec(token string) (ComplexTypeSpec, bool, error)
GetFunctionSpec(token string) (FunctionSpec, bool, error)
GetResourceSpec(token string) (ResourceSpec, bool, error)
}
type packageSpecSource struct {
spec *PackageSpec
}
func (s packageSpecSource) GetTypeDefSpec(token string) (ComplexTypeSpec, bool, error) {
spec, ok := s.spec.Types[token]
return spec, ok, nil
}
func (s packageSpecSource) GetFunctionSpec(token string) (FunctionSpec, bool, error) {
spec, ok := s.spec.Functions[token]
return spec, ok, nil
}
func (s packageSpecSource) GetResourceSpec(token string) (ResourceSpec, bool, error) {
if token == "pulumi:providers:"+s.spec.Name {
return s.spec.Provider, true, nil
}
spec, ok := s.spec.Resources[token]
return spec, ok, nil
}
type partialPackageSpecSource struct {
spec *PartialPackageSpec
}
func (s partialPackageSpecSource) GetTypeDefSpec(token string) (ComplexTypeSpec, bool, error) {
rawSpec, ok := s.spec.Types[token]
if !ok {
return ComplexTypeSpec{}, false, nil
}
var spec ComplexTypeSpec
if err := parseJSONPropertyValue(rawSpec, &spec); err != nil {
return ComplexTypeSpec{}, false, err
}
return spec, true, nil
}
func (s partialPackageSpecSource) GetFunctionSpec(token string) (FunctionSpec, bool, error) {
rawSpec, ok := s.spec.Functions[token]
if !ok {
return FunctionSpec{}, false, nil
}
var spec FunctionSpec
if err := parseJSONPropertyValue(rawSpec, &spec); err != nil {
return FunctionSpec{}, false, err
}
return spec, true, nil
}
func (s partialPackageSpecSource) GetResourceSpec(token string) (ResourceSpec, bool, error) {
var rawSpec json.RawMessage
if token == "pulumi:providers:"+s.spec.Name {
rawSpec = s.spec.Provider
} else {
raw, ok := s.spec.Resources[token]
if !ok {
return ResourceSpec{}, false, nil
}
rawSpec = raw
}
var spec ResourceSpec
if err := parseJSONPropertyValue(rawSpec, &spec); err != nil {
return ResourceSpec{}, false, err
}
return spec, true, nil
}
// types facilitates interning (only storing a single reference to an object) during schema processing. The fields
// correspond to fields in the schema, and are populated during the binding process.
type types struct {
pkg *Package
spec specSource
loader Loader
loadCtx io.Closer
typeDefs map[string]Type // objects and enums
functionDefs map[string]*Function // function definitions
resourceDefs map[string]*Resource // resource definitions
resources map[string]*ResourceType
arrays map[Type]*ArrayType
maps map[Type]*MapType
unions map[string]*UnionType
tokens map[string]*TokenType
inputs map[Type]*InputType
optionals map[Type]*OptionalType
// A pointer to the package reference that `types` is a part of if it exists.
bindToReference PackageReference
}
func (t *types) Close() error {
if t.loadCtx != nil {
return t.loadCtx.Close()
}
return nil
}
// The package which bound types will link back to.
func (t *types) externalPackage() PackageReference {
if t.bindToReference != nil {
return t.bindToReference
}
return t.pkg.Reference()
}
func (t *types) bindPrimitiveType(path, name string) (Type, hcl.Diagnostics) {
switch name {
case "boolean":
return BoolType, nil
case "integer":
return IntType, nil
case "number":
return NumberType, nil
case "string":
return StringType, nil
default:
return invalidType(errorf(path, "unknown primitive type %v", name))
}
}
// typeSpecRef contains the parsed fields from a type spec reference.
type typeSpecRef struct {
URL *url.URL // The parsed URL
Package string // The package component of the schema ref
Version *semver.Version // The version component of the schema ref
Kind string // The kind of reference: 'resources', 'types', or 'provider'
Token string // The type token
}
const (
resourcesRef = "resources"
typesRef = "types"
providerRef = "provider"
)
// validateTypeToken validates an individual type token. It accepts a map which relates permitted package names to the
// set of permitted module names within those packages. If a package name maps to nil, any module name is permitted.
func (spec *PackageSpec) validateTypeToken(
allowedNameSpecs map[string][]string,
section string,
token string,
) hcl.Diagnostics {
var diags hcl.Diagnostics
path := memberPath(section, token)
parts := strings.Split(token, ":")
if len(parts) != 3 {
err := errorf(path, "invalid token '%s' (should have three parts)", token)
diags = diags.Append(err)
// Early return because the other two error checks panic if len(parts) < 3
return diags
}
modules, ok := allowedNameSpecs[parts[0]]
if !ok {
err := errorf(path, "invalid token '%s' (must have package name '%s')", token, spec.Name)
diags = diags.Append(err)
}
if (parts[1] == "" || strings.EqualFold(parts[1], "index")) && strings.EqualFold(parts[2], "provider") {
err := errorf(path, "invalid token '%s' (provider is a reserved word for the root module)", token)
diags = diags.Append(err)
}
if modules != nil && !slices.Contains(modules, parts[1]) {
err := errorf(path, "invalid token '%s' (must have a module name in [%s])", token, strings.Join(modules, ", "))
diags = diags.Append(err)
}
return diags
}
// This is for validating non-reference type tokens.
func (spec *PackageSpec) validateTypeTokens() hcl.Diagnostics {
var diags hcl.Diagnostics
allowedNameSpecs := map[string][]string{spec.Name: nil}
for _, prefix := range spec.AllowedPackageNames {
allowedNameSpecs[prefix] = nil
}
for t := range spec.Resources {
diags = diags.Extend(spec.validateTypeToken(allowedNameSpecs, "resources", t))
}
for t := range spec.Types {
diags = diags.Extend(spec.validateTypeToken(allowedNameSpecs, "types", t))
}
// When validating function type tokens, we'll add `pulumi` to the list of allowed package names in order to support
// defining methods *on provider resources themselves*. In these cases, we'll see tokens of the form
// `pulumi:providers:<package>/<method>`, which we want to treat as valid. We need to be careful to ensure that if
// `pulumi` is already in the list of allowed packages that we do not break anything. Specifically:
//
// * If there is a mapping `pulumi`: nil (accept all module names), we'll leave it be, since this will already accept
// the names we want to allow.
//
// * If there is a mapping `pulumi`: [module1, module2, ...], we'll add `providers` to the list of module names if
// it's not in there already.
//
// * If there is no mapping for `pulumi`, we'll add one that only has `providers` in its list of module names.
pulumiMods, hasPulumi := allowedNameSpecs["pulumi"]
if hasPulumi {
if pulumiMods != nil && !slices.Contains(pulumiMods, "providers") {
allowedNameSpecs["pulumi"] = append(pulumiMods, "providers")
}
} else {
allowedNameSpecs["pulumi"] = []string{"providers"}
}
for t := range spec.Functions {
diags = diags.Extend(spec.validateTypeToken(allowedNameSpecs, "functions", t))
}
return diags
}
// Regex used to parse external schema paths. This is declared at the package scope to avoid repeated recompilation.
var refPathRegex = regexp.MustCompile(`^/?(?P<package>[-\w]+)/(?P<version>v[^/]*)/schema\.json$`)
func (t *types) parseTypeSpecRef(refPath, ref string) (typeSpecRef, hcl.Diagnostics) {
parsedURL, err := url.Parse(ref)
if err != nil {
return typeSpecRef{}, hcl.Diagnostics{errorf(refPath, "failed to parse ref URL '%s': %v", ref, err)}
}
// Parse the package name and version if the URL contains a path. If there is no path--if the URL is just a
// fragment--then the reference refers to the package being bound.
pkgName, pkgVersion := t.pkg.Name, t.pkg.Version
if len(parsedURL.Path) > 0 {
path, err := url.PathUnescape(parsedURL.Path)
if err != nil {
return typeSpecRef{}, hcl.Diagnostics{errorf(refPath, "failed to unescape path '%s': %v", parsedURL.Path, err)}
}
pathMatch := refPathRegex.FindStringSubmatch(path)
if len(pathMatch) != 3 {
return typeSpecRef{}, hcl.Diagnostics{errorf(refPath, "failed to parse path '%s'", path)}
}
pkg, versionToken := pathMatch[1], pathMatch[2]
version, err := semver.ParseTolerant(versionToken)
if err != nil {
return typeSpecRef{}, hcl.Diagnostics{errorf(refPath, "failed to parse package version '%s': %v", versionToken, err)}
}
pkgName, pkgVersion = pkg, &version
}
// Parse the fragment into a reference kind and token. The fragment is in one of two forms:
// 1. #/provider
// 2. #/(resources|types)/some:type:token
//
// Unfortunately, early code generators were lax and emitted unescaped backslashes in the type token, so we can't
// just split on "/".
fragment := path.Clean(parsedURL.EscapedFragment())
if path.IsAbs(fragment) {
fragment = fragment[1:]
}
var kind, token string
slash := strings.Index(fragment, "/")
if slash == -1 {
kind = fragment
} else {
kind, token = fragment[:slash], fragment[slash+1:]
}
var diagnostics hcl.Diagnostics
switch kind {
case "provider":
if token != "" {
return typeSpecRef{}, hcl.Diagnostics{errorf(refPath, "invalid provider reference '%v'", ref)}
}
token = "pulumi:providers:" + pkgName
case "resources", "types":
token, err = url.PathUnescape(token)
if err != nil {
return typeSpecRef{}, hcl.Diagnostics{errorf(refPath, "failed to unescape token '%s': %v", token, err)}
}
// Its possible that the token is "pulumi:providers:<package>", which happened to work through the rest of
// binding but wasn't intended as a valid schema. The ref pointed to doesn't actually exist, there is no entry
// for "pulumi:providers:<package>" under the "resources" section. See
// https://github.com/pulumi/pulumi/issues/20029 for context.
if strings.HasPrefix(token, "pulumi:providers:") {
// For now we still consider this a valid reference, but we want to return a warning diagnostic. In the
// future, we'll make this an error.
diagnostics = hcl.Diagnostics{
warningf(refPath, "reference to provider resource '/resources/%s' is deprecated, use '#/provider' instead", token),
}
}
default:
return typeSpecRef{}, hcl.Diagnostics{errorf(refPath, "invalid type reference '%v'", ref)}
}
return typeSpecRef{
URL: parsedURL,
Package: pkgName,
Version: pkgVersion,
Kind: kind,
Token: token,
}, diagnostics
}
func versionEquals(a, b *semver.Version) bool {
// We treat "nil" as "unconstrained".
if a == nil || b == nil {
return true
}
return a.Equals(*b)
}
func (t *types) newInputType(elementType Type) Type {
if _, ok := elementType.(*InputType); ok {
return elementType
}
typ, ok := t.inputs[elementType]
if !ok {
typ = &InputType{ElementType: elementType}
t.inputs[elementType] = typ
}
return typ
}
func (t *types) newOptionalType(elementType Type) Type {
if _, ok := elementType.(*OptionalType); ok {
return elementType
}
typ, ok := t.optionals[elementType]
if !ok {
typ = &OptionalType{ElementType: elementType}
t.optionals[elementType] = typ
}
return typ
}
func (t *types) newMapType(elementType Type) Type {
typ, ok := t.maps[elementType]
if !ok {
typ = &MapType{ElementType: elementType}
t.maps[elementType] = typ
}
return typ
}
func (t *types) newArrayType(elementType Type) Type {
typ, ok := t.arrays[elementType]
if !ok {
typ = &ArrayType{ElementType: elementType}
t.arrays[elementType] = typ
}
return typ
}
func (t *types) newUnionType(
elements []Type, defaultType Type, discriminator string, mapping map[string]string,
) *UnionType {
union := &UnionType{
ElementTypes: elements,
DefaultType: defaultType,
Discriminator: discriminator,
Mapping: mapping,
}
if typ, ok := t.unions[union.String()]; ok {
return typ
}
t.unions[union.String()] = union
return union
}
func (t *types) bindTypeDef(token string, options ValidationOptions) (Type, hcl.Diagnostics, error) {
// Check to see if this type has already been bound.
if typ, ok := t.typeDefs[token]; ok {
return typ, nil, nil
}
// Check to see if we have a definition for this type. If we don't, just return nil.
spec, ok, err := t.spec.GetTypeDefSpec(token)
if err != nil || !ok {
return nil, nil, err
}
var diags hcl.Diagnostics
path := memberPath("types", token)
parts := strings.Split(token, ":")
if len(parts) == 3 {
name := parts[2]
if isReservedKeyword(name) {
diags = append(diags, errorf(path, name+" is a reserved name, cannot name type"))
return nil, diags, errors.New("type name " + name + " is reserved")
}
}
// Is this an object type?
if spec.Type == "object" {
// Declare the type.
//
// It's important that we set the token here. This package interns types so that they can be equality-compared
// for identity. Types are interned based on their string representation, and the string representation of an
// object type is its token. While this doesn't affect object types directly, it breaks the interning of types
// that reference object types (e.g. arrays, maps, unions)
obj := &ObjectType{Token: token, IsOverlay: spec.IsOverlay, OverlaySupportedLanguages: spec.OverlaySupportedLanguages}
obj.InputShape = &ObjectType{
Token: token, PlainShape: obj, IsOverlay: spec.IsOverlay,
OverlaySupportedLanguages: spec.OverlaySupportedLanguages,
}
t.typeDefs[token] = obj
oDiags, err := t.bindObjectTypeDetails(path, obj, token, spec.ObjectTypeSpec, options)
diags = append(diags, oDiags...)
if err != nil {
return nil, diags, err
}
return obj, diags, nil
}
// Otherwise, bind an enum type.
enum, eDiags := t.bindEnumType(token, spec)
diags = append(diags, eDiags...)
t.typeDefs[token] = enum
return enum, diags, nil
}
func (t *types) bindResourceTypeDef(token string, options ValidationOptions) (*ResourceType, hcl.Diagnostics, error) {
if typ, ok := t.resources[token]; ok {
return typ, nil, nil
}
res, diags, err := t.bindResourceDef(token, options)
if err != nil {
return nil, diags, err
}
if res == nil {
return nil, diags, nil
}
typ := &ResourceType{Token: token, Resource: res}
t.resources[token] = typ
return typ, diags, nil
}
func (t *types) bindTypeSpecRef(
path string,
spec TypeSpec,
inputShape bool,
options ValidationOptions,
) (Type, hcl.Diagnostics, error) {
path = path + "/$ref"
// Explicitly handle built-in types so that we don't have to handle this type of path during ref parsing.
switch spec.Ref {
case "pulumi.json#/Archive":
return ArchiveType, nil, nil
case "pulumi.json#/Asset":
return AssetType, nil, nil
case "pulumi.json#/Json":
return JSONType, nil, nil
case "pulumi.json#/Any":
return AnyType, nil, nil
}
ref, refDiags := t.parseTypeSpecRef(path, spec.Ref)
if refDiags.HasErrors() {
typ, _ := invalidType(refDiags...)
return typ, refDiags, nil
}
// If this is a reference to an external sch
referencesExternalSchema := ref.Package != t.pkg.Name || !versionEquals(ref.Version, t.pkg.Version)
if referencesExternalSchema {
pkg, err := LoadPackageReference(t.loader, ref.Package, ref.Version)
if err != nil {
return nil, nil, fmt.Errorf("resolving package %v: %w", ref.URL, err)
}
switch ref.Kind {
case typesRef:
typ, ok, err := pkg.Types().Get(ref.Token)
if err != nil {
return nil, nil, fmt.Errorf("loading type %v: %w", ref.Token, err)
}
if !ok {
typ, diags := invalidType(errorf(path, "type %v not found in package %v", ref.Token, ref.Package))
return typ, diags, nil
}
if obj, ok := typ.(*ObjectType); ok && inputShape {
typ = obj.InputShape
}
return typ, nil, nil
case resourcesRef, providerRef:
typ, ok, err := pkg.Resources().GetType(ref.Token)
if err != nil {
return nil, nil, fmt.Errorf("loading type %v: %w", ref.Token, err)
}
if !ok {
typ, diags := invalidType(errorf(path, "resource type %v not found in package %v", ref.Token, ref.Package))
return typ, diags, nil
}
return typ, nil, nil
}
}
switch ref.Kind {
case typesRef:
// Try to bind this as a reference to a type defined by this package.
typ, diags, err := t.bindTypeDef(ref.Token, options)
diags = refDiags.Extend(diags)
if err != nil {
return nil, diags, err
}
switch typ := typ.(type) {
case *ObjectType:
// If the type is an object type, we might need to return its input shape.
if inputShape {
return typ.InputShape, diags, nil
}
return typ, diags, nil
case *EnumType:
return typ, diags, nil
default:
contract.Assertf(typ == nil, "unexpected type %T", typ)
}
// If the type is not a known type, bind it as an opaque token type.
tokenType, ok := t.tokens[ref.Token]
if !ok {
tokenType = &TokenType{Token: ref.Token}
if spec.Type != "" {
ut, primDiags := t.bindPrimitiveType(path, spec.Type)
diags = diags.Extend(primDiags)
tokenType.UnderlyingType = ut
}
t.tokens[ref.Token] = tokenType
if !options.AllowDanglingReferences {
typ, diags := invalidType(errorf(path, "type %v not found in package %v", ref.Token, ref.Package))
return typ, diags, nil
}
}
return tokenType, diags, nil
case resourcesRef, providerRef:
typ, diags, err := t.bindResourceTypeDef(ref.Token, options)
diags = refDiags.Extend(diags)
if err != nil {
return nil, diags, err
}
if typ == nil {
typ, diags := invalidType(errorf(path, "resource type %v not found in package %v", ref.Token, ref.Package))
return typ, diags, nil
}
return typ, diags, nil
default:
typ, diags := invalidType(errorf(path, "failed to parse ref %s", spec.Ref))
return typ, diags, nil
}
}
func (t *types) bindTypeSpecOneOf(
path string,
spec TypeSpec,
inputShape bool,
options ValidationOptions,
) (Type, hcl.Diagnostics, error) {
var diags hcl.Diagnostics
if len(spec.OneOf) < 2 {
diags = diags.Append(errorf(path+"/oneOf", "oneOf should list at least two types"))
}
var defaultType Type
if spec.Type != "" {
dt, primDiags := t.bindPrimitiveType(path+"/type", spec.Type)
diags = diags.Extend(primDiags)
defaultType = dt
}
elements := make([]Type, len(spec.OneOf))
for i, spec := range spec.OneOf {
e, typDiags, err := t.bindTypeSpec(fmt.Sprintf("%s/oneOf/%v", path, i), spec, inputShape, options)
diags = diags.Extend(typDiags)
if err != nil {
return nil, diags, err
}
elements[i] = e
}
var discriminator string
var mapping map[string]string
if spec.Discriminator != nil {
if spec.Discriminator.PropertyName == "" {
diags = diags.Append(errorf(path, "discriminator must provide a property name"))
}
discriminator = spec.Discriminator.PropertyName
mapping = spec.Discriminator.Mapping
}
return t.newUnionType(elements, defaultType, discriminator, mapping), diags, nil
}
func (t *types) bindTypeSpec(
path string,
spec TypeSpec,
inputShape bool,
options ValidationOptions,
) (result Type, diags hcl.Diagnostics, err error) {
// NOTE: `spec.Plain` is the spec of the type, not to be confused with the
// `Plain` property of the underlying `Property`, which is passed as
// `plainProperty`.
if inputShape && !spec.Plain {
defer func() {
result = t.newInputType(result)
}()
}
if spec.Ref != "" {
return t.bindTypeSpecRef(path, spec, inputShape, options)
}
if spec.OneOf != nil {
return t.bindTypeSpecOneOf(path, spec, inputShape, options)
}
switch spec.Type {
case "boolean", "integer", "number", "string":
typ, typDiags := t.bindPrimitiveType(path+"/type", spec.Type)
diags = diags.Extend(typDiags)
return typ, diags, nil
case "array":
if spec.Items == nil {
diags = diags.Append(errorf(path, "missing \"items\" property in array type spec"))
typ, _ := invalidType(diags...)
return typ, diags, nil
}
elementType, elementDiags, err := t.bindTypeSpec(path+"/items", *spec.Items, inputShape, options)
diags = diags.Extend(elementDiags)
if err != nil {
return nil, diags, err
}
return t.newArrayType(elementType), diags, nil
case "object":
elementType, elementDiags, err := t.bindTypeSpec(path, TypeSpec{Type: "string"}, inputShape, options)
contract.Assertf(len(elementDiags) == 0, "unexpected diagnostics: %v", elementDiags)
contract.Assertf(err == nil, "error binding type spec")
if spec.AdditionalProperties != nil {
et, elementDiags, err := t.bindTypeSpec(
path+"/additionalProperties",
*spec.AdditionalProperties,
inputShape,
options,
)
diags = diags.Extend(elementDiags)
if err != nil {
return nil, diags, err
}
elementType = et
}
return t.newMapType(elementType), diags, nil
default:
diags = diags.Append(errorf(path+"/type", "unknown type kind %v", spec.Type))
typ, _ := invalidType(diags...)
return typ, diags, nil
}
}
func plainType(typ Type) Type {
for {
switch t := typ.(type) {
case *InputType:
typ = t.ElementType
case *OptionalType:
typ = t.ElementType
case *ObjectType:
if t.PlainShape == nil {
return t
}
typ = t.PlainShape
default:
return t
}
}
}
func bindConstValue(path, kind string, value interface{}, typ Type) (interface{}, hcl.Diagnostics) {
if value == nil {
return nil, nil
}
typeError := func(expectedType string) hcl.Diagnostics {
return hcl.Diagnostics{errorf(path, "invalid constant of type %T for %v %v", value, expectedType, kind)}
}
switch typ = plainType(typ); typ {
case BoolType:
v, ok := value.(bool)
if !ok {
return false, typeError("boolean")
}
return v, nil
case IntType:
v, ok := value.(int)
if !ok {
v, ok := value.(float64)
if !ok {
return 0, typeError("integer")
}
if math.Trunc(v) != v || v < math.MinInt32 || v > math.MaxInt32 {
return 0, typeError("integer")
}
return int32(v), nil
}
if v < math.MinInt32 || v > math.MaxInt32 {
return 0, typeError("integer")
}
//nolint:gosec // int -> int32 conversion is guarded above.
return int32(v), nil
case NumberType:
v, ok := value.(float64)
if !ok {
return 0.0, typeError("number")
}
return v, nil
case StringType:
v, ok := value.(string)
if !ok {
return 0.0, typeError("string")
}
return v, nil
default:
if _, isInvalid := typ.(*InvalidType); isInvalid {
return nil, nil
}
return nil, hcl.Diagnostics{errorf(path, "type %v cannot have a constant value; only booleans, integers, "+
"numbers and strings may have constant values", typ)}
}
}
func bindDefaultValue(path string, value interface{}, spec *DefaultSpec, typ Type) (*DefaultValue, hcl.Diagnostics) {
if value == nil && spec == nil {
return nil, nil
}
var diags hcl.Diagnostics
if value != nil {
typ = plainType(typ)
switch typ := typ.(type) {
case *UnionType:
if typ.DefaultType != nil {
return bindDefaultValue(path, value, spec, typ.DefaultType)
}
for _, elementType := range typ.ElementTypes {
v, diags := bindDefaultValue(path, value, spec, elementType)
if !diags.HasErrors() {
return v, diags
}
}
case *EnumType:
return bindDefaultValue(path, value, spec, typ.ElementType)
}
v, valueDiags := bindConstValue(path, "default", value, typ)
diags = diags.Extend(valueDiags)
value = v
}
dv := &DefaultValue{Value: value}
if spec != nil {
if len(spec.Environment) == 0 {
diags = diags.Append(errorf(path, "Default must specify an environment"))
}
dv.Environment, dv.Language = spec.Environment, makeLanguageMap(spec.Language)
}
return dv, diags
}
// bindProperties binds the map of property specs and list of required properties into a sorted list of properties and
// a lookup table.
func (t *types) bindProperties(path string, properties map[string]PropertySpec, requiredPath string, required []string,
inputShape bool, options ValidationOptions,
) ([]*Property, map[string]*Property, hcl.Diagnostics, error) {
var diags hcl.Diagnostics
for name := range properties {
if isReservedKeyword(name) {
diags = diags.Append(errorf(path+"/"+name, name+" is a reserved property name"))
}
}
if diags.HasErrors() {
return nil, nil, diags, errors.New("invalid property names")
}
// Bind property types and constant or default values.
propertyMap := map[string]*Property{}
result := slice.Prealloc[*Property](len(properties))
for name, spec := range properties {
propertyPath := path + "/" + name
// NOTE: The correct determination for if we should bind an input is:
//
// inputShape && !spec.Plain
//
// We will then be able to remove the markedPlain field of t.bindType
// since `arg(inputShape, t.bindType) <=> inputShape && !spec.Plain`.
// Unfortunately, this fix breaks backwards compatibility in a major
// way, across all providers.
typ, typDiags, err := t.bindTypeSpec(propertyPath, spec.TypeSpec, inputShape, options)
diags = diags.Extend(typDiags)
if err != nil {
return nil, nil, diags, fmt.Errorf("error binding type for property %q: %w", name, err)
}
cv, cvDiags := bindConstValue(propertyPath+"/const", "constant", spec.Const, typ)
diags = diags.Extend(cvDiags)
dv, dvDiags := bindDefaultValue(propertyPath+"/default", spec.Default, spec.DefaultInfo, typ)
diags = diags.Extend(dvDiags)
p := &Property{
Name: name,
Comment: spec.Description,
Type: t.newOptionalType(typ),
ConstValue: cv,
DefaultValue: dv,
DeprecationMessage: spec.DeprecationMessage,
Language: makeLanguageMap(spec.Language),
Secret: spec.Secret,
ReplaceOnChanges: spec.ReplaceOnChanges,
WillReplaceOnChanges: spec.WillReplaceOnChanges,
Plain: spec.Plain,
}
propertyMap[name], result = p, append(result, p)
}
// Compute required properties.
for i, name := range required {
p, ok := propertyMap[name]
if !ok {
diags = diags.Append(errorf(fmt.Sprintf("%s/%v", requiredPath, i), "unknown required property %q", name))
continue
}
if typ, ok := p.Type.(*OptionalType); ok {
p.Type = typ.ElementType
}
}
sort.Slice(result, func(i, j int) bool {
return result[i].Name < result[j].Name
})
return result, propertyMap, diags, nil
}
func (t *types) bindObjectTypeDetails(path string, obj *ObjectType, token string,
spec ObjectTypeSpec,
options ValidationOptions,
) (hcl.Diagnostics, error) {
var diags hcl.Diagnostics
if len(spec.Plain) > 0 {
diags = diags.Append(errorf(path+"/plain",
"plain has been removed; the property type must be marked as plain instead"))
}
properties, propertyMap, propertiesDiags, err := t.bindProperties(path+"/properties", spec.Properties,
path+"/required", spec.Required, false, options)
diags = diags.Extend(propertiesDiags)
if err != nil {
return diags, err
}
inputProperties, inputPropertyMap, inputPropertiesDiags, err := t.bindProperties(
path+"/properties", spec.Properties, path+"/required", spec.Required, true, options)
diags = diags.Extend(inputPropertiesDiags)
if err != nil {
return diags, err
}
language := makeLanguageMap(spec.Language)
obj.PackageReference = t.externalPackage()
obj.Token = token
obj.Comment = spec.Description
obj.Language = language
obj.Properties = properties
obj.properties = propertyMap
obj.IsOverlay = spec.IsOverlay
obj.OverlaySupportedLanguages = spec.OverlaySupportedLanguages
obj.InputShape.PackageReference = t.externalPackage()
obj.InputShape.Token = token
obj.InputShape.Comment = spec.Description
obj.InputShape.Language = language
obj.InputShape.Properties = inputProperties
obj.InputShape.properties = inputPropertyMap
return diags, nil
}
// bindAnonymousObjectType is used for binding object types that do not appear as part of a package's defined types.
// This includes state inputs for resources that have them and function inputs and outputs.
// Object types defined by a package are bound by bindTypeDef.
func (t *types) bindAnonymousObjectType(
path,
token string,
spec ObjectTypeSpec,
options ValidationOptions,
) (*ObjectType, hcl.Diagnostics, error) {
obj := &ObjectType{}
obj.InputShape = &ObjectType{PlainShape: obj}
obj.IsOverlay = spec.IsOverlay
obj.OverlaySupportedLanguages = spec.OverlaySupportedLanguages
diags, err := t.bindObjectTypeDetails(path, obj, token, spec, options)
if err != nil {
return nil, diags, err
}
return obj, diags, nil
}
func (t *types) bindEnumType(token string, spec ComplexTypeSpec) (*EnumType, hcl.Diagnostics) {
var diags hcl.Diagnostics
path := memberPath("types", token)
typ, typDiags := t.bindPrimitiveType(path+"/type", spec.Type)
diags = diags.Extend(typDiags)
switch typ {
case StringType, IntType, NumberType, BoolType:
// OK
default:
if _, isInvalid := typ.(*InvalidType); !isInvalid {
diags = diags.Append(errorf(path+"/type",
"enums may only be of type string, integer, number or boolean"))
}
}
values := make([]*Enum, len(spec.Enum))
for i, spec := range spec.Enum {
value, valueDiags := bindConstValue(fmt.Sprintf("%s/enum/%v/value", path, i), "enum", spec.Value, typ)
diags = diags.Extend(valueDiags)
values[i] = &Enum{
Value: value,
Comment: spec.Description,
Name: spec.Name,
DeprecationMessage: spec.DeprecationMessage,
}
}
return &EnumType{
PackageReference: t.externalPackage(),
Token: token,
Elements: values,
ElementType: typ,
Comment: spec.Description,
IsOverlay: spec.IsOverlay,
}, diags
}
func (t *types) finishTypes(tokens []string, options ValidationOptions) ([]Type, hcl.Diagnostics, error) {
var diags hcl.Diagnostics
// Ensure all of the types defined by the package are bound.
for _, token := range tokens {
_, typeDiags, err := t.bindTypeDef(token, options)
diags = diags.Extend(typeDiags)
if err != nil {
return nil, diags, fmt.Errorf("error binding type %v", token)
}
}
// Build the type list.
typeList := slice.Prealloc[Type](len(t.resources))
for _, t := range t.resources {
typeList = append(typeList, t)
}
for _, t := range t.typeDefs {
typeList = append(typeList, t)
if obj, ok := t.(*ObjectType); ok {
// t is a plain shape: add it and its corresponding input shape to the type list.
typeList = append(typeList, obj.InputShape)
}
}
for _, t := range t.arrays {
typeList = append(typeList, t)
}
for _, t := range t.maps {
typeList = append(typeList, t)
}
for _, t := range t.unions {
typeList = append(typeList, t)
}
for _, t := range t.tokens {
typeList = append(typeList, t)
}
sort.Slice(typeList, func(i, j int) bool {
return typeList[i].String() < typeList[j].String()
})
return typeList, diags, nil
}
func checkDuplicates(
resources map[string]ResourceSpec, functions map[string]FunctionSpec,
) hcl.Diagnostics {
type schemaPath = string
type token = string
names := make(map[token][]schemaPath, len(resources)+len(functions))
duplicates := map[token]struct{}{}
process := func(token token, schemaPath schemaPath) {
v := append(names[token], schemaPath)
names[token] = v
if len(v) > 1 {
duplicates[token] = struct{}{}
}
}
for r := range resources {
process(strings.ToLower(r), memberPath("resources", r))
}
for f := range functions {
process(strings.ToLower(f), memberPath("functions", f))
}
diags := slice.Prealloc[*hcl.Diagnostic](len(duplicates))
for _, dup := range sortedKeys(duplicates) {
paths := names[dup]
contract.Assertf(len(paths) > 1, "this should only include duplicates")
slices.Sort(paths)
others := make([]schemaPath, len(paths)-1)
copy(others, paths[1:])
for i, tk := range paths {
err := errorf(tk, "multiple tokens map to %s", dup)
err.Detail = "other paths(s) are " + strings.Join(others, ", ")
if i < len(others) {
others[i] = paths[i]
}
diags = append(diags, err)
}
}
return diags
}
func bindMethods(
path,
resourceToken string,
methods map[string]string,
types *types,
options ValidationOptions,
) ([]*Method, hcl.Diagnostics, error) {
var diags hcl.Diagnostics
names := slice.Prealloc[string](len(methods))
for name := range methods {
names = append(names, name)
}
sort.Strings(names)
result := slice.Prealloc[*Method](len(methods))
for _, name := range names {
token := methods[name]
methodPath := path + "/" + name
function, functionDiags, err := types.bindFunctionDef(token, options)
diags = diags.Extend(functionDiags)
if err != nil {
return nil, diags, err
}
if function == nil {
diags = diags.Append(errorf(methodPath, "unknown function %s", token))
continue
}
if function.IsMethod {
diags = diags.Append(errorf(methodPath, "function %s is already a method", token))
continue
}
idx := strings.LastIndex(function.Token, "/")
if idx == -1 || function.Token[:idx] != resourceToken {
d := errorf(methodPath, "invalid function token format %s", token)
d.Detail = fmt.Sprintf(`expected a token of the shape: "%s/<method name>"`, resourceToken)
diags = diags.Append(d)
continue
}
if function.Inputs == nil || function.Inputs.Properties == nil || len(function.Inputs.Properties) == 0 ||
function.Inputs.Properties[0].Name != "__self__" {
diags = diags.Append(errorf(methodPath, "function %s has no __self__ parameter", token))
continue
}
function.IsMethod = true
result = append(result, &Method{
Name: name,
Function: function,
})
}
return result, diags, nil
}
func bindParameterization(spec *ParameterizationSpec) (*Parameterization, hcl.Diagnostics) {
if spec == nil {
return nil, nil
}
if spec.BaseProvider.Name == "" {
return nil, hcl.Diagnostics{errorf(
"#/parameterization/baseProvider/name",
"provider name must be specified")}
}
ver, err := semver.Parse(spec.BaseProvider.Version)
if err != nil {
return nil, hcl.Diagnostics{errorf(
"#/parameterization/baseProvider/version",
"invalid version %q: %v", spec.BaseProvider.Version, err)}
}
return &Parameterization{
BaseProvider: BaseProvider{
Name: spec.BaseProvider.Name,
Version: ver,
},
Parameter: spec.Parameter,
}, nil
}
func bindConfig(spec ConfigSpec, types *types, options ValidationOptions) ([]*Property, hcl.Diagnostics, error) {
properties, _, diags, err := types.bindProperties("#/config/variables", spec.Variables,
"#/config/defaults", spec.Required, false, options)
return properties, diags, err
}
func (t *types) bindResourceDef(
token string,
options ValidationOptions,
) (res *Resource, diags hcl.Diagnostics, err error) {
if res, ok := t.resourceDefs[token]; ok {
return res, nil, nil
}
// Declare the resource.
res = &Resource{}
if token == "pulumi:providers:"+t.pkg.Name {
t.resourceDefs[token] = res
diags, err = t.bindProvider(res, options)
} else {
spec, ok, specErr := t.spec.GetResourceSpec(token)
if specErr != nil || !ok {
return nil, nil, err
}
t.resourceDefs[token] = res
path := memberPath("resources", token)
parts := strings.Split(token, ":")
if len(parts) == 3 {
name := parts[2]
if isReservedKeyword(name) {
diags = diags.Append(errorf(path, name+" is a reserved name, cannot name resource"))
}
}
var rDiags hcl.Diagnostics
rDiags, err = t.bindResourceDetails(path, token, spec, res, options)
diags = append(diags, rDiags...)
}
if err != nil {
return nil, diags, err
}
return res, diags, nil
}
func (t *types) bindResourceDetails(
path,
token string,
spec ResourceSpec,
decl *Resource, options ValidationOptions,
) (hcl.Diagnostics, error) {
var diags hcl.Diagnostics
if len(spec.Plain) > 0 {
diags = diags.Append(errorf(path+"/plain", "plain has been removed; property types must be marked as plain instead"))
}
if len(spec.PlainInputs) > 0 {
diags = diags.Append(errorf(path+"/plainInputs",
"plainInputs has been removed; individual property types must be marked as plain instead"))
}
properties, _, propertyDiags, err := t.bindProperties(path+"/properties", spec.Properties,
path+"/required", spec.Required, false, options)
diags = diags.Extend(propertyDiags)
if err != nil {
return diags, fmt.Errorf("failed to bind properties for %v: %w", token, err)
}
// emit a warning if either of these are used
for _, property := range properties {
if isReservedComponentResourcePropertyKey(property.Name) {
warnPath := path + "/properties/" + property.Name
diags = diags.Append(warningf(warnPath, property.Name+" is a reserved property name"))
}
if !spec.IsComponent && isReservedCustomResourcePropertyKey(property.Name) {
warnPath := path + "/properties/" + property.Name
diags = diags.Append(warningf(warnPath, property.Name+" is a reserved property name for resources"))
}
}
inputProperties, _, inputDiags, err := t.bindProperties(path+"/inputProperties", spec.InputProperties,
path+"/requiredInputs", spec.RequiredInputs, true, options)
diags = diags.Extend(inputDiags)
if err != nil {
return diags, fmt.Errorf("failed to bind input properties for %v: %w", token, err)
}
methods, methodDiags, err := bindMethods(path+"/methods", token, spec.Methods, t, options)
diags = diags.Extend(methodDiags)
if err != nil {
return diags, fmt.Errorf("failed to bind methods for %v: %w", token, err)
}
for _, method := range methods {
if _, ok := spec.Properties[method.Name]; ok {
diags = diags.Append(errorf(path+"/methods/"+method.Name, "%v already has a property named %s", token, method.Name))
}
}
var stateInputs *ObjectType
if spec.StateInputs != nil {
si, stateDiags, err := t.bindAnonymousObjectType(path+"/stateInputs", token+"Args", *spec.StateInputs, options)
diags = diags.Extend(stateDiags)
if err != nil {
return diags, fmt.Errorf("error binding inputs for %v: %w", token, err)
}
stateInputs = si.InputShape
}
aliases := slice.Prealloc[*Alias](len(spec.Aliases))
for _, a := range spec.Aliases {
aliases = append(aliases, &Alias{compatibility: a.compatibility, Type: a.Type})
}
*decl = Resource{
PackageReference: t.externalPackage(),
Token: token,
Comment: spec.Description,
InputProperties: inputProperties,
Properties: properties,
StateInputs: stateInputs,
Aliases: aliases,
DeprecationMessage: spec.DeprecationMessage,
Language: makeLanguageMap(spec.Language),
IsComponent: spec.IsComponent,
Methods: methods,
IsOverlay: spec.IsOverlay,
OverlaySupportedLanguages: spec.OverlaySupportedLanguages,
}
return diags, nil
}
func (t *types) bindProvider(decl *Resource, options ValidationOptions) (hcl.Diagnostics, error) {
spec, ok, err := t.spec.GetResourceSpec("pulumi:providers:" + t.pkg.Name)
if err != nil {
return nil, err
}
contract.Assertf(ok, "provider resource %q not found", t.pkg.Name)
diags, err := t.bindResourceDetails("#/provider", "pulumi:providers:"+t.pkg.Name, spec, decl, options)
if err != nil {
return diags, err
}
decl.IsProvider = true
// If any input property is called "version" or "pulumi" error that it's reserved.
for _, property := range decl.InputProperties {
if isReservedProviderPropertyName(property.Name) {
path := "#/provider/properties/" + property.Name
diags = diags.Append(errorf(path, property.Name+" is a reserved provider input property name"))
}
if diags.HasErrors() {
return diags, errors.New("invalid property names")
}
}
// Since non-primitive provider configuration is currently JSON serialized, we can't handle it without
// modifying the path by which it's looked up. As a temporary workaround to enable access to config which
// values which are primitives, we'll simply remove any properties for the provider resource which are not
// strings, or types with an underlying type of string, before we generate the provider code.
stringProperties := slice.Prealloc[*Property](len(decl.Properties))
for _, prop := range decl.Properties {
typ := plainType(prop.Type)
if tokenType, isTokenType := typ.(*TokenType); isTokenType {
if tokenType.UnderlyingType != stringType {
continue
}
} else {
if typ != stringType {
continue
}
}
stringProperties = append(stringProperties, prop)
}
decl.Properties = stringProperties
return diags, nil
}
func (t *types) finishResources(
tokens []string,
options ValidationOptions,
) (*Resource, []*Resource, hcl.Diagnostics, error) {
var diags hcl.Diagnostics
provider, provDiags, err := t.bindResourceTypeDef("pulumi:providers:"+t.pkg.Name, options)
diags = diags.Extend(provDiags)
if err != nil {
return nil, nil, diags, fmt.Errorf("error binding provider: %w", err)
}
resources := slice.Prealloc[*Resource](len(tokens))
for _, token := range tokens {
res, resDiags, err := t.bindResourceTypeDef(token, options)
diags = diags.Extend(resDiags)
if err != nil {
return nil, nil, diags, fmt.Errorf("error binding resource %v: %w", token, err)
}
resources = append(resources, res.Resource)
}
sort.Slice(resources, func(i, j int) bool {
return resources[i].Token < resources[j].Token
})
return provider.Resource, resources, diags, nil
}
func (t *types) bindFunctionDef(token string, options ValidationOptions) (*Function, hcl.Diagnostics, error) {
if fn, ok := t.functionDefs[token]; ok {
return fn, nil, nil
}
spec, ok, err := t.spec.GetFunctionSpec(token)
if err != nil || !ok {
return nil, nil, nil
}
var diags hcl.Diagnostics
path := memberPath("functions", token)
parts := strings.Split(token, ":")
if len(parts) == 3 {
name := parts[2]
if isReservedKeyword(name) {
diags = diags.Append(errorf(path, name+" is a reserved name, cannot name function"))
return nil, diags, errors.New(name + " is a reserved name, cannot name function")
}
}
// Check that spec.MultiArgumentInputs => spec.Inputs
if len(spec.MultiArgumentInputs) > 0 && spec.Inputs == nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "cannot specify multi-argument inputs without specifying inputs",
})
}
var inputs *ObjectType
if spec.Inputs != nil {
ins, inDiags, err := t.bindAnonymousObjectType(path+"/inputs", token+"Args", *spec.Inputs, options)
diags = diags.Extend(inDiags)
if err != nil {
return nil, diags, fmt.Errorf("error binding inputs for function %v: %w", token, err)
}
if len(spec.MultiArgumentInputs) > 0 {
idx := make(map[string]int, len(spec.MultiArgumentInputs))
for i, k := range spec.MultiArgumentInputs {
idx[k] = i
}
// Check that MultiArgumentInputs matches up 1:1 with the input properties
for k, i := range idx {
if _, ok := spec.Inputs.Properties[k]; !ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("multiArgumentInputs[%d] refers to non-existent property %#v", i, k),
})
}
}
var detailGiven bool
for k := range spec.Inputs.Properties {
if _, ok := idx[k]; !ok {
diag := hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Property %#v not specified by multiArgumentInputs", k),
}
if !detailGiven {
detailGiven = true
diag.Detail = "If multiArgumentInputs is given, all properties must be specified"
}
diags = diags.Append(&diag)
}
}
// Order output properties as specified by MultiArgumentInputs
sortProps := func(props []*Property) {
sort.Slice(props, func(i, j int) bool {
return idx[props[i].Name] < idx[props[j].Name]
})
}
sortProps(ins.Properties)
if ins.InputShape != nil {
sortProps(ins.InputShape.Properties)
}
if ins.PlainShape != nil {
sortProps(ins.PlainShape.Properties)
}
}
inputs = ins
}
var outputs *ObjectType
var inlineObjectAsReturnType bool
var returnType Type
var returnTypePlain bool
if spec.ReturnType != nil && spec.Outputs == nil {
// compute the return type from the spec
if spec.ReturnType.ObjectTypeSpec != nil {
// bind as an object type
outs, outDiags, err := t.bindAnonymousObjectType(
path+"/outputs",
token+"Result",
*spec.ReturnType.ObjectTypeSpec,
options,
)
diags = diags.Extend(outDiags)
if err != nil {
return nil, diags, fmt.Errorf("error binding outputs for function %v: %w", token, err)
}
returnType = outs
outputs = outs
inlineObjectAsReturnType = true
returnTypePlain = spec.ReturnType.ObjectTypeSpecIsPlain
} else if spec.ReturnType.TypeSpec != nil {
out, outDiags, err := t.bindTypeSpec(path+"/outputs", *spec.ReturnType.TypeSpec, false, options)
diags = diags.Extend(outDiags)
if err != nil {
return nil, diags, fmt.Errorf("error binding outputs for function %v: %w", token, err)
}
returnType = out
returnTypePlain = spec.ReturnType.TypeSpec.Plain
} else {
// Setting `spec.ReturnType` to a value without setting either `TypeSpec` or `ObjectTypeSpec`
// indicates a logical bug in our marshaling code.
return nil, diags, fmt.Errorf("error binding outputs for function %v: invalid return type", token)
}
} else if spec.Outputs != nil {
// bind the outputs when the specs don't rely on the new ReturnType field
outs, outDiags, err := t.bindAnonymousObjectType(path+"/outputs", token+"Result", *spec.Outputs, options)
diags = diags.Extend(outDiags)
if err != nil {
return nil, diags, fmt.Errorf("error binding outputs for function %v: %w", token, err)
}
outputs = outs
returnType = outs
inlineObjectAsReturnType = true
}
if inputs != nil {
for _, input := range inputs.Properties {
if input == nil {
continue
}
if isReservedKeyword(input.Name) {
diags = diags.Append(errorf(path+"/inputs/"+input.Name, input.Name+" is a reserved input name"))
return nil, diags, errors.New("input name " + input.Name + " is reserved")
}
}
}
fn := &Function{
PackageReference: t.externalPackage(),
Token: token,
Comment: spec.Description,
Inputs: inputs,
MultiArgumentInputs: len(spec.MultiArgumentInputs) > 0,
InlineObjectAsReturnType: inlineObjectAsReturnType,
Outputs: outputs,
ReturnType: returnType,
ReturnTypePlain: returnTypePlain,
DeprecationMessage: spec.DeprecationMessage,
Language: makeLanguageMap(spec.Language),
IsOverlay: spec.IsOverlay,
OverlaySupportedLanguages: spec.OverlaySupportedLanguages,
}
t.functionDefs[token] = fn
return fn, diags, nil
}
func (t *types) finishFunctions(tokens []string, options ValidationOptions) ([]*Function, hcl.Diagnostics, error) {
var diags hcl.Diagnostics
functions := slice.Prealloc[*Function](len(tokens))
for _, token := range tokens {
f, fdiags, err := t.bindFunctionDef(token, options)
diags = diags.Extend(fdiags)
if err != nil {
return nil, diags, fmt.Errorf("error binding function %v: %w", token, err)
}
functions = append(functions, f)
}
sort.Slice(functions, func(i, j int) bool {
return functions[i].Token < functions[j].Token
})
return functions, diags, nil
}
// makeLanguageMap converts a map[string]RawMessage as found on serializable
// spec object into map[string]interface{} (using json.RawMessage for the
// values) for use in schema types. If the passed in map is empty (or nil), we
// return a nil map instead of an empty map to save memory.
func makeLanguageMap(raw map[string]RawMessage) map[string]interface{} {
var language map[string]interface{}
if len(raw) > 0 {
language = make(map[string]interface{})
for name, raw := range raw {
language[name] = json.RawMessage(raw)
}
}
return language
}
// Copyright 2020-2024, 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 schema
import (
"bytes"
"io"
"unicode"
"unicode/utf8"
"github.com/pgavlin/goldmark"
"github.com/pgavlin/goldmark/ast"
"github.com/pgavlin/goldmark/parser"
"github.com/pgavlin/goldmark/text"
"github.com/pgavlin/goldmark/util"
)
const (
// ExamplesShortcode is the name for the `{{% examples %}}` shortcode, which demarcates a set of example sections.
ExamplesShortcode = "examples"
// ExampleShortcode is the name for the `{{% example %}}` shortcode, which demarcates the content for a single
// example.
ExampleShortcode = "example"
)
// Shortcode represents a shortcode element and its contents, e.g. `{{% examples %}}`.
type Shortcode struct {
ast.BaseBlock
// Name is the name of the shortcode.
Name []byte
}
func (s *Shortcode) Dump(w io.Writer, source []byte, level int) {
m := map[string]string{
"Name": string(s.Name),
}
ast.DumpHelper(w, s, source, level, m, nil)
}
// KindShortcode is an ast.NodeKind for the Shortcode node.
var KindShortcode = ast.NewNodeKind("Shortcode")
// Kind implements ast.Node.Kind.
func (*Shortcode) Kind() ast.NodeKind {
return KindShortcode
}
// NewShortcode creates a new shortcode with the given name.
func NewShortcode(name []byte) *Shortcode {
return &Shortcode{Name: name}
}
type shortcodeParser int
// NewShortcodeParser returns a BlockParser that parses shortcode (e.g. `{{% examples %}}`).
func NewShortcodeParser() parser.BlockParser {
return shortcodeParser(0)
}
func (shortcodeParser) Trigger() []byte {
return []byte{'{'}
}
func (shortcodeParser) parseShortcode(line []byte, pos int) (int, int, int, bool, bool) {
// Look for `{{%` to open the shortcode.
text := line[pos:]
if len(text) < 3 || text[0] != '{' || text[1] != '{' || text[2] != '%' {
return 0, 0, 0, false, false
}
text, pos = text[3:], pos+3
// Scan through whitespace.
for {
if len(text) == 0 {
return 0, 0, 0, false, false
}
r, sz := utf8.DecodeRune(text)
if !unicode.IsSpace(r) {
break
}
text, pos = text[sz:], pos+sz
}
// Check for a '/' to indicate that this is a closing shortcode.
isClose := false
if text[0] == '/' {
isClose = true
text, pos = text[1:], pos+1
}
// Find the end of the name and the closing delimiter (`%}}`) for this shortcode.
nameStart, nameEnd, inName := pos, pos, true
for {
if len(text) == 0 {
return 0, 0, 0, false, false
}
if len(text) >= 3 && text[0] == '%' && text[1] == '}' && text[2] == '}' {
if inName {
nameEnd = pos
}
pos = pos + 3
// We don't need to update text
// because we return after this break.
break
}
r, sz := utf8.DecodeRune(text)
if inName && unicode.IsSpace(r) {
nameEnd, inName = pos, false
}
text, pos = text[sz:], pos+sz
}
return nameStart, nameEnd, pos, isClose, true
}
func (p shortcodeParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) {
line, _ := reader.PeekLine()
pos := pc.BlockOffset()
if pos < 0 {
return nil, parser.NoChildren
}
nameStart, nameEnd, shortcodeEnd, isClose, ok := p.parseShortcode(line, pos)
if !ok || isClose {
return nil, parser.NoChildren
}
name := line[nameStart:nameEnd]
reader.Advance(shortcodeEnd)
return NewShortcode(name), parser.HasChildren
}
func (p shortcodeParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State {
line, seg := reader.PeekLine()
pos := pc.BlockOffset()
if pos < 0 {
return parser.Continue | parser.HasChildren
} else if pos > seg.Len() {
return parser.Continue | parser.HasChildren
}
nameStart, nameEnd, shortcodeEnd, isClose, ok := p.parseShortcode(line, pos)
if !ok || !isClose {
return parser.Continue | parser.HasChildren
}
shortcode := node.(*Shortcode)
if !bytes.Equal(line[nameStart:nameEnd], shortcode.Name) {
return parser.Continue | parser.HasChildren
}
reader.Advance(shortcodeEnd)
return parser.Close
}
func (shortcodeParser) Close(node ast.Node, reader text.Reader, pc parser.Context) {
}
// CanInterruptParagraph returns true for shortcodes.
func (shortcodeParser) CanInterruptParagraph() bool {
return true
}
// CanAcceptIndentedLine returns false for shortcodes; all shortcodes must start at the first column.
func (shortcodeParser) CanAcceptIndentedLine() bool {
return false
}
// ParseDocs parses the given documentation text as Markdown with shortcodes and returns the AST.
func ParseDocs(docs []byte) ast.Node {
p := goldmark.DefaultParser()
p.AddOptions(parser.WithBlockParsers(util.Prioritized(shortcodeParser(0), 50)))
return p.Parse(text.NewReader(docs))
}
// Copyright 2020-2024, 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 schema
import (
"bytes"
"fmt"
"io"
"net/url"
"github.com/pgavlin/goldmark/ast"
"github.com/pgavlin/goldmark/renderer"
"github.com/pgavlin/goldmark/renderer/markdown"
"github.com/pgavlin/goldmark/util"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
// A RendererOption controls the behavior of a Renderer.
type RendererOption func(*Renderer)
// A ReferenceRenderer is responsible for rendering references to entities in a schema.
type ReferenceRenderer func(r *Renderer, w io.Writer, source []byte, link *ast.Link, enter bool) (ast.WalkStatus, error)
// WithReferenceRenderer sets the reference renderer for a renderer.
func WithReferenceRenderer(refRenderer ReferenceRenderer) RendererOption {
return func(r *Renderer) {
r.refRenderer = refRenderer
}
}
// A Renderer provides the ability to render parsed documentation back to Markdown source.
type Renderer struct {
md *markdown.Renderer
refRenderer ReferenceRenderer
}
// MarkdownRenderer returns the underlying Markdown renderer used by the Renderer.
func (r *Renderer) MarkdownRenderer() *markdown.Renderer {
return r.md
}
func (r *Renderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
// blocks
reg.Register(KindShortcode, r.renderShortcode)
// inlines
reg.Register(ast.KindLink, r.renderLink)
}
func (r *Renderer) renderShortcode(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if enter {
if err := r.md.OpenBlock(w, source, node); err != nil {
return ast.WalkStop, err
}
if _, err := fmt.Fprintf(r.md.Writer(w), "{{%% %s %%}}\n", string(node.(*Shortcode).Name)); err != nil {
return ast.WalkStop, err
}
} else {
if _, err := fmt.Fprintf(r.md.Writer(w), "{{%% /%s %%}}\n", string(node.(*Shortcode).Name)); err != nil {
return ast.WalkStop, err
}
if err := r.md.CloseBlock(w); err != nil {
return ast.WalkStop, err
}
}
return ast.WalkContinue, nil
}
func isEntityReference(dest []byte) bool {
if len(dest) == 0 {
return false
}
parsed, err := url.Parse(string(dest))
if err != nil {
return false
}
if parsed.IsAbs() {
return parsed.Scheme == "schema"
}
return parsed.Host == "" && parsed.Path == "" && parsed.RawQuery == "" && parsed.Fragment != ""
}
func (r *Renderer) renderLink(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
// If this is an entity reference, pass it off to the reference renderer (if any).
link := node.(*ast.Link)
if r.refRenderer != nil && isEntityReference(link.Destination) {
return r.refRenderer(r, w, source, link, enter)
}
return r.md.RenderLink(w, source, node, enter)
}
// RenderDocs renders parsed documentation to the given Writer. The source that was used to parse the documentation
// must be provided.
func RenderDocs(w io.Writer, source []byte, node ast.Node, options ...RendererOption) error {
md := &markdown.Renderer{}
dr := &Renderer{md: md}
for _, o := range options {
o(dr)
}
nodeRenderers := []util.PrioritizedValue{
util.Prioritized(dr, 100),
util.Prioritized(md, 200),
}
r := renderer.NewRenderer(renderer.WithNodeRenderers(nodeRenderers...))
return r.Render(w, source, node)
}
// RenderDocsToString is like RenderDocs, but renders to a string instead of a Writer.
func RenderDocsToString(source []byte, node ast.Node, options ...RendererOption) string {
var buf bytes.Buffer
err := RenderDocs(&buf, source, node, options...)
contract.AssertNoErrorf(err, "error rendering docs")
return buf.String()
}
// Copyright 2016-2022, 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 schema
import (
"bytes"
"context"
"errors"
"fmt"
"github.com/natefinch/atomic"
"github.com/blang/semver"
"github.com/segmentio/encoding/json"
pkgWorkspace "github.com/pulumi/pulumi/pkg/v3/workspace"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/env"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)
// ParameterizationDescriptor is the serializable description of a dependency's parameterization.
type ParameterizationDescriptor struct {
// Name is the name of the package.
Name string `json:"name" yaml:"name"`
// Version is the version of the package.
Version semver.Version `json:"version" yaml:"version"`
// Value is the parameter value of the package.
Value []byte `json:"value" yaml:"value"`
}
// PackageDescriptor is a descriptor for a package, this is similar to a plugin spec but also contains parameterization
// info.
type PackageDescriptor struct {
// Name is the simple name of the plugin.
Name string `json:"name" yaml:"name"`
// Version is the optional version of the plugin.
Version *semver.Version `json:"version,omitempty" yaml:"version,omitempty"`
// DownloadURL is the optional URL to use when downloading the provider plugin binary.
DownloadURL string `json:"downloadURL,omitempty" yaml:"downloadURL,omitempty"`
// Parameterization is the optional parameterization of the package.
Parameterization *ParameterizationDescriptor `json:"parameterization,omitempty" yaml:"parameterization,omitempty"`
}
// PackageName returns the name of the package.
func (pd PackageDescriptor) PackageName() string {
if pd.Parameterization != nil {
return pd.Parameterization.Name
}
return pd.Name
}
// PackageVersion returns the version of the package.
func (pd PackageDescriptor) PackageVersion() *semver.Version {
if pd.Parameterization != nil {
return &pd.Parameterization.Version
}
return pd.Version
}
func (pd *PackageDescriptor) String() string {
version := "nil"
if pd.Version != nil {
version = pd.Version.String()
}
// If the package descriptor has a parameterization, write that information out first.
if pd.Parameterization != nil {
return fmt.Sprintf("%s@%s (%s@%s)", pd.Parameterization.Name, pd.Parameterization.Version, pd.Name, version)
}
return fmt.Sprintf("%s@%s", pd.Name, version)
}
type Loader interface {
// Deprecated: use LoadPackageV2
LoadPackage(pkg string, version *semver.Version) (*Package, error)
LoadPackageV2(ctx context.Context, descriptor *PackageDescriptor) (*Package, error)
}
type ReferenceLoader interface {
Loader
// Deprecated: use LoadPackageReferenceV2
LoadPackageReference(pkg string, version *semver.Version) (PackageReference, error)
LoadPackageReferenceV2(ctx context.Context, descriptor *PackageDescriptor) (PackageReference, error)
}
type pluginLoader struct {
host plugin.Host
cacheOptions pluginLoaderCacheOptions
}
// Caching options intended for benchmarking or debugging:
type pluginLoaderCacheOptions struct {
// useEntriesCache enables in-memory re-use of packages
disableEntryCache bool
// useFileCache enables skipping plugin loading when possible and caching JSON schemas to files
disableFileCache bool
// useMmap enables the use of memory mapped IO to avoid copying the JSON schema
disableMmap bool
}
func NewPluginLoader(host plugin.Host) ReferenceLoader {
return newPluginLoaderWithOptions(host, pluginLoaderCacheOptions{})
}
func newPluginLoaderWithOptions(host plugin.Host, cacheOptions pluginLoaderCacheOptions) ReferenceLoader {
var l ReferenceLoader
l = &pluginLoader{
host: host,
cacheOptions: cacheOptions,
}
if !cacheOptions.disableEntryCache {
l = NewCachedLoader(l)
}
return l
}
func (l *pluginLoader) LoadPackage(pkg string, version *semver.Version) (*Package, error) {
ref, err := l.LoadPackageReference(pkg, version)
if err != nil {
return nil, err
}
return ref.Definition()
}
func (l *pluginLoader) LoadPackageV2(ctx context.Context, descriptor *PackageDescriptor) (*Package, error) {
ref, err := l.LoadPackageReferenceV2(ctx, descriptor)
if err != nil {
return nil, err
}
return ref.Definition()
}
var ErrGetSchemaNotImplemented = getSchemaNotImplemented{}
type getSchemaNotImplemented struct{}
func (f getSchemaNotImplemented) Error() string {
return "it looks like GetSchema is not implemented"
}
func schemaIsEmpty(schemaBytes []byte) bool {
// A non-empty schema is any that contains non-whitespace, non brace characters.
//
// Some providers implemented GetSchema initially by returning text matching the regular
// expression: "\s*\{\s*\}\s*". This handles those cases while not strictly checking that braces
// match or reading the whole document.
for _, v := range schemaBytes {
if v != ' ' && v != '\t' && v != '\r' && v != '\n' && v != '{' && v != '}' {
return false
}
}
return true
}
func (l *pluginLoader) LoadPackageReference(pkg string, version *semver.Version) (PackageReference, error) {
return l.LoadPackageReferenceV2(
context.TODO(),
&PackageDescriptor{
Name: pkg,
Version: version,
})
}
func (l *pluginLoader) LoadPackageReferenceV2(
ctx context.Context, descriptor *PackageDescriptor,
) (PackageReference, error) {
if descriptor.Name == "pulumi" {
return DefaultPulumiPackage.Reference(), nil
}
schemaBytes, pluginVersion, err := l.loadSchemaBytes(ctx, descriptor)
if err != nil {
return nil, err
}
if schemaIsEmpty(schemaBytes) {
return nil, getSchemaNotImplemented{}
}
var spec PartialPackageSpec
if _, err := json.Parse(schemaBytes, &spec, json.ZeroCopy); err != nil {
return nil, err
}
// If the spec we've loaded doesn't specify a version, and we've got a plugin version to hand, we'll add that plugin
// version to the loaded schema. Note that in the case of parameterized providers and their schema, plugin and package
// version need not (and in general, won't) match -- if we were using version 0.8.0 of the Terraform provider to
// bridge some package foo/bar@v0.1.0, for instance, we'd have a plugin version of 0.8.0 and a package version of
// 0.1.0. We thus guard against this case, though in theory this is unnecessary -- schema versions are required for
// parameterized providers, so we should expect not to hit this case and overwrite a (parameterized) package version
// with an almost certainly different plugin version.
if pluginVersion != nil && descriptor.Parameterization == nil && spec.Version == "" {
spec.Version = pluginVersion.String()
}
p, err := ImportPartialSpec(spec, nil, l)
if err != nil {
return nil, err
}
return p, nil
}
// LoadPackageReference loads a package reference for the given pkg+version using the
// given loader.
//
// Deprecated: use LoadPackageReferenceV2
func LoadPackageReference(loader Loader, pkg string, version *semver.Version) (PackageReference, error) {
return LoadPackageReferenceV2(
context.TODO(),
loader,
&PackageDescriptor{
Name: pkg,
Version: version,
})
}
// LoadPackageReferenceV2 loads a package reference for the given descriptor using the given loader. When a reference is
// loaded, the name and version of the reference are compared to the requested name and version. If the name or version
// do not match, a PackageReferenceNameMismatchError or PackageReferenceVersionMismatchError is returned, respectively.
//
// In the event that a mismatch error is returned, the reference is still returned. This is to allow for the caller to
// decide whether or not the mismatch impacts their use of the reference.
func LoadPackageReferenceV2(
ctx context.Context, loader Loader, descriptor *PackageDescriptor,
) (PackageReference, error) {
var ref PackageReference
var err error
if refLoader, ok := loader.(ReferenceLoader); ok {
ref, err = refLoader.LoadPackageReferenceV2(ctx, descriptor)
} else {
p, pErr := loader.LoadPackageV2(ctx, descriptor)
err = pErr
if err == nil {
ref = p.Reference()
}
}
if err != nil {
return nil, err
}
name := descriptor.Name
if descriptor.Parameterization != nil {
name = descriptor.Parameterization.Name
}
version := descriptor.Version
if descriptor.Parameterization != nil {
version = &descriptor.Parameterization.Version
}
if name != ref.Name() {
return ref, &PackageReferenceNameMismatchError{
RequestedName: name,
RequestedVersion: version,
LoadedName: ref.Name(),
LoadedVersion: ref.Version(),
}
}
if version != nil && ref.Version() != nil && !ref.Version().Equals(*version) {
err := &PackageReferenceVersionMismatchError{
RequestedName: name,
RequestedVersion: version,
LoadedName: ref.Name(),
LoadedVersion: ref.Version(),
}
if l, ok := loader.(*cachedLoader); ok {
err.Message = fmt.Sprintf("entries: %v", l.entries)
}
return ref, err
}
return ref, nil
}
// PackageReferenceNameMismatchError is the type of errors returned by LoadPackageReferenceV2 when the name of the
// loaded reference does not match the requested name.
type PackageReferenceNameMismatchError struct {
// The requested . name
RequestedName string
// The requested version.
RequestedVersion *semver.Version
// The loaded name.
LoadedName string
// The loaded version.
LoadedVersion *semver.Version
// An optional message to be appended to the error's string representation.
Message string
}
func (e *PackageReferenceNameMismatchError) Error() string {
if e.Message == "" {
return fmt.Sprintf(
"loader returned %s@%v; requested %s@%v",
e.LoadedName, e.LoadedVersion,
e.RequestedName, e.RequestedVersion,
)
}
return fmt.Sprintf(
"loader returned %s@%v; requested %s@%v (%s)",
e.LoadedName, e.LoadedVersion,
e.RequestedName, e.RequestedVersion,
e.Message,
)
}
// PackageReferenceVersionMismatchError is the type of errors returned by LoadPackageReferenceV2 when the version of the
// loaded reference does not match the requested version.
type PackageReferenceVersionMismatchError struct {
// The requested name.
RequestedName string
// The requested version.
RequestedVersion *semver.Version
// The loaded name.
LoadedName string
// The loaded version.
LoadedVersion *semver.Version
// An optional message to be appended to the error's string representation.
Message string
}
func (e *PackageReferenceVersionMismatchError) Error() string {
if e.Message == "" {
return fmt.Sprintf(
"loader returned %s@%v; requested %s@%v",
e.LoadedName, e.LoadedVersion,
e.RequestedName, e.RequestedVersion,
)
}
return fmt.Sprintf(
"loader returned %s@%v; requested %s@%v (%s)",
e.LoadedName, e.LoadedVersion,
e.RequestedName, e.RequestedVersion,
e.Message,
)
}
func pluginSpecFromPackageDescriptor(descriptor *PackageDescriptor) workspace.PluginSpec {
return workspace.PluginSpec{
Name: descriptor.Name,
Version: descriptor.Version,
PluginDownloadURL: descriptor.DownloadURL,
Kind: apitype.ResourcePlugin,
}
}
// loadSchemaBytes loads the byte representation of the schema for the given package descriptor. Additionally, when
// successful, it returns the version of the underlying *plugin* that provided that schema (not to be confused with the
// version of the package included in the schema itself).
func (l *pluginLoader) loadSchemaBytes(
ctx context.Context, descriptor *PackageDescriptor,
) ([]byte, *semver.Version, error) {
attachPort, err := plugin.GetProviderAttachPort(tokens.Package(descriptor.Name))
if err != nil {
return nil, nil, err
}
// If PULUMI_DEBUG_PROVIDERS requested an attach port, skip caching and workspace
// interaction and load the schema directly from the given port.
if attachPort != nil {
schemaBytes, provider, err := l.loadPluginSchemaBytes(ctx, descriptor)
if err != nil {
return nil, nil, fmt.Errorf("Error loading schema from plugin: %w", err)
}
pluginVersion := descriptor.Version
if pluginVersion == nil {
info, err := provider.GetPluginInfo(ctx)
contract.IgnoreError(err) // nonfatal error
pluginVersion = info.Version
}
return schemaBytes, pluginVersion, nil
}
pluginInfo, err := l.host.ResolvePlugin(pluginSpecFromPackageDescriptor(descriptor))
if err != nil {
// Try and install the plugin if it was missing and try again, unless auto plugin installs are turned off.
var missingError *workspace.MissingError
if !errors.As(err, &missingError) || env.DisableAutomaticPluginAcquisition.Value() {
return nil, nil, err
}
spec := workspace.PluginSpec{
Kind: apitype.ResourcePlugin,
Name: descriptor.Name,
Version: descriptor.Version,
PluginDownloadURL: descriptor.DownloadURL,
}
log := func(sev diag.Severity, msg string) {
l.host.Log(sev, "", msg, 0)
}
_, err = pkgWorkspace.InstallPlugin(ctx, spec, log)
if err != nil {
return nil, nil, err
}
pluginInfo, err = l.host.ResolvePlugin(pluginSpecFromPackageDescriptor(descriptor))
if err != nil {
return nil, descriptor.Version, err
}
}
contract.Assertf(pluginInfo != nil, "loading pkg %q: pluginInfo was unexpectedly nil", descriptor.Name)
pluginVersion := descriptor.Version
if pluginVersion == nil {
pluginVersion = pluginInfo.Version
}
canCache := pluginInfo.SchemaPath != "" && pluginVersion != nil && descriptor.Parameterization == nil
if canCache {
schemaBytes, ok := l.loadCachedSchemaBytes(descriptor.Name, pluginInfo.SchemaPath, pluginInfo.SchemaTime)
if ok {
return schemaBytes, nil, nil
}
}
schemaBytes, provider, err := l.loadPluginSchemaBytes(ctx, descriptor)
if err != nil {
return nil, nil, fmt.Errorf("Error loading schema from plugin: %w", err)
}
if canCache {
err = atomic.WriteFile(pluginInfo.SchemaPath, bytes.NewReader(schemaBytes))
if err != nil {
return nil, nil, fmt.Errorf("Error writing schema from plugin to cache: %w", err)
}
}
if pluginVersion == nil {
info, _ := provider.GetPluginInfo(ctx) // nonfatal error
pluginVersion = info.Version
}
return schemaBytes, pluginVersion, nil
}
func (l *pluginLoader) loadPluginSchemaBytes(
ctx context.Context, descriptor *PackageDescriptor,
) ([]byte, plugin.Provider, error) {
wsDescriptor := workspace.PackageDescriptor{
PluginSpec: workspace.PluginSpec{
Name: descriptor.Name,
Version: descriptor.Version,
PluginDownloadURL: descriptor.DownloadURL,
Kind: apitype.ResourcePlugin,
},
}
if descriptor.Parameterization != nil {
wsDescriptor.Parameterization = &workspace.Parameterization{
Name: descriptor.Parameterization.Name,
Version: descriptor.Parameterization.Version,
Value: descriptor.Parameterization.Value,
}
}
provider, err := l.host.Provider(wsDescriptor)
if err != nil {
return nil, nil, err
}
contract.Assertf(provider != nil, "unexpected nil provider for %s@%v", descriptor.Name, descriptor.Version)
var schemaFormatVersion int32
getSchemaRequest := plugin.GetSchemaRequest{
Version: schemaFormatVersion,
}
// If this is a parameterized package, we need to pass the parameter value to the provider.
if descriptor.Parameterization != nil {
parameterization := plugin.ParameterizeRequest{
Parameters: &plugin.ParameterizeValue{
Name: descriptor.Parameterization.Name,
Version: descriptor.Parameterization.Version,
Value: descriptor.Parameterization.Value,
},
}
resp, err := provider.Parameterize(ctx, parameterization)
if err != nil {
return nil, nil, err
}
if resp.Name != descriptor.Parameterization.Name {
return nil, nil, fmt.Errorf(
"unexpected parameterization response: %s != %s", resp.Name, descriptor.Parameterization.Name)
}
if !resp.Version.EQ(descriptor.Parameterization.Version) {
return nil, nil, fmt.Errorf(
"unexpected parameterization response: %s != %s", resp.Version, descriptor.Parameterization.Version)
}
getSchemaRequest.SubpackageName = resp.Name
getSchemaRequest.SubpackageVersion = &resp.Version
}
schema, err := provider.GetSchema(ctx, getSchemaRequest)
if err != nil {
return nil, nil, err
}
return schema.Schema, provider, nil
}
// Copyright 2016-2024, 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 schema
import (
"context"
"sync"
"github.com/blang/semver"
)
func NewCachedLoader(loader ReferenceLoader) ReferenceLoader {
return &cachedLoader{
loader: loader,
entries: make(map[string]PackageReference),
}
}
type cachedLoader struct {
loader ReferenceLoader
m sync.RWMutex
entries map[string]PackageReference
}
func (l *cachedLoader) LoadPackage(pkg string, version *semver.Version) (*Package, error) {
ref, err := l.LoadPackageReference(pkg, version)
if err != nil {
return nil, err
}
return ref.Definition()
}
func (l *cachedLoader) LoadPackageV2(ctx context.Context, descriptor *PackageDescriptor) (*Package, error) {
ref, err := l.LoadPackageReferenceV2(ctx, descriptor)
if err != nil {
return nil, err
}
return ref.Definition()
}
func (l *cachedLoader) LoadPackageReference(pkg string, version *semver.Version) (PackageReference, error) {
return l.LoadPackageReferenceV2(context.Background(), &PackageDescriptor{
Name: pkg,
Version: version,
})
}
func (l *cachedLoader) LoadPackageReferenceV2(
ctx context.Context, descriptor *PackageDescriptor,
) (PackageReference, error) {
l.m.Lock()
defer l.m.Unlock()
var key string
if descriptor.Parameterization == nil {
key = packageIdentity(descriptor.Name, descriptor.Version)
} else {
key = packageIdentity(descriptor.Parameterization.Name, &descriptor.Parameterization.Version)
}
if p, ok := l.entries[key]; ok {
return p, nil
}
p, err := l.loader.LoadPackageReferenceV2(ctx, descriptor)
if err != nil {
return nil, err
}
l.entries[key] = p
return p, nil
}
// Copyright 2016-2023, 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 schema
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"github.com/blang/semver"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil/rpcerror"
codegenrpc "github.com/pulumi/pulumi/sdk/v3/proto/go/codegen"
"github.com/segmentio/encoding/json"
)
// LoaderClient reflects a loader service, loaded dynamically from the engine process over gRPC.
type LoaderClient struct {
conn *grpc.ClientConn // the underlying gRPC connection.
clientRaw codegenrpc.LoaderClient // the raw loader client; usually unsafe to use directly.
}
var _ ReferenceLoader = (*LoaderClient)(nil)
func NewLoaderClient(target string) (*LoaderClient, error) {
contract.Assertf(target != "", "unexpected empty target for loader")
conn, err := grpc.NewClient(
target,
grpc.WithTransportCredentials(insecure.NewCredentials()),
rpcutil.GrpcChannelOptions(),
)
if err != nil {
return nil, err
}
l := &LoaderClient{
conn: conn,
clientRaw: codegenrpc.NewLoaderClient(conn),
}
return l, nil
}
func (l *LoaderClient) Close() error {
if l.clientRaw != nil {
err := l.conn.Close()
l.conn = nil
l.clientRaw = nil
return err
}
return nil
}
func (l *LoaderClient) LoadPackageReference(pkg string, version *semver.Version) (PackageReference, error) {
return l.LoadPackageReferenceV2(context.TODO(), &PackageDescriptor{
Name: pkg,
Version: version,
})
}
func (l *LoaderClient) LoadPackageReferenceV2(
ctx context.Context, descriptor *PackageDescriptor,
) (PackageReference, error) {
label := "GetSchema"
logging.V(7).Infof("%s executing: package=%s, version=%s", label, descriptor.Name, descriptor.Version)
var versionString string
if descriptor.Version != nil {
versionString = descriptor.Version.String()
}
var parameterization *codegenrpc.Parameterization
if descriptor.Parameterization != nil {
parameterization = &codegenrpc.Parameterization{
Name: descriptor.Parameterization.Name,
Version: descriptor.Parameterization.Version.String(),
Value: descriptor.Parameterization.Value,
}
}
resp, err := l.clientRaw.GetSchema(ctx, &codegenrpc.GetSchemaRequest{
Package: descriptor.Name,
Version: versionString,
DownloadUrl: descriptor.DownloadURL,
Parameterization: parameterization,
})
if err != nil {
rpcError := rpcerror.Convert(err)
logging.V(7).Infof("%s failed: %v", label, rpcError)
return nil, err
}
var spec PartialPackageSpec
if _, err := json.Parse(resp.Schema, &spec, json.ZeroCopy); err != nil {
return nil, err
}
p, err := ImportPartialSpec(spec, nil, l)
if err != nil {
return nil, err
}
logging.V(7).Infof("%s success", label)
return p, nil
}
func (l *LoaderClient) LoadPackage(pkg string, version *semver.Version) (*Package, error) {
ref, err := l.LoadPackageReference(pkg, version)
if err != nil {
return nil, err
}
return ref.Definition()
}
func (l *LoaderClient) LoadPackageV2(ctx context.Context, descriptor *PackageDescriptor) (*Package, error) {
ref, err := l.LoadPackageReferenceV2(ctx, descriptor)
if err != nil {
return nil, err
}
return ref.Definition()
}
// Copyright 2016-2024, 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.
//go:build !js
// +build !js
package schema
import (
"io"
"os"
"time"
"github.com/edsrzf/mmap-go"
)
var mmapedFiles = make(map[string]mmap.MMap)
func (l *pluginLoader) loadCachedSchemaBytes(pkg string, path string, schemaTime time.Time) ([]byte, bool) {
if l.cacheOptions.disableFileCache {
return nil, false
}
if schemaMmap, ok := mmapedFiles[path]; ok {
return schemaMmap, true
}
success := false
schemaFile, err := os.OpenFile(path, os.O_RDONLY, 0o644)
defer func() {
if !success {
schemaFile.Close()
}
}()
if err != nil {
return nil, success
}
stat, err := schemaFile.Stat()
if err != nil {
return nil, success
}
cachedAt := stat.ModTime()
if schemaTime.After(cachedAt) {
return nil, success
}
if l.cacheOptions.disableMmap {
data, err := io.ReadAll(schemaFile)
if err != nil {
return nil, success
}
success = true
return data, success
}
schemaMmap, err := mmap.Map(schemaFile, mmap.RDONLY, 0)
if err != nil {
return nil, success
}
success = true
return schemaMmap, success
}
// Copyright 2016-2023, 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 schema
import (
"context"
"fmt"
"google.golang.org/grpc"
"github.com/blang/semver"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
codegenrpc "github.com/pulumi/pulumi/sdk/v3/proto/go/codegen"
"github.com/segmentio/encoding/json"
)
type loaderServer struct {
codegenrpc.UnsafeLoaderServer // opt out of forward compat
loader ReferenceLoader
}
func NewLoaderServer(loader ReferenceLoader) codegenrpc.LoaderServer {
return &loaderServer{loader: loader}
}
func (m *loaderServer) GetSchema(ctx context.Context,
req *codegenrpc.GetSchemaRequest,
) (*codegenrpc.GetSchemaResponse, error) {
label := "GetSchema"
logging.V(7).Infof("%s executing: package=%s, version=%s", label, req.Package, req.Version)
var version *semver.Version
if req.Version != "" {
v, err := semver.ParseTolerant(req.Version)
if err != nil {
logging.V(7).Infof("%s failed: %v", label, err)
return nil, fmt.Errorf("%s not a valid semver: %w", req.Version, err)
}
version = &v
}
descriptor := &PackageDescriptor{
Name: req.Package,
Version: version,
DownloadURL: req.DownloadUrl,
}
if req.Parameterization != nil {
descriptor.Parameterization = &ParameterizationDescriptor{
Name: req.Parameterization.Name,
Value: req.Parameterization.Value,
}
v, err := semver.ParseTolerant(req.Parameterization.Version)
if err != nil {
logging.V(7).Infof("%s failed: %v", label, err)
return nil, fmt.Errorf("%s not a valid semver: %w", req.Version, err)
}
descriptor.Parameterization.Version = v
}
pkg, err := m.loader.LoadPackageV2(ctx, descriptor)
if err != nil {
logging.V(7).Infof("%s failed: %v", label, err)
return nil, err
}
// Marshal the package into a JSON string.
spec, err := pkg.MarshalSpec()
if err != nil {
logging.V(7).Infof("%s failed: %v", label, err)
return nil, err
}
data, err := json.Marshal(spec)
if err != nil {
logging.V(7).Infof("%s failed: %v", label, err)
return nil, err
}
logging.V(7).Infof("%s success: data=#%d", label, len(data))
return &codegenrpc.GetSchemaResponse{
Schema: data,
}, nil
}
func LoaderRegistration(l codegenrpc.LoaderServer) func(*grpc.Server) {
return func(srv *grpc.Server) {
codegenrpc.RegisterLoaderServer(srv, l)
}
}
// Copyright 2022-2024, 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 schema
import (
"context"
"github.com/blang/semver"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
func newPulumiPackage() *Package {
spec := PackageSpec{
Name: "pulumi",
DisplayName: "Pulumi",
Version: "1.0.0",
Description: "mock pulumi package",
Resources: map[string]ResourceSpec{
"pulumi:pulumi:StackReference": {
ObjectTypeSpec: ObjectTypeSpec{
Properties: map[string]PropertySpec{
"outputs": {TypeSpec: TypeSpec{
Type: "object",
AdditionalProperties: &TypeSpec{
Ref: "pulumi.json#/Any",
},
}},
},
Required: []string{
"outputs",
},
},
InputProperties: map[string]PropertySpec{
"name": {TypeSpec: TypeSpec{Type: "string"}},
},
},
},
Provider: ResourceSpec{
InputProperties: map[string]PropertySpec{
"name": {
Description: "fully qualified name of stack, i.e. <organization>/<project>/<stack>",
TypeSpec: TypeSpec{
Type: "string",
},
},
},
},
}
pkg, diags, err := bindSpec(spec, nil, nullLoader{}, false, ValidationOptions{
AllowDanglingReferences: true,
})
if err == nil && diags.HasErrors() {
err = diags
}
contract.AssertNoErrorf(err, "failed to bind mock pulumi package")
return pkg
}
type nullLoader struct{}
func (nullLoader) LoadPackage(pkg string, version *semver.Version) (*Package, error) {
contract.Failf("nullLoader invoked on %s,%s", pkg, version)
return nil, nil
}
func (nullLoader) LoadPackageV2(ctx context.Context, descriptor *PackageDescriptor) (*Package, error) {
contract.Failf("nullLoader invoked on %s,%s", descriptor.Name, descriptor.Version)
return nil, nil
}
var DefaultPulumiPackage = newPulumiPackage()
// Copyright 2022-2024, 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 schema
import (
"fmt"
"reflect"
"sort"
"sync"
"github.com/blang/semver"
"github.com/hashicorp/hcl/v2"
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/segmentio/encoding/json"
)
// A PackageReference represents a references Pulumi Package. Applications that do not need access to the entire
// definition of a Pulumi Package should use PackageReference rather than Package, as the former uses memory more
// efficiently than the latter by binding package members on-demand.
type PackageReference interface {
// Name returns the package name.
Name() string
// Version returns the package version.
Version() *semver.Version
// Description returns the packages description.
Description() string
// Publisher returns the package publisher.
Publisher() string
// Namespace returns the package namespace.
Namespace() string
// Repository returns the package repository.
Repository() string
// SupportPack specifies the package definition can be packed by language plugins, this is always true for
// parameterized packages.
SupportPack() bool
// Types returns the package's types.
Types() PackageTypes
// Config returns the package's configuration variables, if any.
Config() ([]*Property, error)
// Provider returns the package's provider.
Provider() (*Resource, error)
// Resources returns the package's resources.
Resources() PackageResources
// Functions returns the package's functions.
Functions() PackageFunctions
// The language specific metadata for a given language.
//
// The package must have been originally bound with a matching [Language]
// importer.
Language(string) (any, error)
// TokenToModule extracts a package member's module name from its token.
TokenToModule(token string) string
// Definition fully loads the referenced package and returns the result.
Definition() (*Package, error)
}
// PackageTypes provides random and sequential access to a package's types.
type PackageTypes interface {
// Range returns a range iterator for the package's types. Call Next to
// advance the iterator, and Token/Type to access each entry. Type definitions
// are loaded on demand. Iteration order is undefined.
//
// Example:
//
// for it := pkg.Types().Range(); it.Next(); {
// token := it.Token()
// typ, err := it.Type()
// ...
// }
//
Range() TypesIter
// Get finds and loads the type with the given token. If the type is not found,
// this function returns (nil, false, nil).
Get(token string) (Type, bool, error)
}
// TypesIter is an iterator for ranging over a package's types. See PackageTypes.Range.
type TypesIter interface {
Token() string
Type() (Type, error)
Next() bool
}
// PackageResources provides random and sequential access to a package's resources.
type PackageResources interface {
// Range returns a range iterator for the package's resources. Call Next to
// advance the iterator, and Token/Resource to access each entry. Resource definitions
// are loaded on demand. Iteration order is undefined.
//
// Example:
//
// for it := pkg.Resources().Range(); it.Next(); {
// token := it.Token()
// res, err := it.Resource()
// ...
// }
//
Range() ResourcesIter
// Get finds and loads the resource with the given token. If the resource is not found,
// this function returns (nil, false, nil).
Get(token string) (*Resource, bool, error)
// GetType loads the *ResourceType that corresponds to a given resource definition.
GetType(token string) (*ResourceType, bool, error)
}
// ResourcesIter is an iterator for ranging over a package's resources. See PackageResources.Range.
type ResourcesIter interface {
Token() string
Resource() (*Resource, error)
Next() bool
}
// PackageFunctions provides random and sequential access to a package's functions.
type PackageFunctions interface {
// Range returns a range iterator for the package's functions. Call Next to
// advance the iterator, and Token/Function to access each entry. Function definitions
// are loaded on demand. Iteration order is undefined.
//
// Example:
//
// for it := pkg.Functions().Range(); it.Next(); {
// token := it.Token()
// fn, err := it.Function()
// ...
// }
//
Range() FunctionsIter
// Get finds and loads the function with the given token. If the function is not found,
// this function returns (nil, false, nil).
Get(token string) (*Function, bool, error)
}
// FunctionsIter is an iterator for ranging over a package's functions. See PackageFunctions.Range.
type FunctionsIter interface {
Token() string
Function() (*Function, error)
Next() bool
}
// packageDefRef is an implementation of PackageReference backed by a *Package.
type packageDefRef struct {
pkg *Package
}
func (p packageDefRef) Name() string {
return p.pkg.Name
}
func (p packageDefRef) Version() *semver.Version {
return p.pkg.Version
}
func (p packageDefRef) Description() string {
return p.pkg.Description
}
func (p packageDefRef) Publisher() string {
return p.pkg.Publisher
}
func (p packageDefRef) Namespace() string {
return p.pkg.Namespace
}
func (p packageDefRef) Repository() string {
return p.pkg.Repository
}
func (p packageDefRef) SupportPack() bool {
return p.pkg.SupportPack
}
func (p packageDefRef) Types() PackageTypes {
return packageDefTypes{p.pkg}
}
func (p packageDefRef) Config() ([]*Property, error) {
return p.pkg.Config, nil
}
func (p packageDefRef) Provider() (*Resource, error) {
return p.pkg.Provider, nil
}
func (p packageDefRef) Resources() PackageResources {
return packageDefResources{p.pkg}
}
func (p packageDefRef) Functions() PackageFunctions {
return packageDefFunctions{p.pkg}
}
func (p packageDefRef) Language(language string) (any, error) {
return p.pkg.Language[language], nil
}
func (p packageDefRef) TokenToModule(token string) string {
return p.pkg.TokenToModule(token)
}
func (p packageDefRef) Definition() (*Package, error) {
return p.pkg, nil
}
type packageDefTypes struct {
*Package
}
func (p packageDefTypes) Range() TypesIter {
return &packageDefTypesIter{types: p.Types}
}
func (p packageDefTypes) Get(token string) (Type, bool, error) {
def, ok := p.typeTable[token]
return def, ok, nil
}
type packageDefTypesIter struct {
types []Type
t Type
}
func (i *packageDefTypesIter) Token() string {
if obj, ok := i.t.(*ObjectType); ok {
return obj.Token
}
return i.t.(*EnumType).Token
}
func (i *packageDefTypesIter) Type() (Type, error) {
return i.t, nil
}
func (i *packageDefTypesIter) Next() bool {
if len(i.types) == 0 {
return false
}
i.t, i.types = i.types[0], i.types[1:]
return true
}
type packageDefResources struct {
*Package
}
func (p packageDefResources) Range() ResourcesIter {
return &packageDefResourcesIter{resources: p.Resources}
}
func (p packageDefResources) Get(token string) (*Resource, bool, error) {
if token == p.Provider.Token {
return p.Provider, true, nil
}
def, ok := p.resourceTable[token]
return def, ok, nil
}
func (p packageDefResources) GetType(token string) (*ResourceType, bool, error) {
typ, ok := p.GetResourceType(token)
return typ, ok, nil
}
type packageDefResourcesIter struct {
resources []*Resource
r *Resource
}
func (i *packageDefResourcesIter) Token() string {
return i.r.Token
}
func (i *packageDefResourcesIter) Resource() (*Resource, error) {
return i.r, nil
}
func (i *packageDefResourcesIter) Next() bool {
if len(i.resources) == 0 {
return false
}
i.r, i.resources = i.resources[0], i.resources[1:]
return true
}
type packageDefFunctions struct {
*Package
}
func (p packageDefFunctions) Range() FunctionsIter {
return &packageDefFunctionsIter{functions: p.Functions}
}
func (p packageDefFunctions) Get(token string) (*Function, bool, error) {
def, ok := p.functionTable[token]
return def, ok, nil
}
type packageDefFunctionsIter struct {
functions []*Function
f *Function
}
func (i *packageDefFunctionsIter) Token() string {
return i.f.Token
}
func (i *packageDefFunctionsIter) Function() (*Function, error) {
return i.f, nil
}
func (i *packageDefFunctionsIter) Next() bool {
if len(i.functions) == 0 {
return false
}
i.f, i.functions = i.functions[0], i.functions[1:]
return true
}
// PartialPackage is an implementation of PackageReference that loads and binds package members on demand. A
// PartialPackage is backed by a PartialPackageSpec, which leaves package members in their JSON-encoded form until
// they are required. PartialPackages are created using ImportPartialSpec.
type PartialPackage struct {
// This mutex guards two operations:
// - access to PartialPackage.def
// - package binding operations
m sync.Mutex
spec *PartialPackageSpec
languages map[string]Language
types *types
config []*Property
def *Package
}
func (p *PartialPackage) Name() string {
p.m.Lock()
defer p.m.Unlock()
if p.def != nil {
return p.def.Name
}
return p.types.pkg.Name
}
func (p *PartialPackage) Version() *semver.Version {
p.m.Lock()
defer p.m.Unlock()
if p.def != nil {
return p.def.Version
}
return p.types.pkg.Version
}
func (p *PartialPackage) Description() string {
p.m.Lock()
defer p.m.Unlock()
if p.def != nil {
return p.def.Description
}
return p.types.pkg.Description
}
func (p *PartialPackage) Publisher() string {
p.m.Lock()
defer p.m.Unlock()
if p.def != nil {
return p.def.Publisher
}
return p.types.pkg.Publisher
}
func (p *PartialPackage) Namespace() string {
p.m.Lock()
defer p.m.Unlock()
if p.def != nil {
return p.def.Namespace
}
return p.types.pkg.Namespace
}
func (p *PartialPackage) Repository() string {
p.m.Lock()
defer p.m.Unlock()
if p.def != nil {
return p.def.Repository
}
return p.types.pkg.Repository
}
func (p *PartialPackage) SupportPack() bool {
p.m.Lock()
defer p.m.Unlock()
if p.def != nil {
return p.def.SupportPack
}
return p.types.pkg.SupportPack
}
func (p *PartialPackage) Types() PackageTypes {
p.m.Lock()
defer p.m.Unlock()
if p.def != nil {
return packageDefTypes{p.def}
}
return partialPackageTypes{p}
}
func (p *PartialPackage) Config() ([]*Property, error) {
p.m.Lock()
defer p.m.Unlock()
if p.def != nil {
return p.def.Config, nil
}
if p.config != nil {
return p.config, nil
}
return p.bindConfig()
}
func (p *PartialPackage) bindConfig() ([]*Property, error) {
var spec ConfigSpec
if err := parseJSONPropertyValue(p.spec.Config, &spec); err != nil {
return nil, fmt.Errorf("unmarshaling config: %w", err)
}
config, diags, err := bindConfig(spec, p.types, ValidationOptions{
AllowDanglingReferences: true,
})
if err != nil {
return nil, err
}
if diags.HasErrors() {
return nil, diags
}
p.config = config
return p.config, nil
}
func (p *PartialPackage) Provider() (*Resource, error) {
p.m.Lock()
defer p.m.Unlock()
if p.def != nil {
return p.def.Provider, nil
}
provider, diags, err := p.types.bindResourceDef("pulumi:providers:"+p.spec.Name, ValidationOptions{
AllowDanglingReferences: true,
})
if err != nil {
return nil, err
}
if diags.HasErrors() {
return nil, err
}
return provider, nil
}
func (p *PartialPackage) Resources() PackageResources {
p.m.Lock()
defer p.m.Unlock()
if p.def != nil {
return packageDefResources{p.def}
}
return partialPackageResources{p}
}
func (p *PartialPackage) Functions() PackageFunctions {
p.m.Lock()
defer p.m.Unlock()
if p.def != nil {
return packageDefFunctions{p.def}
}
return partialPackageFunctions{p}
}
func (p *PartialPackage) Language(language string) (any, error) {
p.m.Lock()
defer p.m.Unlock()
if p.def != nil {
if l, ok := p.def.Language[language]; ok {
return l, nil
}
}
if p.spec == nil {
return nil, nil
}
val, ok := p.spec.Language[language]
if !ok {
return nil, nil
}
importer, ok := p.languages[language]
if !ok {
return nil, nil
}
imported, err := importer.ImportPackageSpec(json.RawMessage(val))
if err != nil {
return nil, err
}
if p.def == nil {
p.def = new(Package)
}
if p.def.Language == nil {
p.def.Language = map[string]any{}
}
p.def.Language[language] = imported
return imported, nil
}
func (p *PartialPackage) TokenToModule(token string) string {
p.m.Lock()
defer p.m.Unlock()
if p.def != nil {
return p.def.TokenToModule(token)
}
return p.types.pkg.TokenToModule(token)
}
func (p *PartialPackage) Definition() (*Package, error) {
p.m.Lock()
defer p.m.Unlock()
if p.def != nil {
return p.def, nil
}
config, err := p.bindConfig()
if err != nil {
return nil, err
}
var diags hcl.Diagnostics
provider, resources, resourceDiags, err := p.types.finishResources(sortedKeys(p.spec.Resources), ValidationOptions{
AllowDanglingReferences: true,
})
if err != nil {
return nil, err
}
diags = diags.Extend(resourceDiags)
functions, functionDiags, err := p.types.finishFunctions(sortedKeys(p.spec.Functions), ValidationOptions{
AllowDanglingReferences: true,
})
if err != nil {
return nil, err
}
diags = diags.Extend(functionDiags)
typeList, typeDiags, err := p.types.finishTypes(sortedKeys(p.spec.Types), ValidationOptions{
AllowDanglingReferences: true,
})
if err != nil {
return nil, err
}
diags = diags.Extend(typeDiags)
pkg := p.types.pkg
pkg.Config = config
pkg.Types = typeList
pkg.Provider = provider
pkg.Resources = resources
pkg.Functions = functions
pkg.resourceTable = p.types.resourceDefs
pkg.functionTable = p.types.functionDefs
pkg.typeTable = p.types.typeDefs
pkg.resourceTypeTable = p.types.resources
if p.spec.Parameterization != nil {
pkg.Parameterization = &Parameterization{
BaseProvider: BaseProvider{
Name: p.spec.Parameterization.BaseProvider.Name,
Version: semver.MustParse(p.spec.Parameterization.BaseProvider.Version),
},
Parameter: p.spec.Parameterization.Parameter,
}
}
if err := pkg.ImportLanguages(p.languages); err != nil {
return nil, err
}
if diags.HasErrors() {
return nil, diags
}
contract.IgnoreClose(p.types)
p.spec = nil
p.types = nil
p.languages = nil
p.config = nil
p.def = pkg
return pkg, nil
}
// Snapshot returns a definition for the package that contains only the members that have been accessed thus far. If
// Definition has been called, the returned definition will include all of the package's members. It is safe to call
// Snapshot multiple times.
func (p *PartialPackage) Snapshot() (*Package, error) {
p.m.Lock()
defer p.m.Unlock()
if p.def != nil {
return p.def, nil
}
config := p.config
provider := p.types.resourceDefs["pulumi:providers:"+p.spec.Name]
resources := slice.Prealloc[*Resource](len(p.types.resourceDefs))
resourceDefs := make(map[string]*Resource, len(p.types.resourceDefs))
for token, res := range p.types.resourceDefs {
resources, resourceDefs[token] = append(resources, res), res
}
sort.Slice(resources, func(i, j int) bool {
return resources[i].Token < resources[j].Token
})
functions := slice.Prealloc[*Function](len(p.types.functionDefs))
functionDefs := make(map[string]*Function, len(p.types.functionDefs))
for token, fn := range p.types.functionDefs {
functions, functionDefs[token] = append(functions, fn), fn
}
sort.Slice(functions, func(i, j int) bool {
return functions[i].Token < functions[j].Token
})
typeList, diags, err := p.types.finishTypes(nil, ValidationOptions{
AllowDanglingReferences: true,
})
contract.AssertNoErrorf(err, "error snapshotting types")
contract.Assertf(len(diags) == 0, "unexpected diagnostics: %v", diags)
typeDefs := make(map[string]Type, len(p.types.typeDefs))
for token, typ := range p.types.typeDefs {
typeDefs[token] = typ
}
resourceTypes := make(map[string]*ResourceType, len(p.types.resources))
for token, typ := range p.types.resources {
resourceTypes[token] = typ
}
// NOTE: these writes are very much not concurrency-safe. There is a data race on each write to a slice-typed field
// because slices are multi-word values. Unfortunately, fixing this is rather involved. The simplest solution--
// returning a copy of p.types.pkg--breaks package membership tests that use pointer equality (e.g. if the result
// of Snapshot() is in a variable named `pkg`, `pkg.Resources[0].Package == pkg` will evaluate to `false`). It is
// likely that we will need to make a breaking change in order to fix this.
//
// There is also a race between the call to ImportLanguages and readers of the Language property on the various
// types.
pkg := p.types.pkg
pkg.Config = config
pkg.Types = typeList
pkg.Provider = provider
pkg.Resources = resources
pkg.Functions = functions
pkg.resourceTable = resourceDefs
pkg.functionTable = functionDefs
pkg.typeTable = typeDefs
pkg.resourceTypeTable = resourceTypes
if err := pkg.ImportLanguages(p.languages); err != nil {
return nil, err
}
return pkg, nil
}
type partialPackageTypes struct {
*PartialPackage
}
func (p partialPackageTypes) Range() TypesIter {
return &partialPackageTypesIter{
types: p,
iter: reflect.ValueOf(p.spec.Types).MapRange(),
}
}
func (p partialPackageTypes) Get(token string) (Type, bool, error) {
p.m.Lock()
defer p.m.Unlock()
typ, diags, err := p.types.bindTypeDef(token, ValidationOptions{
AllowDanglingReferences: true,
})
if err != nil {
return nil, false, err
}
if diags.HasErrors() {
return nil, false, diags
}
return typ, typ != nil, nil
}
type partialPackageTypesIter struct {
types partialPackageTypes
iter *reflect.MapIter
}
func (i *partialPackageTypesIter) Token() string {
return i.iter.Key().String()
}
func (i *partialPackageTypesIter) Type() (Type, error) {
typ, _, err := i.types.Get(i.Token())
return typ, err
}
func (i *partialPackageTypesIter) Next() bool {
return i.iter.Next()
}
type partialPackageResources struct {
*PartialPackage
}
func (p partialPackageResources) Range() ResourcesIter {
return &partialPackageResourcesIter{
resources: p,
iter: reflect.ValueOf(p.spec.Resources).MapRange(),
}
}
func (p partialPackageResources) Get(token string) (*Resource, bool, error) {
p.m.Lock()
defer p.m.Unlock()
res, diags, err := p.types.bindResourceDef(token, ValidationOptions{
AllowDanglingReferences: true,
})
if err != nil {
return nil, false, err
}
if diags.HasErrors() {
return nil, false, diags
}
return res, res != nil, nil
}
func (p partialPackageResources) GetType(token string) (*ResourceType, bool, error) {
p.m.Lock()
defer p.m.Unlock()
typ, diags, err := p.types.bindResourceTypeDef(token, ValidationOptions{
AllowDanglingReferences: true,
})
if err != nil {
return nil, false, err
}
if diags.HasErrors() {
return nil, false, diags
}
return typ, typ != nil, nil
}
type partialPackageResourcesIter struct {
resources partialPackageResources
iter *reflect.MapIter
}
func (i *partialPackageResourcesIter) Token() string {
return i.iter.Key().String()
}
func (i *partialPackageResourcesIter) Resource() (*Resource, error) {
res, _, err := i.resources.Get(i.Token())
return res, err
}
func (i *partialPackageResourcesIter) Next() bool {
return i.iter.Next()
}
type partialPackageFunctions struct {
*PartialPackage
}
func (p partialPackageFunctions) Range() FunctionsIter {
return &partialPackageFunctionsIter{
functions: p,
iter: reflect.ValueOf(p.spec.Functions).MapRange(),
}
}
func (p partialPackageFunctions) Get(token string) (*Function, bool, error) {
p.m.Lock()
defer p.m.Unlock()
fn, diags, err := p.types.bindFunctionDef(token, ValidationOptions{
AllowDanglingReferences: true,
})
if err != nil {
return nil, false, err
}
if diags.HasErrors() {
return nil, false, diags
}
return fn, fn != nil, nil
}
type partialPackageFunctionsIter struct {
functions partialPackageFunctions
iter *reflect.MapIter
}
func (i *partialPackageFunctionsIter) Token() string {
return i.iter.Key().String()
}
func (i *partialPackageFunctionsIter) Function() (*Function, error) {
fn, _, err := i.functions.Get(i.Token())
return fn, err
}
func (i *partialPackageFunctionsIter) Next() bool {
return i.iter.Next()
}
// parseJSONPropertyValue parses a JSON value from the given RawMessage. If the RawMessage is empty, parsing is skipped
// and the function returns nil.
func parseJSONPropertyValue(raw json.RawMessage, dest interface{}) error {
if len(raw) == 0 {
return nil
}
_, err := json.Parse([]byte(raw), dest, json.ZeroCopy)
return err
}
// Copyright 2016-2025, 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 schema
import "slices"
var (
// These property names are reserved
reservedKeywords = []string{"pulumi"}
reservedTopLevelPropertyNames = []string{"version"}
// urn is a reserved property name for all resources
// id is a reserved property name for resources which are not components
reservedResourcePropertyKeys = []string{"urn"}
// These are only reserved for non-component resources
reservedNonComponentPropertyKeys = []string{"id"}
)
func isReservedKeyword(name string) bool {
return slices.Contains(reservedKeywords, name)
}
func isReservedProviderPropertyName(name string) bool {
return slices.Contains(reservedTopLevelPropertyNames, name) || isReservedKeyword(name)
}
func isReservedComponentResourcePropertyKey(name string) bool {
return slices.Contains(reservedResourcePropertyKeys, name) || isReservedKeyword(name)
}
func isReservedCustomResourcePropertyKey(name string) bool {
return slices.Contains(reservedNonComponentPropertyKeys, name)
}
// Copyright 2016-2022, 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 schema
import (
"bytes"
"encoding/json"
"fmt"
"net/url"
"regexp"
"sort"
"strings"
"github.com/blang/semver"
"github.com/hashicorp/hcl/v2"
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
"gopkg.in/yaml.v3"
)
// TODO:
// - Providerless packages
// Type represents a datatype in the Pulumi Schema. Types created by this package are identical if they are
// equal values.
type Type interface {
String() string
isType()
}
type primitiveType int
const (
boolType primitiveType = 1
intType primitiveType = 2
numberType primitiveType = 3
stringType primitiveType = 4
archiveType primitiveType = 5
assetType primitiveType = 6
anyType primitiveType = 7
jsonType primitiveType = 8
anyResourceType primitiveType = 9
)
func (t primitiveType) String() string {
switch t {
case boolType:
return "boolean"
case intType:
return "integer"
case numberType:
return "number"
case stringType:
return "string"
case archiveType:
return "pulumi:pulumi:Archive"
case assetType:
return "pulumi:pulumi:Asset"
case jsonType:
fallthrough
case anyResourceType:
fallthrough
case anyType:
return "pulumi:pulumi:Any"
default:
panic("unknown primitive type")
}
}
func (primitiveType) isType() {}
// IsPrimitiveType returns true if the given Type is a primitive type. The primitive types are bool, int, number,
// string, archive, asset, and any.
func IsPrimitiveType(t Type) bool {
_, ok := plainType(t).(primitiveType)
return ok
}
var (
// BoolType represents the set of boolean values.
BoolType Type = boolType
// IntType represents the set of 32-bit integer values.
IntType Type = intType
// NumberType represents the set of IEEE754 double-precision values.
NumberType Type = numberType
// StringType represents the set of UTF-8 string values.
StringType Type = stringType
// ArchiveType represents the set of Pulumi Archive values.
ArchiveType Type = archiveType
// AssetType represents the set of Pulumi Asset values.
AssetType Type = assetType
// JSONType represents the set of JSON-encoded values.
JSONType Type = jsonType
// AnyType represents the complete set of values.
AnyType Type = anyType
// AnyResourceType represents any Pulumi resource - custom or component
AnyResourceType Type = anyResourceType
)
// An InvalidType represents an invalid type with associated diagnostics.
type InvalidType struct {
Diagnostics hcl.Diagnostics
}
func (t *InvalidType) String() string {
return "Invalid"
}
func (*InvalidType) isType() {}
func invalidType(diags ...*hcl.Diagnostic) (Type, hcl.Diagnostics) {
t := &InvalidType{Diagnostics: hcl.Diagnostics(diags)}
return t, hcl.Diagnostics(diags)
}
// MapType represents maps from strings to particular element types.
type MapType struct {
// ElementType is the element type of the map.
ElementType Type
}
func (t *MapType) String() string {
return fmt.Sprintf("Map<%v>", t.ElementType)
}
func (*MapType) isType() {}
// ArrayType represents arrays of particular element types.
type ArrayType struct {
// ElementType is the element type of the array.
ElementType Type
}
func (t *ArrayType) String() string {
return fmt.Sprintf("Array<%v>", t.ElementType)
}
func (*ArrayType) isType() {}
// EnumType represents an enum.
type EnumType struct {
// PackageReference is the PackageReference that defines the resource.
PackageReference PackageReference
// Token is the type's Pulumi type token.
Token string
// Comment is the description of the type, if any.
Comment string
// Elements are the predefined enum values.
Elements []*Enum
// ElementType is the underlying type for the enum.
ElementType Type
// IsOverlay indicates whether the type is an overlay provided by the package. Overlay code is generated by the
// package rather than using the core Pulumi codegen libraries.
IsOverlay bool
}
// Enum contains information about an enum.
type Enum struct {
// Value is the value of the enum.
Value interface{}
// Comment is the description for the enum value.
Comment string
// Name for the enum.
Name string
// DeprecationMessage indicates whether or not the value is deprecated.
DeprecationMessage string
}
func (t *EnumType) String() string {
return t.Token
}
func (*EnumType) isType() {}
// UnionType represents values that may be any one of a specified set of types.
type UnionType struct {
// ElementTypes are the allowable types for the union type.
ElementTypes []Type
// DefaultType is the default type, if any, for the union type. This can be used by targets that do not support
// unions, or in positions where unions are not appropriate.
DefaultType Type
// Discriminator informs the consumer of an alternative schema based on the value associated with it.
Discriminator string
// Mapping is an optional object to hold mappings between payload values and schema names or references.
Mapping map[string]string
}
func (t *UnionType) String() string {
elements := make([]string, len(t.ElementTypes))
for i, e := range t.ElementTypes {
elements[i] = e.String()
}
if t.DefaultType != nil {
elements = append(elements, "default="+t.DefaultType.String())
}
return fmt.Sprintf("Union<%v>", strings.Join(elements, ", "))
}
func (*UnionType) isType() {}
// ObjectType represents schematized maps from strings to particular types.
type ObjectType struct {
// PackageReference is the PackageReference that defines the resource.
PackageReference PackageReference
// Token is the type's Pulumi type token.
Token string
// Comment is the description of the type, if any.
Comment string
// Properties is the list of the type's properties.
Properties []*Property
// Language specifies additional language-specific data about the object type.
Language map[string]interface{}
// IsOverlay indicates whether the type is an overlay provided by the package. Overlay code is generated by the
// package rather than using the core Pulumi codegen libraries.
IsOverlay bool
// OverlaySupportedLanguages indicates what languages the overlay supports. This only has an effect if
// the Resource is an Overlay (IsOverlay == true).
// Supported values are "nodejs", "python", "go", "csharp", "java", "yaml"
OverlaySupportedLanguages []string
// InputShape is the input shape for this object. Only valid if IsPlainShape returns true.
InputShape *ObjectType
// PlainShape is the plain shape for this object. Only valid if IsInputShape returns true.
PlainShape *ObjectType
properties map[string]*Property
}
// IsPlainShape returns true if this object type is the plain shape of a (plain, input)
// pair. The plain shape of an object does not contain *InputType values and only
// references other plain shapes.
func (t *ObjectType) IsPlainShape() bool {
return t.PlainShape == nil
}
// IsInputShape returns true if this object type is the input shape of a (plain, input)
// pair. The input shape of an object may contain *InputType values and may
// reference other input shapes.
func (t *ObjectType) IsInputShape() bool {
return t.PlainShape != nil
}
func (t *ObjectType) Property(name string) (*Property, bool) {
if t.properties == nil && len(t.Properties) > 0 {
t.properties = make(map[string]*Property)
for _, p := range t.Properties {
t.properties[p.Name] = p
}
}
p, ok := t.properties[name]
return p, ok
}
func (t *ObjectType) String() string {
if t.PlainShape != nil {
return t.Token + "•Input"
}
return t.Token
}
func (*ObjectType) isType() {}
type ResourceType struct {
// Token is the type's Pulumi type token.
Token string
// Resource is the type's underlying resource.
Resource *Resource
}
func (t *ResourceType) String() string {
return t.Token
}
func (t *ResourceType) isType() {}
// TokenType represents an opaque type that is referred to only by its token. A TokenType may have an underlying type
// that can be used in place of the token.
type TokenType struct {
// Token is the type's Pulumi type token.
Token string
// Underlying type is the type's underlying type, if any.
UnderlyingType Type
}
func (t *TokenType) String() string {
return t.Token
}
func (*TokenType) isType() {}
// InputType represents a type that accepts either a prompt value or an output value.
type InputType struct {
// ElementType is the element type of the input.
ElementType Type
}
func (t *InputType) String() string {
return fmt.Sprintf("Input<%v>", t.ElementType)
}
func (*InputType) isType() {}
// OptionalType represents a type that accepts an optional value.
type OptionalType struct {
// ElementType is the element type of the input.
ElementType Type
}
func (t *OptionalType) String() string {
return fmt.Sprintf("Optional<%v>", t.ElementType)
}
func (*OptionalType) isType() {}
// DefaultValue describes a default value for a property.
type DefaultValue struct {
// Value specifies a static default value, if any. This value must be representable in the Pulumi schema type
// system, and its type must be assignable to that of the property to which the default applies.
Value interface{}
// Environment specifies a set of environment variables to probe for a default value.
Environment []string
// Language specifies additional language-specific data about the default value.
Language map[string]interface{}
}
// Property describes an object or resource property.
type Property struct {
// Name is the name of the property.
Name string
// Comment is the description of the property, if any.
Comment string
// Type is the type of the property.
Type Type
// ConstValue is the constant value for the property, if any.
ConstValue interface{}
// DefaultValue is the default value for the property, if any.
DefaultValue *DefaultValue
// DeprecationMessage indicates whether or not the property is deprecated.
DeprecationMessage string
// Language specifies additional language-specific data about the property.
Language map[string]interface{}
// Secret is true if the property is secret (default false).
Secret bool
// ReplaceOnChanges specifies if the property is to be replaced instead of updated (default false).
ReplaceOnChanges bool
// WillReplaceOnChanges indicates that the provider will replace the resource when
// this property is changed. This property is used exclusively for docs.
WillReplaceOnChanges bool
Plain bool
}
// IsRequired returns true if this property is required (i.e. its type is not Optional).
func (p *Property) IsRequired() bool {
_, optional := p.Type.(*OptionalType)
return !optional
}
// Alias describes an alias for a Pulumi resource.
type Alias struct {
// This is true if the alias is an old style object alias, and should be written back out as such.
compatibility bool
// The type alias.
Type string
}
// Resource describes a Pulumi resource.
type Resource struct {
// PackageReference is the PackageReference that defines the resource.
PackageReference PackageReference
// Token is the resource's Pulumi type token.
Token string
// Comment is the description of the resource, if any.
Comment string
// IsProvider is true if the resource is a provider resource.
IsProvider bool
// InputProperties is the list of the resource's input properties.
InputProperties []*Property
// Properties is the list of the resource's output properties. This should be a superset of the input properties.
Properties []*Property
// StateInputs is the set of inputs used to get an existing resource, if any.
StateInputs *ObjectType
// Aliases is the list of aliases for the resource.
Aliases []*Alias
// DeprecationMessage indicates whether or not the resource is deprecated.
DeprecationMessage string
// Language specifies additional language-specific data about the resource.
Language map[string]interface{}
// IsComponent indicates whether the resource is a ComponentResource.
IsComponent bool
// Methods is the list of methods for the resource.
Methods []*Method
// IsOverlay indicates whether the type is an overlay provided by the package. Overlay code is generated by the
// package rather than using the core Pulumi codegen libraries.
IsOverlay bool
// OverlaySupportedLanguages indicates what languages the overlay supports. This only has an effect if
// the Resource is an Overlay (IsOverlay == true).
// Supported values are "nodejs", "python", "go", "csharp", "java", "yaml"
OverlaySupportedLanguages []string
}
// The set of resource paths where ReplaceOnChanges is true.
//
// For example, if you have the following resource struct:
//
// Resource A {
//
// Properties: {
// Object B {
// Object D: {
// ReplaceOnChanges: true
// }
// Object F: {}
// }
// Object C {
// ReplaceOnChanges: true
// }
// }
// }
//
// A.ReplaceOnChanges() == [[B, D], [C]]
func (r *Resource) ReplaceOnChanges() (changes [][]*Property, err []error) {
for _, p := range r.Properties {
if p.ReplaceOnChanges {
changes = append(changes, []*Property{p})
} else {
stack := map[string]struct{}{p.Type.String(): {}}
childChanges, errList := replaceOnChangesType(p.Type, &stack)
err = append(err, errList...)
for _, c := range childChanges {
changes = append(changes, append([]*Property{p}, c...))
}
}
}
for i, e := range err {
err[i] = fmt.Errorf("Failed to genereate full `ReplaceOnChanges`: %w", e)
}
return changes, err
}
func replaceOnChangesType(t Type, stack *map[string]struct{}) ([][]*Property, []error) {
var errTmp []error
if o, ok := t.(*OptionalType); ok {
return replaceOnChangesType(o.ElementType, stack)
} else if o, ok := t.(*ObjectType); ok {
changes := [][]*Property{}
err := []error{}
for _, p := range o.Properties {
if p.ReplaceOnChanges {
changes = append(changes, []*Property{p})
} else if _, ok := (*stack)[p.Type.String()]; !ok {
// We handle recursive objects
(*stack)[p.Type.String()] = struct{}{}
var object [][]*Property
object, errTmp = replaceOnChangesType(p.Type, stack)
err = append(err, errTmp...)
for _, path := range object {
changes = append(changes, append([]*Property{p}, path...))
}
delete(*stack, p.Type.String())
} else {
err = append(err, fmt.Errorf("Found recursive object %q", p.Name))
}
}
// We don't want to emit errors where replaceOnChanges is not used.
if len(changes) == 0 {
return nil, nil
}
return changes, err
} else if a, ok := t.(*ArrayType); ok {
// This looks for types internal to the array, not a property of the array.
return replaceOnChangesType(a.ElementType, stack)
} else if m, ok := t.(*MapType); ok {
// This looks for types internal to the map, not a property of the array.
return replaceOnChangesType(m.ElementType, stack)
}
return nil, nil
}
// Joins the output of `ReplaceOnChanges` into property path names.
//
// For example, given an input [[B, D], [C]] where each property has a name
// equivalent to it's variable, this function should yield: ["B.D", "C"]
func PropertyListJoinToString(propertyList [][]*Property, nameConverter func(string) string) []string {
var nonOptional func(Type) Type
nonOptional = func(t Type) Type {
if o, ok := t.(*OptionalType); ok {
return nonOptional(o.ElementType)
}
return t
}
out := make([]string, len(propertyList))
for i, p := range propertyList {
names := make([]string, len(p))
for j, n := range p {
if _, ok := nonOptional(n.Type).(*ArrayType); ok {
names[j] = nameConverter(n.Name) + "[*]"
} else if _, ok := nonOptional(n.Type).(*MapType); ok {
names[j] = nameConverter(n.Name) + ".*"
} else {
names[j] = nameConverter(n.Name)
}
}
out[i] = strings.Join(names, ".")
}
return out
}
type Method struct {
// Name is the name of the method.
Name string
// Function is the function definition for the method.
Function *Function
}
// Function describes a Pulumi function.
type Function struct {
// PackageReference is the PackageReference that defines the function.
PackageReference PackageReference
// Token is the function's Pulumi type token.
Token string
// Comment is the description of the function, if any.
Comment string
// Inputs is the bag of input values for the function, if any.
Inputs *ObjectType
// Determines whether the input bag should be treated as a single argument or as multiple arguments.
MultiArgumentInputs bool
// Outputs is the bag of output values for the function, if any.
Outputs *ObjectType
// The return type of the function, if any.
ReturnType Type
// The return type is plain and not wrapped in an Output.
ReturnTypePlain bool
// When InlineObjectAsReturnType is true, it means that the return type definition is defined inline
// as an object type that should be generated as a separate type and it is not
// a reference to a existing type in the schema.
InlineObjectAsReturnType bool
// DeprecationMessage indicates whether or not the function is deprecated.
DeprecationMessage string
// Language specifies additional language-specific data about the function.
Language map[string]interface{}
// IsMethod indicates whether the function is a method of a resource.
IsMethod bool
// IsOverlay indicates whether the function is an overlay provided by the package. Overlay code is generated by the
// package rather than using the core Pulumi codegen libraries.
IsOverlay bool
// OverlaySupportedLanguages indicates what languages the overlay supports. This only has an effect if
// the Resource is an Overlay (IsOverlay == true).
// Supported values are "nodejs", "python", "go", "csharp", "java", "yaml"
OverlaySupportedLanguages []string
}
// NeedsOutputVersion determines if codegen should emit a ${fn}Output version that
// automatically accepts Inputs and returns ReturnType.
func (fun *Function) NeedsOutputVersion() bool {
// Skip functions that return no value. Arguably we could
// support them and return `Task`, but there are no such
// functions in `pulumi-azure-native` or `pulumi-aws` so we
// omit to simplify.
return fun.ReturnType != nil
}
// BaseProvider
type BaseProvider struct {
// Name is the name of the provider.
Name string
// Version is the version of the provider.
Version semver.Version
}
type Parameterization struct {
BaseProvider BaseProvider
// Parameter is the parameter for the provider.
Parameter []byte
}
// Package describes a Pulumi package.
type Package struct {
// True if this package should be written in the new style to support pack and conformance testing.
SupportPack bool
moduleFormat *regexp.Regexp
// Name is the unqualified name of the package (e.g. "aws", "azure", "gcp", "kubernetes". "random")
Name string
// DisplayName is the human-friendly name of the package.
DisplayName string
// Version is the version of the package.
Version *semver.Version
// Description is the description of the package.
Description string
// Keywords is the list of keywords that are associated with the package, if any.
// Some reserved keywords can be specified as well that help with categorizing the
// package in the Pulumi registry. `category/<name>` and `kind/<type>` are the only
// reserved keywords at this time, where `<name>` can be one of:
// `cloud`, `database`, `infrastructure`, `monitoring`, `network`, `utility`, `vcs`
// and `<type>` is either `native` or `component`. If the package is a bridged Terraform
// provider, then don't include the `kind/` label.
Keywords []string
// Homepage is the package's homepage.
Homepage string
// License indicates which license is used for the package's contents.
License string
// Attribution allows freeform text attribution of derived work, if needed.
Attribution string
// Repository is the URL at which the source for the package can be found.
Repository string
// LogoURL is the URL for the package's logo, if any.
LogoURL string
// PluginDownloadURL is the URL to use to acquire the provider plugin binary, if any.
PluginDownloadURL string
// Publisher is the name of the person or organization that authored and published the package.
Publisher string
// Namespace is the namespace of the package, that's used to diambiguate the package name.
Namespace string
// A list of allowed package name in addition to the Name property.
AllowedPackageNames []string
// Types is the list of non-resource types defined by the package.
Types []Type
// Config is the set of configuration properties defined by the package.
Config []*Property
// Provider is the resource provider for the package, if any.
Provider *Resource
// Resources is the list of resource types defined by the package.
Resources []*Resource
// Functions is the list of functions defined by the package.
Functions []*Function
// Language specifies additional language-specific data about the package.
Language map[string]interface{}
// Dependencies specifies the dependencies of the package
Dependencies []PackageDescriptor
// Parameterization is the optional parameterization for the package, if any.
Parameterization *Parameterization
resourceTable map[string]*Resource
resourceTypeTable map[string]*ResourceType
functionTable map[string]*Function
typeTable map[string]Type
importedLanguages map[string]struct{}
}
// Language provides hooks for importing language-specific metadata in a package.
type Language interface {
// ImportDefaultSpec decodes language-specific metadata associated with a DefaultValue.
ImportDefaultSpec(bytes json.RawMessage) (any, error)
// ImportPropertySpec decodes language-specific metadata associated with a Property.
ImportPropertySpec(bytes json.RawMessage) (interface{}, error)
// ImportObjectTypeSpec decodes language-specific metadata associated with a ObjectType.
ImportObjectTypeSpec(bytes json.RawMessage) (interface{}, error)
// ImportResourceSpec decodes language-specific metadata associated with a Resource.
ImportResourceSpec(bytes json.RawMessage) (interface{}, error)
// ImportFunctionSpec decodes language-specific metadata associated with a Function.
ImportFunctionSpec(bytes json.RawMessage) (interface{}, error)
// ImportPackageSpec decodes language-specific metadata associated with a Package.
ImportPackageSpec(bytes json.RawMessage) (interface{}, error)
}
func sortedLanguageNames(metadata map[string]interface{}) []string {
names := slice.Prealloc[string](len(metadata))
for lang := range metadata {
names = append(names, lang)
}
sort.Strings(names)
return names
}
func importDefaultLanguages(def *DefaultValue, languages map[string]Language) error {
for _, name := range sortedLanguageNames(def.Language) {
val := def.Language[name]
if raw, ok := val.(json.RawMessage); ok {
if lang, ok := languages[name]; ok {
val, err := lang.ImportDefaultSpec(raw)
if err != nil {
return fmt.Errorf("importing %v metadata: %w", name, err)
}
def.Language[name] = val
}
}
}
return nil
}
func importPropertyLanguages(property *Property, languages map[string]Language) error {
if property.DefaultValue != nil {
if err := importDefaultLanguages(property.DefaultValue, languages); err != nil {
return fmt.Errorf("importing default value: %w", err)
}
}
for _, name := range sortedLanguageNames(property.Language) {
val := property.Language[name]
if raw, ok := val.(json.RawMessage); ok {
if lang, ok := languages[name]; ok {
val, err := lang.ImportPropertySpec(raw)
if err != nil {
return fmt.Errorf("importing %v metadata: %w", name, err)
}
property.Language[name] = val
}
}
}
return nil
}
func importObjectTypeLanguages(object *ObjectType, languages map[string]Language) error {
for _, property := range object.Properties {
if err := importPropertyLanguages(property, languages); err != nil {
return fmt.Errorf("importing property %v: %w", property.Name, err)
}
}
for _, name := range sortedLanguageNames(object.Language) {
val := object.Language[name]
if raw, ok := val.(json.RawMessage); ok {
if lang, ok := languages[name]; ok {
val, err := lang.ImportObjectTypeSpec(raw)
if err != nil {
return fmt.Errorf("importing %v metadata: %w", name, err)
}
object.Language[name] = val
}
}
}
return nil
}
func importResourceLanguages(resource *Resource, languages map[string]Language) error {
for _, property := range resource.InputProperties {
if err := importPropertyLanguages(property, languages); err != nil {
return fmt.Errorf("importing input property %v: %w", property.Name, err)
}
}
for _, property := range resource.Properties {
if err := importPropertyLanguages(property, languages); err != nil {
return fmt.Errorf("importing property %v: %w", property.Name, err)
}
}
if resource.StateInputs != nil {
for _, property := range resource.StateInputs.Properties {
if err := importPropertyLanguages(property, languages); err != nil {
return fmt.Errorf("importing state input property %v: %w", property.Name, err)
}
}
}
for _, name := range sortedLanguageNames(resource.Language) {
val := resource.Language[name]
if raw, ok := val.(json.RawMessage); ok {
if lang, ok := languages[name]; ok {
val, err := lang.ImportResourceSpec(raw)
if err != nil {
return fmt.Errorf("importing %v metadata: %w", name, err)
}
resource.Language[name] = val
}
}
}
return nil
}
func importFunctionLanguages(function *Function, languages map[string]Language) error {
if function.Inputs != nil {
if err := importObjectTypeLanguages(function.Inputs, languages); err != nil {
return fmt.Errorf("importing inputs: %w", err)
}
}
if function.ReturnType != nil {
if objectType, ok := function.ReturnType.(*ObjectType); ok && objectType != nil {
if err := importObjectTypeLanguages(objectType, languages); err != nil {
return fmt.Errorf("importing outputs: %w", err)
}
}
}
for _, name := range sortedLanguageNames(function.Language) {
val := function.Language[name]
if raw, ok := val.(json.RawMessage); ok {
if lang, ok := languages[name]; ok {
val, err := lang.ImportFunctionSpec(raw)
if err != nil {
return fmt.Errorf("importing %v metadata: %w", name, err)
}
function.Language[name] = val
}
}
}
return nil
}
func (pkg *Package) ImportLanguages(languages map[string]Language) error {
if pkg.importedLanguages == nil {
pkg.importedLanguages = map[string]struct{}{}
}
found := false
for lang := range languages {
if _, ok := pkg.importedLanguages[lang]; !ok {
found = true
break
}
}
if !found {
return nil
}
for _, t := range pkg.Types {
if object, ok := t.(*ObjectType); ok {
if err := importObjectTypeLanguages(object, languages); err != nil {
return fmt.Errorf("importing object type %v: %w", object.Token, err)
}
}
}
for _, config := range pkg.Config {
if err := importPropertyLanguages(config, languages); err != nil {
return fmt.Errorf("importing configuration property %v: %w", config.Name, err)
}
}
if pkg.Provider != nil {
if err := importResourceLanguages(pkg.Provider, languages); err != nil {
return fmt.Errorf("importing provider: %w", err)
}
}
for _, resource := range pkg.Resources {
if err := importResourceLanguages(resource, languages); err != nil {
return fmt.Errorf("importing resource %v: %w", resource.Token, err)
}
}
for _, function := range pkg.Functions {
if err := importFunctionLanguages(function, languages); err != nil {
return fmt.Errorf("importing function %v: %w", function.Token, err)
}
}
for _, name := range sortedLanguageNames(pkg.Language) {
val := pkg.Language[name]
if raw, ok := val.(json.RawMessage); ok {
if lang, ok := languages[name]; ok {
val, err := lang.ImportPackageSpec(raw)
if err != nil {
return fmt.Errorf("importing %v metadata: %w", name, err)
}
pkg.Language[name] = val
}
}
}
for lang := range languages {
pkg.importedLanguages[lang] = struct{}{}
}
return nil
}
func packageIdentity(name string, version *semver.Version) string {
// The package's identity is its name and version (if any) separated buy a ':'. The ':' character is not allowed
// in package names and so is safe to use as a separator.
id := name + ":"
if version != nil {
return id + version.String()
}
return id
}
func (pkg *Package) Identity() string {
return packageIdentity(pkg.Name, pkg.Version)
}
func (pkg *Package) Equals(other *Package) bool {
return pkg == other || pkg.Identity() == other.Identity()
}
var defaultModuleFormat = regexp.MustCompile("(.*)")
func (pkg *Package) TokenToModule(tok string) string {
// token := pkg ":" module ":" member
components := strings.Split(tok, ":")
if len(components) != 3 {
return ""
}
switch components[1] {
case "providers":
return ""
default:
format := pkg.moduleFormat
if format == nil {
format = defaultModuleFormat
}
matches := format.FindStringSubmatch(components[1])
if len(matches) < 2 || strings.HasPrefix(matches[1], "index") {
return ""
}
return matches[1]
}
}
func TokenToRuntimeModule(tok string) string {
// token := pkg ":" module ":" member
components := strings.Split(tok, ":")
if len(components) != 3 {
return ""
}
return components[1]
}
func (pkg *Package) TokenToRuntimeModule(tok string) string {
return TokenToRuntimeModule(tok)
}
func (pkg *Package) GetResource(token string) (*Resource, bool) {
r, ok := pkg.resourceTable[token]
return r, ok
}
func (pkg *Package) GetFunction(token string) (*Function, bool) {
f, ok := pkg.functionTable[token]
return f, ok
}
func (pkg *Package) GetResourceType(token string) (*ResourceType, bool) {
t, ok := pkg.resourceTypeTable[token]
return t, ok
}
func (pkg *Package) GetType(token string) (Type, bool) {
t, ok := pkg.typeTable[token]
return t, ok
}
func (pkg *Package) Reference() PackageReference {
return packageDefRef{pkg: pkg}
}
func (pkg *Package) MarshalSpec() (spec *PackageSpec, err error) {
version := ""
if pkg.Version != nil {
version = pkg.Version.String()
}
var metadata *MetadataSpec
// Don't set support pack in meta spec if Parameterization is present because that implictly sets
// SupportPack when reading back in anyway.
supportPack := pkg.SupportPack && pkg.Parameterization == nil
if pkg.moduleFormat != nil || supportPack {
metadata = &MetadataSpec{SupportPack: supportPack}
if pkg.moduleFormat != nil {
metadata.ModuleFormat = pkg.moduleFormat.String()
}
}
var parameterization *ParameterizationSpec
if pkg.Parameterization != nil {
parameterization = &ParameterizationSpec{
BaseProvider: BaseProviderSpec{
Name: pkg.Parameterization.BaseProvider.Name,
Version: pkg.Parameterization.BaseProvider.Version.String(),
},
Parameter: pkg.Parameterization.Parameter,
}
}
spec = &PackageSpec{
Name: pkg.Name,
Version: version,
DisplayName: pkg.DisplayName,
Publisher: pkg.Publisher,
Namespace: pkg.Namespace,
Description: pkg.Description,
Keywords: pkg.Keywords,
Homepage: pkg.Homepage,
License: pkg.License,
Attribution: pkg.Attribution,
Repository: pkg.Repository,
LogoURL: pkg.LogoURL,
PluginDownloadURL: pkg.PluginDownloadURL,
Meta: metadata,
Dependencies: pkg.Dependencies,
Types: map[string]ComplexTypeSpec{},
Resources: map[string]ResourceSpec{},
Functions: map[string]FunctionSpec{},
AllowedPackageNames: pkg.AllowedPackageNames,
Parameterization: parameterization,
}
lang, err := marshalLanguage(pkg.Language)
if err != nil {
return nil, fmt.Errorf("marshaling package language: %w", err)
}
spec.Language = lang
spec.Config.Required, spec.Config.Variables, err = pkg.marshalProperties(pkg.Config, true)
if err != nil {
return nil, fmt.Errorf("marshaling package config: %w", err)
}
spec.Provider, err = pkg.marshalResource(pkg.Provider)
if err != nil {
return nil, fmt.Errorf("marshaling provider: %w", err)
}
for _, t := range pkg.Types {
switch t := t.(type) {
case *ObjectType:
if t.IsInputShape() {
continue
}
// Use the input shape when marshaling in order to get the plain annotations right.
o, err := pkg.marshalObject(t.InputShape, false)
if err != nil {
return nil, fmt.Errorf("marshaling type '%v': %w", t.Token, err)
}
spec.Types[t.Token] = o
case *EnumType:
spec.Types[t.Token] = pkg.marshalEnum(t)
}
}
for _, res := range pkg.Resources {
r, err := pkg.marshalResource(res)
if err != nil {
return nil, fmt.Errorf("marshaling resource '%v': %w", res.Token, err)
}
spec.Resources[res.Token] = r
}
for _, fn := range pkg.Functions {
f, err := pkg.marshalFunction(fn)
if err != nil {
return nil, fmt.Errorf("marshaling function '%v': %w", fn.Token, err)
}
spec.Functions[fn.Token] = f
}
return spec, nil
}
func (pkg *Package) MarshalJSON() ([]byte, error) {
spec, err := pkg.MarshalSpec()
if err != nil {
return nil, err
}
return jsonMarshal(spec)
}
func (pkg *Package) MarshalYAML() ([]byte, error) {
spec, err := pkg.MarshalSpec()
if err != nil {
return nil, err
}
var b bytes.Buffer
enc := yaml.NewEncoder(&b)
enc.SetIndent(2)
if err := enc.Encode(spec); err != nil {
return nil, err
}
return b.Bytes(), nil
}
func (pkg *Package) marshalObjectData(comment string, properties []*Property, language map[string]interface{},
plain, isOverlay bool, overlaySupportedLanguages []string,
) (ObjectTypeSpec, error) {
required, props, err := pkg.marshalProperties(properties, plain)
if err != nil {
return ObjectTypeSpec{}, err
}
lang, err := marshalLanguage(language)
if err != nil {
return ObjectTypeSpec{}, err
}
return ObjectTypeSpec{
Description: comment,
Properties: props,
Type: "object",
Required: required,
Language: lang,
IsOverlay: isOverlay,
OverlaySupportedLanguages: overlaySupportedLanguages,
}, nil
}
func (pkg *Package) marshalObject(t *ObjectType, plain bool) (ComplexTypeSpec, error) {
data, err := pkg.marshalObjectData(t.Comment, t.Properties, t.Language, plain, t.IsOverlay, nil)
if err != nil {
return ComplexTypeSpec{}, err
}
return ComplexTypeSpec{ObjectTypeSpec: data}, nil
}
func (pkg *Package) marshalEnum(t *EnumType) ComplexTypeSpec {
values := make([]EnumValueSpec, len(t.Elements))
for i, el := range t.Elements {
values[i] = EnumValueSpec{
Name: el.Name,
Description: el.Comment,
Value: el.Value,
DeprecationMessage: el.DeprecationMessage,
}
}
return ComplexTypeSpec{
ObjectTypeSpec: ObjectTypeSpec{
Description: t.Comment,
Type: pkg.marshalType(t.ElementType, false).Type,
IsOverlay: t.IsOverlay,
},
Enum: values,
}
}
func (pkg *Package) marshalResource(r *Resource) (ResourceSpec, error) {
object, err := pkg.marshalObjectData(r.Comment, r.Properties, r.Language, true, r.IsOverlay,
r.OverlaySupportedLanguages)
if err != nil {
return ResourceSpec{}, fmt.Errorf("marshaling properties: %w", err)
}
requiredInputs, inputs, err := pkg.marshalProperties(r.InputProperties, false)
if err != nil {
return ResourceSpec{}, fmt.Errorf("marshaling input properties: %w", err)
}
var stateInputs *ObjectTypeSpec
if r.StateInputs != nil {
o, err := pkg.marshalObject(r.StateInputs, false)
if err != nil {
return ResourceSpec{}, fmt.Errorf("marshaling state inputs: %w", err)
}
stateInputs = &o.ObjectTypeSpec
}
aliases := slice.Prealloc[AliasSpec](len(r.Aliases))
for _, a := range r.Aliases {
aliases = append(aliases, AliasSpec{
compatibility: a.compatibility,
Type: a.Type,
})
}
var methods map[string]string
if len(r.Methods) != 0 {
methods = map[string]string{}
for _, m := range r.Methods {
methods[m.Name] = m.Function.Token
}
}
return ResourceSpec{
ObjectTypeSpec: object,
InputProperties: inputs,
RequiredInputs: requiredInputs,
StateInputs: stateInputs,
Aliases: aliases,
DeprecationMessage: r.DeprecationMessage,
IsComponent: r.IsComponent,
Methods: methods,
}, nil
}
func (pkg *Package) marshalFunction(f *Function) (FunctionSpec, error) {
var inputs *ObjectTypeSpec
if f.Inputs != nil {
ins, err := pkg.marshalObject(f.Inputs, true)
if err != nil {
return FunctionSpec{}, fmt.Errorf("marshaling inputs: %w", err)
}
inputs = &ins.ObjectTypeSpec
}
var multiArgumentInputs []string
if f.MultiArgumentInputs {
multiArgumentInputs = make([]string, len(f.Inputs.Properties))
for i, prop := range f.Inputs.Properties {
multiArgumentInputs[i] = prop.Name
}
}
var outputs *ObjectTypeSpec
if f.Outputs != nil {
outs, err := pkg.marshalObject(f.Outputs, true)
if err != nil {
return FunctionSpec{}, fmt.Errorf("marshaling outputs: %w", err)
}
outputs = &outs.ObjectTypeSpec
}
var returnType *ReturnTypeSpec
if f.ReturnType != nil {
returnType = &ReturnTypeSpec{}
if objectType, ok := f.ReturnType.(*ObjectType); ok {
ret, err := pkg.marshalObject(objectType, true)
if err != nil {
return FunctionSpec{}, fmt.Errorf("marshaling object spec: %w", err)
}
returnType.ObjectTypeSpec = &ret.ObjectTypeSpec
if f.ReturnTypePlain {
returnType.ObjectTypeSpecIsPlain = true
}
} else {
typeSpec := pkg.marshalType(f.ReturnType, true)
returnType.TypeSpec = &typeSpec
if f.ReturnTypePlain {
returnType.TypeSpec.Plain = true
}
}
}
lang, err := marshalLanguage(f.Language)
if err != nil {
return FunctionSpec{}, err
}
return FunctionSpec{
Description: f.Comment,
DeprecationMessage: f.DeprecationMessage,
IsOverlay: f.IsOverlay,
OverlaySupportedLanguages: f.OverlaySupportedLanguages,
Inputs: inputs,
MultiArgumentInputs: multiArgumentInputs,
Outputs: outputs,
ReturnType: returnType,
Language: lang,
}, nil
}
func (pkg *Package) marshalProperties(props []*Property, plain bool) (required []string, specs map[string]PropertySpec,
err error,
) {
if len(props) == 0 {
return
}
specs = make(map[string]PropertySpec, len(props))
for _, p := range props {
typ := p.Type
if t, optional := typ.(*OptionalType); optional {
typ = t.ElementType
} else {
required = append(required, p.Name)
}
var defaultValue interface{}
var defaultSpec *DefaultSpec
if p.DefaultValue != nil {
defaultValue = p.DefaultValue.Value
if len(p.DefaultValue.Environment) != 0 || len(p.DefaultValue.Language) != 0 {
lang, err := marshalLanguage(p.DefaultValue.Language)
if err != nil {
return nil, nil, fmt.Errorf("property '%v': %w", p.Name, err)
}
defaultSpec = &DefaultSpec{
Environment: p.DefaultValue.Environment,
Language: lang,
}
}
}
lang, err := marshalLanguage(p.Language)
if err != nil {
return nil, nil, fmt.Errorf("property '%v': %w", p.Name, err)
}
propertyType := pkg.marshalType(typ, plain)
propertyType.Plain = p.Plain
specs[p.Name] = PropertySpec{
TypeSpec: propertyType,
Description: p.Comment,
Const: p.ConstValue,
Default: defaultValue,
DefaultInfo: defaultSpec,
DeprecationMessage: p.DeprecationMessage,
Language: lang,
Secret: p.Secret,
ReplaceOnChanges: p.ReplaceOnChanges,
WillReplaceOnChanges: p.WillReplaceOnChanges,
}
}
return required, specs, nil
}
// marshalType marshals the given type into a TypeSpec. If plain is true, then the type is being marshaled within a
// plain type context (e.g. a resource output property or a function input/output object type), and therefore does not
// require `Plain` annotations (hence the odd-looking `Plain: !plain` fields below).
func (pkg *Package) marshalType(t Type, plain bool) TypeSpec {
switch t := t.(type) {
case *InputType:
el := pkg.marshalType(t.ElementType, false)
el.Plain = false
return el
case *ArrayType:
el := pkg.marshalType(t.ElementType, plain)
return TypeSpec{
Type: "array",
Items: &el,
Plain: !plain,
}
case *MapType:
el := pkg.marshalType(t.ElementType, plain)
return TypeSpec{
Type: "object",
AdditionalProperties: &el,
Plain: !plain,
}
case *UnionType:
oneOf := make([]TypeSpec, len(t.ElementTypes))
for i, el := range t.ElementTypes {
oneOf[i] = pkg.marshalType(el, plain)
}
defaultType := ""
if t.DefaultType != nil {
defaultType = pkg.marshalType(t.DefaultType, plain).Type
}
var discriminator *DiscriminatorSpec
if t.Discriminator != "" {
discriminator = &DiscriminatorSpec{
PropertyName: t.Discriminator,
Mapping: t.Mapping,
}
}
return TypeSpec{
Type: defaultType,
OneOf: oneOf,
Discriminator: discriminator,
Plain: !plain,
}
case *ObjectType:
return TypeSpec{
Ref: pkg.marshalTypeRef(t.PackageReference, "types", t.Token),
Plain: !plain,
}
case *EnumType:
return TypeSpec{
Ref: pkg.marshalTypeRef(t.PackageReference, "types", t.Token),
Plain: !plain,
}
case *ResourceType:
return TypeSpec{
Ref: pkg.marshalTypeRef(t.Resource.PackageReference, "resources", t.Token),
Plain: !plain,
}
case *TokenType:
var defaultType string
if t.UnderlyingType != nil {
defaultType = pkg.marshalType(t.UnderlyingType, plain).Type
}
return TypeSpec{
Type: defaultType,
Ref: pkg.marshalTypeRef(pkg.Reference(), "types", t.Token),
Plain: !plain,
}
default:
switch t {
case BoolType:
return TypeSpec{
Type: "boolean",
Plain: !plain,
}
case StringType:
return TypeSpec{
Type: "string",
Plain: !plain,
}
case IntType:
return TypeSpec{
Type: "integer",
Plain: !plain,
}
case NumberType:
return TypeSpec{
Type: "number",
Plain: !plain,
}
case AnyType:
return TypeSpec{
Ref: "pulumi.json#/Any",
Plain: !plain,
}
case ArchiveType:
return TypeSpec{
Ref: "pulumi.json#/Archive",
Plain: !plain,
}
case AssetType:
return TypeSpec{
Ref: "pulumi.json#/Asset",
Plain: !plain,
}
case JSONType:
return TypeSpec{
Ref: "pulumi.json#/Json",
Plain: !plain,
}
default:
panic(fmt.Errorf("unexepcted type %v (%T)", t, t))
}
}
}
func (pkg *Package) marshalTypeRef(container PackageReference, section, token string) string {
token = url.PathEscape(token)
if p, err := container.Definition(); err == nil && p == pkg {
return fmt.Sprintf("#/%s/%s", section, token)
}
// TODO(schema): this isn't quite right--it doesn't handle schemas sourced from URLs--but it's good enough for now.
return fmt.Sprintf("/%s/v%v/schema.json#/%s/%s", container.Name(), container.Version(), section, token)
}
func marshalLanguage(lang map[string]interface{}) (map[string]RawMessage, error) {
if len(lang) == 0 {
return nil, nil
}
result := map[string]RawMessage{}
for name, data := range lang {
bytes, err := jsonMarshal(data)
if err != nil {
return nil, fmt.Errorf("marshaling %v language data: %w", name, err)
}
result[name] = RawMessage(bytes)
}
return result, nil
}
func jsonMarshal(v interface{}) ([]byte, error) {
var b bytes.Buffer
enc := json.NewEncoder(&b)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
if err := enc.Encode(v); err != nil {
return nil, err
}
return b.Bytes(), nil
}
type RawMessage []byte
func (m RawMessage) MarshalJSON() ([]byte, error) {
return []byte(m), nil
}
func (m *RawMessage) UnmarshalJSON(bytes []byte) error {
*m = make([]byte, len(bytes))
copy(*m, bytes)
return nil
}
func (m RawMessage) MarshalYAML() ([]byte, error) {
return []byte(m), nil
}
func (m *RawMessage) UnmarshalYAML(node *yaml.Node) error {
var value interface{}
if err := node.Decode(&value); err != nil {
return err
}
bytes, err := jsonMarshal(value)
if err != nil {
return err
}
*m = bytes
return nil
}
// TypeSpec is the serializable form of a reference to a type.
type TypeSpec struct {
// Type is the primitive or composite type, if any. May be "boolean", "string", "integer", "number", "array", or
// "object".
Type string `json:"type,omitempty" yaml:"type,omitempty"`
// Ref is a reference to a type in this or another document. For example, the built-in Archive, Asset, and Any
// types are referenced as "pulumi.json#/Archive", "pulumi.json#/Asset", and "pulumi.json#/Any", respectively.
// A type from this document is referenced as "#/types/pulumi:type:token".
// A type from another document is referenced as "path#/types/pulumi:type:token", where path is of the form:
// "/provider/vX.Y.Z/schema.json" or "pulumi.json" or "http[s]://example.com/provider/vX.Y.Z/schema.json"
// A resource from this document is referenced as "#/resources/pulumi:type:token".
// A resource from another document is referenced as "path#/resources/pulumi:type:token", where path is of the form:
// "/provider/vX.Y.Z/schema.json" or "pulumi.json" or "http[s]://example.com/provider/vX.Y.Z/schema.json"
Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
// AdditionalProperties, if set, describes the element type of an "object" (i.e. a string -> value map).
AdditionalProperties *TypeSpec `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"`
// Items, if set, describes the element type of an array.
Items *TypeSpec `json:"items,omitempty" yaml:"items,omitempty"`
// OneOf indicates that values of the type may be one of any of the listed types.
OneOf []TypeSpec `json:"oneOf,omitempty" yaml:"oneOf,omitempty"`
// Discriminator informs the consumer of an alternative schema based on the value associated with it.
Discriminator *DiscriminatorSpec `json:"discriminator,omitempty" yaml:"discriminator,omitempty"`
// Plain indicates that when used as an input, this type does not accept eventual values.
Plain bool `json:"plain,omitempty" yaml:"plain,omitempty"`
}
// DiscriminatorSpec informs the consumer of an alternative schema based on the value associated with it.
type DiscriminatorSpec struct {
// PropertyName is the name of the property in the payload that will hold the discriminator value.
PropertyName string `json:"propertyName" yaml:"propertyName"`
// Mapping is an optional object to hold mappings between payload values and schema names or references.
Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"`
}
// DefaultSpec is the serializable form of extra information about the default value for a property.
type DefaultSpec struct {
// Environment specifies a set of environment variables to probe for a default value.
Environment []string `json:"environment,omitempty" yaml:"environment,omitempty"`
// Language specifies additional language-specific data about the default value.
Language map[string]RawMessage `json:"language,omitempty" yaml:"language,omitempty"`
}
// PropertySpec is the serializable form of an object or resource property.
type PropertySpec struct {
TypeSpec `yaml:",inline"`
// Description is the description of the property, if any.
Description string `json:"description,omitempty" yaml:"description,omitempty"`
// Const is the constant value for the property, if any. The type of the value must be assignable to the type of
// the property.
Const interface{} `json:"const,omitempty" yaml:"const,omitempty"`
// Default is the default value for the property, if any. The type of the value must be assignable to the type of
// the property.
Default interface{} `json:"default,omitempty" yaml:"default,omitempty"`
// DefaultInfo contains additional information about the property's default value, if any.
DefaultInfo *DefaultSpec `json:"defaultInfo,omitempty" yaml:"defaultInfo,omitempty"`
// DeprecationMessage indicates whether or not the property is deprecated.
DeprecationMessage string `json:"deprecationMessage,omitempty" yaml:"deprecationMessage,omitempty"`
// Language specifies additional language-specific data about the property.
Language map[string]RawMessage `json:"language,omitempty" yaml:"language,omitempty"`
// Secret specifies if the property is secret (default false).
Secret bool `json:"secret,omitempty" yaml:"secret,omitempty"`
// ReplaceOnChanges specifies if the property is to be replaced instead of updated (default false).
ReplaceOnChanges bool `json:"replaceOnChanges,omitempty" yaml:"replaceOnChanges,omitempty"`
// WillReplaceOnChanges indicates that the provider will replace the resource when
// this property is changed. This property is used exclusively for docs.
WillReplaceOnChanges bool `json:"willReplaceOnChanges,omitempty" yaml:"willReplaceOnChanges,omitempty"`
}
// ObjectTypeSpec is the serializable form of an object type.
type ObjectTypeSpec struct {
// Description is the description of the type, if any.
Description string `json:"description,omitempty" yaml:"description,omitempty"`
// Properties, if present, is a map from property name to PropertySpec that describes the type's properties.
Properties map[string]PropertySpec `json:"properties,omitempty" yaml:"properties,omitempty"`
// Type must be "object" if this is an object type, or the underlying type for an enum.
Type string `json:"type,omitempty" yaml:"type,omitempty"`
// Required, if present, is a list of the names of an object type's required properties. These properties must be set
// for inputs and will always be set for outputs.
Required []string `json:"required,omitempty" yaml:"required,omitempty"`
// Plain, was a list of the names of an object type's plain properties. This property is ignored: instead, property
// types should be marked as plain where necessary.
Plain []string `json:"plain,omitempty" yaml:"plain,omitempty"`
// Language specifies additional language-specific data about the type.
Language map[string]RawMessage `json:"language,omitempty" yaml:"language,omitempty"`
// IsOverlay indicates whether the type is an overlay provided by the package. Overlay code is generated by the
// package rather than using the core Pulumi codegen libraries.
IsOverlay bool `json:"isOverlay,omitempty" yaml:"isOverlay,omitempty"`
// OverlaySupportedLanguages indicates what languages the overlay supports. This only has an effect if
// the Resource is an Overlay (IsOverlay == true).
// Supported values are "nodejs", "python", "go", "csharp", "java", "yaml"
OverlaySupportedLanguages []string `json:"overlaySupportedLanguages,omitempty" yaml:"overlaySupportedLanguages,omitempty"` //nolint:lll
}
// ComplexTypeSpec is the serializable form of an object or enum type.
type ComplexTypeSpec struct {
ObjectTypeSpec `yaml:",inline"`
// Enum, if present, is the list of possible values for an enum type.
Enum []EnumValueSpec `json:"enum,omitempty" yaml:"enum,omitempty"`
}
// EnumValueSpec is the serializable form of the values metadata associated with an enum type.
type EnumValueSpec struct {
// Name, if present, overrides the name of the enum value that would usually be derived from the value.
Name string `json:"name,omitempty" yaml:"name,omitempty"`
// Description of the enum value.
Description string `json:"description,omitempty" yaml:"description,omitempty"`
// Value is the enum value itself.
Value interface{} `json:"value" yaml:"value"`
// DeprecationMessage indicates whether or not the value is deprecated.
DeprecationMessage string `json:"deprecationMessage,omitempty" yaml:"deprecationMessage,omitempty"`
}
// AliasSpec is the serializable form of an alias description.
type AliasSpec struct {
// Type is the type portion of the alias, if any.
Type string `json:"type,omitempty" yaml:"type,omitempty"`
// This is set by the marshaller to indicate that the alias is a string, and to write it back as one.
compatibility bool
}
// AliasSpec can marshal from just a string
func (a *AliasSpec) UnmarshalJSON(data []byte) error {
var s string
err := json.Unmarshal(data, &s)
if err == nil {
a.Type = s
a.compatibility = true
return nil
}
var o struct {
Type string `json:"type"`
}
err = json.Unmarshal(data, &o)
if err == nil {
a.Type = o.Type
return nil
}
return err
}
// AliasSpec can marshal to just a string
func (a AliasSpec) MarshalJSON() ([]byte, error) {
if a.compatibility {
return json.Marshal(a.Type)
}
var o struct {
Type string `json:"type"`
}
o.Type = a.Type
return json.Marshal(&o)
}
// AliasSpec can unmarshal from just a string
func (a *AliasSpec) UnmarshalYAML(node *yaml.Node) error {
var s string
err := node.Decode(&s)
if err == nil {
a.Type = s
a.compatibility = true
return nil
}
var o struct {
Type string `yaml:"type"`
}
err = node.Decode(&o)
if err == nil {
a.Type = o.Type
return nil
}
return err
}
// AliasSpec can marshal to just a string
func (a AliasSpec) MarshalYAML() (interface{}, error) {
if a.compatibility {
return a.Type, nil
}
var o struct {
Type string `yaml:"type"`
}
o.Type = a.Type
return o, nil
}
// ResourceSpec is the serializable form of a resource description.
type ResourceSpec struct {
ObjectTypeSpec `yaml:",inline"`
// InputProperties is a map from property name to PropertySpec that describes the resource's input properties.
InputProperties map[string]PropertySpec `json:"inputProperties,omitempty" yaml:"inputProperties,omitempty"`
// RequiredInputs is a list of the names of the resource's required input properties.
RequiredInputs []string `json:"requiredInputs,omitempty" yaml:"requiredInputs,omitempty"`
// PlainInputs was a list of the names of the resource's plain input properties. This property is ignored:
// instead, property types should be marked as plain where necessary.
PlainInputs []string `json:"plainInputs,omitempty" yaml:"plainInputs,omitempty"`
// StateInputs is an optional ObjectTypeSpec that describes additional inputs that may be necessary to get an
// existing resource. If this is unset, only an ID is necessary.
StateInputs *ObjectTypeSpec `json:"stateInputs,omitempty" yaml:"stateInputs,omitempty"`
// Aliases is the list of aliases for the resource. This can either be a list of strings or a list of objects with
// type fields.
Aliases []AliasSpec `json:"aliases,omitempty" yaml:"aliases,omitempty"`
// DeprecationMessage indicates whether or not the resource is deprecated.
DeprecationMessage string `json:"deprecationMessage,omitempty" yaml:"deprecationMessage,omitempty"`
// IsComponent indicates whether the resource is a ComponentResource.
IsComponent bool `json:"isComponent,omitempty" yaml:"isComponent,omitempty"`
// Methods maps method names to functions in this schema.
Methods map[string]string `json:"methods,omitempty" yaml:"methods,omitempty"`
}
// ReturnTypeSpec is either ObjectTypeSpec or TypeSpec.
type ReturnTypeSpec struct {
ObjectTypeSpec *ObjectTypeSpec
// If ObjectTypeSpec is non-nil, it can also be marked with ObjectTypeSpecIsPlain: true
// indicating that the generated code should not wrap in the result in an Output but return
// it directly. This option is incompatible with marking individual properties with
// ObjectTypSpec.Plain.
ObjectTypeSpecIsPlain bool
TypeSpec *TypeSpec
}
type returnTypeSpecObjectSerialForm struct {
ObjectTypeSpec
Plain any `json:"plain,omitempty"`
}
func (returnTypeSpec *ReturnTypeSpec) marshalJSONLikeObject() (map[string]interface{}, error) {
ts := returnTypeSpec
bytes, err := ts.MarshalJSON()
if err != nil {
return nil, err
}
var r map[string]interface{}
if err := json.Unmarshal(bytes, &r); err != nil {
return nil, err
}
return r, nil
}
func (returnTypeSpec *ReturnTypeSpec) MarshalJSON() ([]byte, error) {
ts := returnTypeSpec
if ts.ObjectTypeSpec != nil {
form := returnTypeSpecObjectSerialForm{
ObjectTypeSpec: *ts.ObjectTypeSpec,
}
if ts.ObjectTypeSpecIsPlain {
form.Plain = true
} else if len(ts.ObjectTypeSpec.Plain) > 0 {
form.Plain = ts.ObjectTypeSpec.Plain
}
return json.Marshal(form)
}
return json.Marshal(ts.TypeSpec)
}
func (returnTypeSpec *ReturnTypeSpec) UnmarshalJSON(inputJSON []byte) error {
ts := returnTypeSpec
var m returnTypeSpecObjectSerialForm
err := json.Unmarshal(inputJSON, &m)
if err == nil {
if m.Properties != nil {
ts.ObjectTypeSpec = &m.ObjectTypeSpec
if plain, ok := m.Plain.(bool); ok && plain {
ts.ObjectTypeSpecIsPlain = true
}
if plain, ok := m.Plain.([]interface{}); ok {
for _, p := range plain {
if ps, ok := p.(string); ok {
ts.ObjectTypeSpec.Plain = append(ts.ObjectTypeSpec.Plain, ps)
}
}
}
return nil
}
}
return json.Unmarshal(inputJSON, &ts.TypeSpec)
}
// Deprecated.
type Decoder func([]byte, interface{}) error
// Deprecated.
func (returnTypeSpec *ReturnTypeSpec) UnmarshalReturnTypeSpec(data []byte, decode Decoder) error {
var objectMap map[string]interface{}
if err := decode(data, &objectMap); err != nil {
return err
}
if len(objectMap) == 0 {
return nil
}
inputJSON, err := json.Marshal(objectMap)
if err != nil {
return err
}
return returnTypeSpec.UnmarshalJSON(inputJSON)
}
// Deprecated.
func (returnTypeSpec *ReturnTypeSpec) UnmarshalYAML(inputYAML []byte) error {
return returnTypeSpec.UnmarshalReturnTypeSpec(inputYAML, yaml.Unmarshal)
}
// FunctionSpec is the serializable form of a function description.
type FunctionSpec struct {
// Description is the description of the function, if any.
Description string `json:"description,omitempty" yaml:"description,omitempty"`
// Inputs is the bag of input values for the function, if any.
Inputs *ObjectTypeSpec `json:"inputs,omitempty" yaml:"inputs,omitempty"`
// Determines whether the input bag should be treated as a single argument or as multiple arguments.
// When MultiArgumentInputs is non-empty, it must match up 1:1 with the property names in of the Inputs object.
// The order in which the properties are listed in MultiArgumentInputs determines the order in which the
// arguments are passed to the function.
MultiArgumentInputs []string `json:"multiArgumentInputs,omitempty" yaml:"multiArgumentInputs,omitempty"`
// Outputs is the bag of output values for the function, if any.
// This field is DEPRECATED. Use ReturnType instead where it allows for more flexible types
// to describe the outputs of the function definition. It is invalid to specify both Outputs and ReturnType.
Outputs *ObjectTypeSpec `json:"outputs,omitempty" yaml:"outputs,omitempty"`
// Specified the return type of the function definition
ReturnType *ReturnTypeSpec
// DeprecationMessage indicates whether the function is deprecated.
DeprecationMessage string `json:"deprecationMessage,omitempty" yaml:"deprecationMessage,omitempty"`
// Language specifies additional language-specific data about the function.
Language map[string]RawMessage `json:"language,omitempty" yaml:"language,omitempty"`
// IsOverlay indicates whether the function is an overlay provided by the package. Overlay code is generated by the
// package rather than using the core Pulumi codegen libraries.
IsOverlay bool `json:"isOverlay,omitempty" yaml:"isOverlay,omitempty"`
// OverlaySupportedLanguages indicates what languages the overlay supports. This only has an effect if
// the Resource is an Overlay (IsOverlay == true).
// Supported values are "nodejs", "python", "go", "csharp", "java", "yaml"
OverlaySupportedLanguages []string `json:"overlaySupportedLanguages,omitempty" yaml:"overlaySupportedLanguages,omitempty"` //nolint:lll
}
func emptyObject(data RawMessage) (bool, error) {
var objectData *map[string]RawMessage
if err := json.Unmarshal(data, &objectData); err != nil {
return false, err
}
if objectData == nil {
return true, nil
}
return len(*objectData) == 0, nil
}
func unmarshalFunctionSpec(funcSpec *FunctionSpec, data map[string]RawMessage) error {
if description, ok := data["description"]; ok {
if err := json.Unmarshal(description, &funcSpec.Description); err != nil {
return err
}
}
if inputs, ok := data["inputs"]; ok {
if err := json.Unmarshal(inputs, &funcSpec.Inputs); err != nil {
return err
}
}
if multiArgumentInputs, ok := data["multiArgumentInputs"]; ok {
if err := json.Unmarshal(multiArgumentInputs, &funcSpec.MultiArgumentInputs); err != nil {
return err
}
}
if returnType, ok := data["outputs"]; ok {
isEmpty, err := emptyObject(returnType)
if err != nil {
return err
}
if !isEmpty {
if err := json.Unmarshal(returnType, &funcSpec.ReturnType); err != nil {
return err
}
} else {
funcSpec.ReturnType = nil
}
}
if deprecationMessage, ok := data["deprecationMessage"]; ok {
if err := json.Unmarshal(deprecationMessage, &funcSpec.DeprecationMessage); err != nil {
return err
}
}
if language, ok := data["language"]; ok {
if err := json.Unmarshal(language, &funcSpec.Language); err != nil {
return err
}
}
if isOverlay, ok := data["isOverlay"]; ok {
if err := json.Unmarshal(isOverlay, &funcSpec.IsOverlay); err != nil {
return err
}
}
if overlaySupportedLanguages, ok := data["overlaySupportedLanguages"]; ok {
if err := json.Unmarshal(overlaySupportedLanguages, &funcSpec.OverlaySupportedLanguages); err != nil {
return err
}
}
return nil
}
// UnmarshalJSON is custom unmarshalling logic for FunctionSpec so that we can derive Outputs from ReturnType
// which otherwise isn't possible when both are retrieved from the same JSON field
func (funcSpec *FunctionSpec) UnmarshalJSON(inputJSON []byte) error {
var data map[string]RawMessage
if err := json.Unmarshal(inputJSON, &data); err != nil {
return err
}
return unmarshalFunctionSpec(funcSpec, data)
}
// UnmarshalYAML is custom unmarshalling logic for FunctionSpec so that we can derive Outputs from ReturnType
// which otherwise isn't possible when both are retrieved from the same JSON field
func (funcSpec *FunctionSpec) UnmarshalYAML(node *yaml.Node) error {
var data map[string]RawMessage
if err := node.Decode(&data); err != nil {
return err
}
return unmarshalFunctionSpec(funcSpec, data)
}
func (funcSpec FunctionSpec) marshalFunctionSpec() (map[string]interface{}, error) {
data := make(map[string]interface{})
if funcSpec.Description != "" {
data["description"] = funcSpec.Description
}
if funcSpec.Inputs != nil {
data["inputs"] = funcSpec.Inputs
}
if len(funcSpec.MultiArgumentInputs) > 0 {
data["multiArgumentInputs"] = funcSpec.MultiArgumentInputs
}
if funcSpec.ReturnType != nil {
rto, err := funcSpec.ReturnType.marshalJSONLikeObject()
if err != nil {
return nil, err
}
data["outputs"] = rto
}
// for backward-compat when we only specify the outputs object of the function
if funcSpec.ReturnType == nil && funcSpec.Outputs != nil {
data["outputs"] = funcSpec.Outputs
}
if funcSpec.DeprecationMessage != "" {
data["deprecationMessage"] = funcSpec.DeprecationMessage
}
if funcSpec.IsOverlay {
// the default is false, so only write the property when it is true
data["isOverlay"] = true
}
if len(funcSpec.OverlaySupportedLanguages) > 0 {
// by default it supports all languages the provider supports, so only write the property when it is not the default
data["overlaySupportedLanguages"] = funcSpec.OverlaySupportedLanguages
}
if len(funcSpec.Language) > 0 {
data["language"] = funcSpec.Language
}
return data, nil
}
func (funcSpec FunctionSpec) MarshalJSON() ([]byte, error) {
data, err := funcSpec.marshalFunctionSpec()
if err != nil {
return nil, err
}
return json.Marshal(data)
}
func (funcSpec FunctionSpec) MarshalYAML() (interface{}, error) {
return funcSpec.marshalFunctionSpec()
}
// ConfigSpec is the serializable description of a package's configuration variables.
type ConfigSpec struct {
// Variables is a map from variable name to PropertySpec that describes a package's configuration variables.
Variables map[string]PropertySpec `json:"variables,omitempty" yaml:"variables,omitempty"`
// Required is a list of the names of the package's required configuration variables.
Required []string `json:"defaults,omitempty" yaml:"defaults,omitempty"`
}
// MetadataSpec contains information for the importer about this package.
type MetadataSpec struct {
// ModuleFormat is a regex that is used by the importer to extract a module name from the module portion of a
// type token. Packages that use the module format "namespace1/namespace2/.../namespaceN" do not need to specify
// a format. The regex must define one capturing group that contains the module name, which must be formatted as
// "namespace1/namespace2/...namespaceN".
ModuleFormat string `json:"moduleFormat,omitempty" yaml:"moduleFormat,omitempty"`
// SupportPack indicates whether or not the package is written to support the pack command. This causes versions to
// be written out, plugin.json files to be filled in, and package metadata to be written to the directory.
// This defaults to false currently, but conformance testing _always_ turns it on.
SupportPack bool `json:"supportPack,omitempty" yaml:"supportPack,omitempty"`
}
// PackageInfoSpec is the serializable description of a Pulumi package's metadata.
type PackageInfoSpec struct {
// Name is the unqualified name of the package (e.g. "aws", "azure", "gcp", "kubernetes", "random")
Name string `json:"name" yaml:"name"`
// DisplayName is the human-friendly name of the package.
DisplayName string `json:"displayName,omitempty" yaml:"displayName,omitempty"`
// Version is the version of the package. The version must be valid semver.
Version string `json:"version,omitempty" yaml:"version,omitempty"`
// Description is the description of the package.
Description string `json:"description,omitempty" yaml:"description,omitempty"`
// Keywords is the list of keywords that are associated with the package, if any.
// Some reserved keywords can be specified as well that help with categorizing the
// package in the Pulumi registry. `category/<name>` and `kind/<type>` are the only
// reserved keywords at this time, where `<name>` can be one of:
// `cloud`, `database`, `infrastructure`, `monitoring`, `network`, `utility`, `vcs`
// and `<type>` is either `native` or `component`. If the package is a bridged Terraform
// provider, then don't include the `kind/` label.
Keywords []string `json:"keywords,omitempty" yaml:"keywords,omitempty"`
// Homepage is the package's homepage.
Homepage string `json:"homepage,omitempty" yaml:"homepage,omitempty"`
// License indicates which license is used for the package's contents.
License string `json:"license,omitempty" yaml:"license,omitempty"`
// Attribution allows freeform text attribution of derived work, if needed.
Attribution string `json:"attribution,omitempty" yaml:"attribution,omitempty"`
// Repository is the URL at which the source for the package can be found.
Repository string `json:"repository,omitempty" yaml:"repository,omitempty"`
// LogoURL is the URL for the package's logo, if any.
LogoURL string `json:"logoUrl,omitempty" yaml:"logoUrl,omitempty"`
// PluginDownloadURL is the URL to use to acquire the provider plugin binary, if any.
PluginDownloadURL string `json:"pluginDownloadURL,omitempty" yaml:"pluginDownloadURL,omitempty"`
// Publisher is the name of the person or organization that authored and published the package.
Publisher string `json:"publisher,omitempty" yaml:"publisher,omitempty"`
// Namespace is the namespace of the package, that's used to diambiguate the package name.
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
// Dependencies is the list of dependencies of the package.
Dependencies []PackageDescriptor `json:"dependencies,omitempty" yaml:"dependencies,omitempty"`
// Meta contains information for the importer about this package.
Meta *MetadataSpec `json:"meta,omitempty" yaml:"meta,omitempty"`
// A list of allowed package name in addition to the Name property.
AllowedPackageNames []string `json:"allowedPackageNames,omitempty" yaml:"allowedPackageNames,omitempty"`
// Language specifies additional language-specific data about the package.
Language map[string]RawMessage `json:"language,omitempty" yaml:"language,omitempty"`
// Parameterization is the optional parameterization for this package.
Parameterization *ParameterizationSpec `json:"parameterization,omitempty" yaml:"parameterization,omitempty"`
}
// BaseProviderSpec is the serializable description of a Pulumi base provider.
type BaseProviderSpec struct {
// The name of the base provider.
Name string `json:"name" yaml:"name"`
// The version of the base provider.
Version string `json:"version" yaml:"version"`
}
// ParameterizationSpec is the serializable description of a provider parameterization.
type ParameterizationSpec struct {
// The base provider to parameterize.
BaseProvider BaseProviderSpec `json:"baseProvider" yaml:"baseProvider"`
// The parameter to apply to the base provider.
Parameter []byte `json:"parameter" yaml:"parameter"`
}
// PackageSpec is the serializable description of a Pulumi package.
type PackageSpec struct {
// Name is the unqualified name of the package (e.g. "aws", "azure", "gcp", "kubernetes", "random")
Name string `json:"name" yaml:"name"`
// DisplayName is the human-friendly name of the package.
DisplayName string `json:"displayName,omitempty" yaml:"displayName,omitempty"`
// Version is the version of the package. The version must be valid semver.
Version string `json:"version,omitempty" yaml:"version,omitempty"`
// Description is the description of the package.
Description string `json:"description,omitempty" yaml:"description,omitempty"`
// Keywords is the list of keywords that are associated with the package, if any.
// Some reserved keywords can be specified as well that help with categorizing the
// package in the Pulumi registry. `category/<name>` and `kind/<type>` are the only
// reserved keywords at this time, where `<name>` can be one of:
// `cloud`, `database`, `infrastructure`, `monitoring`, `network`, `utility`, `vcs`
// and `<type>` is either `native` or `component`. If the package is a bridged Terraform
// provider, then don't include the `kind/` label.
Keywords []string `json:"keywords,omitempty" yaml:"keywords,omitempty"`
// Homepage is the package's homepage.
Homepage string `json:"homepage,omitempty" yaml:"homepage,omitempty"`
// License indicates which license is used for the package's contents.
License string `json:"license,omitempty" yaml:"license,omitempty"`
// Attribution allows freeform text attribution of derived work, if needed.
Attribution string `json:"attribution,omitempty" yaml:"attribution,omitempty"`
// Repository is the URL at which the source for the package can be found.
Repository string `json:"repository,omitempty" yaml:"repository,omitempty"`
// LogoURL is the URL for the package's logo, if any.
LogoURL string `json:"logoUrl,omitempty" yaml:"logoUrl,omitempty"`
// PluginDownloadURL is the URL to use to acquire the provider plugin binary, if any.
PluginDownloadURL string `json:"pluginDownloadURL,omitempty" yaml:"pluginDownloadURL,omitempty"`
// Publisher is the name of the person or organization that authored and published the package.
Publisher string `json:"publisher,omitempty" yaml:"publisher,omitempty"`
// Namespace is the namespace of the package, that's used to diambiguate the package name.
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
// Meta contains information for the importer about this package.
Meta *MetadataSpec `json:"meta,omitempty" yaml:"meta,omitempty"`
// A list of allowed package name in addition to the Name property.
AllowedPackageNames []string `json:"allowedPackageNames,omitempty" yaml:"allowedPackageNames,omitempty"`
// Language specifies additional language-specific data about the package.
Language map[string]RawMessage `json:"language,omitempty" yaml:"language,omitempty"`
// Config describes the set of configuration variables defined by this package.
Config ConfigSpec `json:"config,omitempty" yaml:"config"`
// Types is a map from type token to ComplexTypeSpec that describes the set of complex types (ie. object, enum)
// defined by this package.
Types map[string]ComplexTypeSpec `json:"types,omitempty" yaml:"types,omitempty"`
// Provider describes the provider type for this package.
Provider ResourceSpec `json:"provider,omitempty" yaml:"provider"`
// Resources is a map from type token to ResourceSpec that describes the set of resources defined by this package.
Resources map[string]ResourceSpec `json:"resources,omitempty" yaml:"resources,omitempty"`
// Functions is a map from token to FunctionSpec that describes the set of functions defined by this package.
Functions map[string]FunctionSpec `json:"functions,omitempty" yaml:"functions,omitempty"`
// Dependencies is a list of dependencies of this packaeg
Dependencies []PackageDescriptor `json:"dependencies,omitempty" yaml:"dependencies,omitempty"`
// Parameterization is the optional parameterization for this package.
Parameterization *ParameterizationSpec `json:"parameterization,omitempty" yaml:"parameterization,omitempty"`
}
func (p *PackageSpec) Info() PackageInfoSpec {
return PackageInfoSpec{
Name: p.Name,
DisplayName: p.DisplayName,
Version: p.Version,
Description: p.Description,
Keywords: p.Keywords,
Homepage: p.Homepage,
License: p.License,
Attribution: p.Attribution,
Repository: p.Repository,
LogoURL: p.LogoURL,
PluginDownloadURL: p.PluginDownloadURL,
Publisher: p.Publisher,
Namespace: p.Namespace,
Dependencies: p.Dependencies,
Meta: p.Meta,
AllowedPackageNames: p.AllowedPackageNames,
Language: p.Language,
Parameterization: p.Parameterization,
}
}
// PartialPackageSpec is a serializable description of a Pulumi package that defers the deserialization of most package
// members until they are needed. Used to support PartialPackage and PackageReferences.
type PartialPackageSpec struct {
PackageInfoSpec `yaml:",inline"`
// Config describes the set of configuration variables defined by this package.
Config json.RawMessage `json:"config" yaml:"config"`
// Types is a map from type token to ComplexTypeSpec that describes the set of complex types (ie. object, enum)
// defined by this package.
Types map[string]json.RawMessage `json:"types,omitempty" yaml:"types,omitempty"`
// Provider describes the provider type for this package.
Provider json.RawMessage `json:"provider" yaml:"provider"`
// Resources is a map from type token to ResourceSpec that describes the set of resources defined by this package.
Resources map[string]json.RawMessage `json:"resources,omitempty" yaml:"resources,omitempty"`
// Functions is a map from token to FunctionSpec that describes the set of functions defined by this package.
Functions map[string]json.RawMessage `json:"functions,omitempty" yaml:"functions,omitempty"`
}
// Copyright 2021 Google LLC
//
// 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 schema
import (
fuzz "github.com/AdaLogics/go-fuzz-headers"
)
func SchemaFuzzer(data []byte) int {
pkgSpec := PackageSpec{}
f := fuzz.NewConsumer(data)
err := f.GenerateStruct(&pkgSpec)
if err != nil {
return 0
}
_, _ = ImportSpec(pkgSpec, nil, ValidationOptions{
AllowDanglingReferences: true,
})
return 1
}
// Copyright 2020-2024, 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.
//nolint:revive // Legacy package name we don't want to change
package utils
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/blang/semver"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
)
type SchemaProvider struct {
name string
version string
}
func NewSchemaProvider(name, version string) SchemaProvider {
return SchemaProvider{name, version}
}
// NewHost creates a schema-only plugin host, supporting multiple package versions in tests. This
// enables running tests offline. If this host is used to load a plugin, that is, to run a Pulumi
// program, it will panic.
func NewHostWithProviders(schemaDirectoryPath string, providers ...SchemaProvider) plugin.Host {
mockProvider := func(name tokens.Package, version string) *deploytest.PluginLoader {
return deploytest.NewProviderLoader(name, semver.MustParse(version), func() (plugin.Provider, error) {
return &deploytest.Provider{
GetSchemaF: func(context.Context, plugin.GetSchemaRequest) (plugin.GetSchemaResponse, error) {
path := filepath.Join(schemaDirectoryPath, fmt.Sprintf("%s-%s.json", name, version))
data, err := os.ReadFile(path)
if err != nil {
return plugin.GetSchemaResponse{}, err
}
return plugin.GetSchemaResponse{
Schema: data,
}, nil
},
}, nil
})
}
pluginLoaders := slice.Prealloc[*deploytest.PluginLoader](len(providers))
for _, v := range providers {
pluginLoaders = append(pluginLoaders, mockProvider(tokens.Package(v.name), v.version))
}
// For the pulumi/pulumi repository, this must be kept in sync with the makefile and/or committed
// schema files in the given schema directory. This is the minimal set of schemas that must be
// supplied.
return deploytest.NewPluginHost(nil, nil, nil,
pluginLoaders...,
)
}
// NewHost creates a schema-only plugin host, supporting multiple package versions in tests. This
// enables running tests offline. If this host is used to load a plugin, that is, to run a Pulumi
// program, it will panic.
func NewHost(schemaDirectoryPath string) plugin.Host {
// For the pulumi/pulumi repository, this must be kept in sync with the makefile and/or committed
// schema files in the given schema directory. This is the minimal set of schemas that must be
// supplied.
return NewHostWithProviders(schemaDirectoryPath,
SchemaProvider{"tls", "4.10.0"},
SchemaProvider{"aws", "4.15.0"},
SchemaProvider{"aws", "4.26.0"},
SchemaProvider{"aws", "4.36.0"},
SchemaProvider{"aws", "4.37.1"},
SchemaProvider{"aws", "5.16.2"},
SchemaProvider{"azure", "4.18.0"},
SchemaProvider{"azure-native", "1.28.0"},
SchemaProvider{"azure-native", "1.29.0"},
SchemaProvider{"random", "4.2.0"},
SchemaProvider{"random", "4.3.1"},
SchemaProvider{"random", "4.11.2"},
SchemaProvider{"kubernetes", "3.7.0"},
SchemaProvider{"kubernetes", "3.7.2"},
SchemaProvider{"eks", "0.37.1"},
SchemaProvider{"google-native", "0.18.2"},
SchemaProvider{"google-native", "0.27.0"},
SchemaProvider{"aws-native", "0.99.0"},
SchemaProvider{"docker", "3.1.0"},
SchemaProvider{"std", "1.0.0"},
// PCL examples in 'testing/test/testdata/transpiled_examples require these versions
SchemaProvider{"aws", "5.4.0"},
SchemaProvider{"azure-native", "1.56.0"},
SchemaProvider{"eks", "0.40.0"},
SchemaProvider{"aws-native", "0.13.0"},
SchemaProvider{"docker", "4.0.0-alpha.0"},
SchemaProvider{"awsx", "1.0.0-beta.5"},
SchemaProvider{"kubernetes", "3.0.0"},
SchemaProvider{"aws", "4.37.1"},
SchemaProvider{"component", "13.3.7"},
SchemaProvider{"other", "0.1.0"},
SchemaProvider{"synthetic", "1.0.0"},
SchemaProvider{"basic-unions", "0.1.0"},
SchemaProvider{"range", "1.0.0"},
SchemaProvider{"lambda", "0.1.0"},
SchemaProvider{"remoteref", "1.0.0"},
SchemaProvider{"splat", "1.0.0"},
SchemaProvider{"snowflake", "0.66.1"},
SchemaProvider{"using-dashes", "1.0.0"},
SchemaProvider{"auto-deploy", "0.0.1"},
SchemaProvider{"localref", "1.0.0"},
SchemaProvider{"enum", "1.0.0"},
SchemaProvider{"plain-properties", "1.0.0"},
SchemaProvider{"recursive", "1.0.0"},
SchemaProvider{"aws-static-website", "0.4.0"},
SchemaProvider{"aliases", "1.0.0"},
SchemaProvider{"dangling-reference-bad", "0.1.0"},
SchemaProvider{"dangling-reference-good", "0.1.0"},
)
}
// Copyright 2016-2025, 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 deploytest
import (
"context"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)
type Analyzer struct {
Info plugin.AnalyzerInfo
AnalyzeF func(r plugin.AnalyzerResource) (plugin.AnalyzeResponse, error)
AnalyzeStackF func(resources []plugin.AnalyzerStackResource) (plugin.AnalyzeResponse, error)
RemediateF func(r plugin.AnalyzerResource) (plugin.RemediateResponse, error)
ConfigureF func(policyConfig map[string]plugin.AnalyzerPolicyConfig) error
CancelF func() error
}
var _ = plugin.Analyzer((*Analyzer)(nil))
func (a *Analyzer) Close() error {
return nil
}
func (a *Analyzer) Name() tokens.QName {
return tokens.QName(a.Info.Name)
}
func (a *Analyzer) Analyze(r plugin.AnalyzerResource) (plugin.AnalyzeResponse, error) {
if a.AnalyzeF != nil {
return a.AnalyzeF(r)
}
return plugin.AnalyzeResponse{}, nil
}
func (a *Analyzer) AnalyzeStack(resources []plugin.AnalyzerStackResource) (plugin.AnalyzeResponse, error) {
if a.AnalyzeStackF != nil {
return a.AnalyzeStackF(resources)
}
return plugin.AnalyzeResponse{}, nil
}
func (a *Analyzer) Remediate(r plugin.AnalyzerResource) (plugin.RemediateResponse, error) {
if a.RemediateF != nil {
return a.RemediateF(r)
}
return plugin.RemediateResponse{}, nil
}
func (a *Analyzer) GetAnalyzerInfo() (plugin.AnalyzerInfo, error) {
return a.Info, nil
}
func (a *Analyzer) GetPluginInfo() (workspace.PluginInfo, error) {
return workspace.PluginInfo{
Kind: apitype.AnalyzerPlugin,
Name: a.Info.Name,
}, nil
}
func (a *Analyzer) Configure(policyConfig map[string]plugin.AnalyzerPolicyConfig) error {
if a.ConfigureF != nil {
return a.ConfigureF(policyConfig)
}
return nil
}
func (a *Analyzer) Cancel(ctx context.Context) error {
if a.CancelF != nil {
return a.CancelF()
}
return nil
}
// 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 deploytest
import (
"context"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
)
// BackendClient provides a simple implementation of deploy.BackendClient that defers to a function value.
type BackendClient struct {
GetStackOutputsF func(
ctx context.Context,
name string,
onDecryptError func(error) error,
) (resource.PropertyMap, error)
GetStackResourceOutputsF func(ctx context.Context, name string) (resource.PropertyMap, error)
}
// GetStackOutputs returns the outputs (if any) for the named stack, returning an error if the stack cannot be found or
// loaded. If the stack contains secrets that cannot be decrypted, the onDecryptError callback will be called with the
// error. The callback should return a new error to be returned to the caller, or nil to ignore the error.
func (b *BackendClient) GetStackOutputs(
ctx context.Context,
name string,
onDecryptError func(error) error,
) (resource.PropertyMap, error) {
return b.GetStackOutputsF(ctx, name, onDecryptError)
}
// GetStackResourceOutputs returns the resource outputs for a stack, or an error if the stack
// cannot be found. Resources are retrieved from the latest stack snapshot, which may include
// ongoing updates. They are returned in a `PropertyMap` mapping resource URN to another
// `Propertymap` with members `type` (containing the Pulumi type ID for the resource) and
// `outputs` (containing the resource outputs themselves).
func (b *BackendClient) GetStackResourceOutputs(
ctx context.Context, name string,
) (resource.PropertyMap, error) {
return b.GetStackResourceOutputsF(ctx, name)
}
// Copyright 2016-2023, 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 deploytest
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil"
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
)
func NewCallbacksServer() (*CallbackServer, error) {
callbackServer := &CallbackServer{
callbacks: make(map[string]func(args []byte) (proto.Message, error)),
stop: make(chan bool),
}
handle, err := rpcutil.ServeWithOptions(rpcutil.ServeOptions{
Cancel: callbackServer.stop,
Init: func(srv *grpc.Server) error {
pulumirpc.RegisterCallbacksServer(srv, callbackServer)
return nil
},
Options: rpcutil.OpenTracingServerInterceptorOptions(nil),
})
if err != nil {
return nil, fmt.Errorf("could not start resource provider service: %w", err)
}
callbackServer.handle = handle
return callbackServer, nil
}
type CallbackServer struct {
pulumirpc.UnsafeCallbacksServer
stop chan bool
handle rpcutil.ServeHandle
callbacks map[string]func(req []byte) (proto.Message, error)
}
func (s *CallbackServer) Close() error {
s.stop <- true
return <-s.handle.Done
}
func (s *CallbackServer) Allocate(
callback func(args []byte) (proto.Message, error),
) (*pulumirpc.Callback, error) {
token := uuid.NewString()
s.callbacks[token] = callback
return &pulumirpc.Callback{
Target: fmt.Sprintf("127.0.0.1:%d", s.handle.Port),
Token: token,
}, nil
}
func (s *CallbackServer) Invoke(
ctx context.Context, req *pulumirpc.CallbackInvokeRequest,
) (*pulumirpc.CallbackInvokeResponse, error) {
callback, ok := s.callbacks[req.Token]
if !ok {
return nil, nil
}
response, err := callback(req.Request)
if err != nil {
return nil, err
}
responseBytes, err := proto.Marshal(response)
if err != nil {
return nil, fmt.Errorf("marshaling response: %w", err)
}
return &pulumirpc.CallbackInvokeResponse{
Response: responseBytes,
}, nil
}
// Copyright 2016-2024, 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 deploytest
import (
"context"
"errors"
"io"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/pulumi/pulumi/sdk/v3/go/common/promise"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)
var ErrLanguageRuntimeIsClosed = errors.New("language runtime is shutting down")
type LanguageRuntimeFactory func() plugin.LanguageRuntime
type ProgramFunc func(runInfo plugin.RunInfo, monitor *ResourceMonitor) error
func NewLanguageRuntimeF(program ProgramFunc, requiredPackages ...workspace.PackageDescriptor) LanguageRuntimeFactory {
return func() plugin.LanguageRuntime {
return NewLanguageRuntime(program, requiredPackages...)
}
}
func NewLanguageRuntime(program ProgramFunc, requiredPackages ...workspace.PackageDescriptor) plugin.LanguageRuntime {
return &languageRuntime{
requiredPackages: requiredPackages,
program: program,
}
}
func NewLanguageRuntimeWithShutdown(
program ProgramFunc, shutdown func(), requiredPackages ...workspace.PackageDescriptor,
) plugin.LanguageRuntime {
return &languageRuntime{
requiredPackages: requiredPackages,
program: program,
shutdown: shutdown,
}
}
type languageRuntime struct {
requiredPackages []workspace.PackageDescriptor
program ProgramFunc
closed bool
shutdown func()
}
func (p *languageRuntime) Close() error {
p.closed = true
return nil
}
func (p *languageRuntime) GetRequiredPackages(info plugin.ProgramInfo) ([]workspace.PackageDescriptor, error) {
if p.closed {
return nil, ErrLanguageRuntimeIsClosed
}
return p.requiredPackages, nil
}
func (p *languageRuntime) Run(info plugin.RunInfo) (string, bool, error) {
if p.closed {
return "", false, ErrLanguageRuntimeIsClosed
}
monitor, err := dialMonitor(context.Background(), info.MonitorAddress)
if err != nil {
return "", false, err
}
defer contract.IgnoreClose(monitor)
// Run the program.
done := make(chan error)
go func() {
err := errors.New("program did not exit successfully, either due to panic, or t.FailNow() being called")
// This is a rather strange pattern. We defer a function here that sends the error
// to the done channel and then call the program function, instead of just sending
// the error directly to the done channel. This is because the program function is
// a test function that may use testify's `require` package. That package calls
// t.FailNow() when an error occurs, which in turn causes runtime.Goexit() to be
// called. runtime.Goexit() causes the goroutine to exit immediately, so if t.FailNow()
// is called we never actually send the error to the done channel.
//
// Helpfully runtime.Goexit() does allow deferred functions in the goroutine to still
// run before the goroutine exits, so we can use this deferred function to make sure
// we can always send something to the done channel, which will prevent the test from
// just hanging. Note that in this case it doesn't really matter that we don't return
// the error message, as the `require` library will already have recorded and printed
// the error.
defer func() {
done <- err
}()
err = p.program(info, monitor)
}()
if progerr := <-done; progerr != nil {
return progerr.Error(), false, nil
}
return "", false, nil
}
func (p *languageRuntime) GetPluginInfo() (workspace.PluginInfo, error) {
if p.closed {
return workspace.PluginInfo{}, ErrLanguageRuntimeIsClosed
}
return workspace.PluginInfo{Name: "TestLanguage"}, nil
}
func (p *languageRuntime) InstallDependencies(
plugin.InstallDependenciesRequest,
) (io.Reader, io.Reader, <-chan error, error) {
if p.closed {
return nil, nil, nil, ErrLanguageRuntimeIsClosed
}
// We'll return default readers for stdout and stderr, as well as a closed channel to signal that the installation
// is complete and that anyone blocking on it can proceed immediately.
stdout := strings.NewReader("")
stderr := strings.NewReader("")
done := make(chan error)
close(done)
return stdout, stderr, done, nil
}
func (p *languageRuntime) RuntimeOptionsPrompts(info plugin.ProgramInfo) ([]plugin.RuntimeOptionPrompt, error) {
if p.closed {
return []plugin.RuntimeOptionPrompt{}, ErrLanguageRuntimeIsClosed
}
return []plugin.RuntimeOptionPrompt{}, nil
}
func (p *languageRuntime) About(info plugin.ProgramInfo) (plugin.AboutInfo, error) {
if p.closed {
return plugin.AboutInfo{}, ErrLanguageRuntimeIsClosed
}
return plugin.AboutInfo{}, nil
}
func (p *languageRuntime) GetProgramDependencies(
info plugin.ProgramInfo, transitiveDependencies bool,
) ([]plugin.DependencyInfo, error) {
if p.closed {
return nil, ErrLanguageRuntimeIsClosed
}
return nil, nil
}
func (p *languageRuntime) RunPlugin(ctx context.Context, info plugin.RunPluginInfo) (
io.Reader, io.Reader, *promise.Promise[int32], error,
) {
return nil, nil, nil, errors.New("inline plugins are not currently supported")
}
func (p *languageRuntime) GenerateProject(string, string, string,
bool, string, map[string]string,
) (hcl.Diagnostics, error) {
return nil, errors.New("GenerateProject is not supported")
}
func (p *languageRuntime) GeneratePackage(
string, string, map[string][]byte, string, map[string]string, bool,
) (hcl.Diagnostics, error) {
return nil, errors.New("GeneratePackage is not supported")
}
func (p *languageRuntime) GenerateProgram(map[string]string, string, bool) (map[string][]byte, hcl.Diagnostics, error) {
return nil, nil, errors.New("GenerateProgram is not supported")
}
func (p *languageRuntime) Pack(string, string) (string, error) {
return "", errors.New("Pack is not supported")
}
func (p *languageRuntime) Link(plugin.ProgramInfo, map[string]string) error {
return errors.New("Link is not supported")
}
func (p *languageRuntime) Cancel() error {
p.closed = true
if p.shutdown != nil {
p.shutdown()
}
return nil
}
// 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 deploytest
import (
"context"
"errors"
"fmt"
"io"
"sync"
"github.com/blang/semver"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
)
var ErrHostIsClosed = errors.New("plugin host is shutting down")
var UseGrpcPluginsByDefault = false
type (
LoadPluginFunc func(opts interface{}) (interface{}, error)
LoadPluginWithHostFunc func(opts interface{}, host plugin.Host) (interface{}, error)
)
type (
LoadProviderFunc func() (plugin.Provider, error)
LoadProviderWithHostFunc func(host plugin.Host) (plugin.Provider, error)
)
type (
LoadAnalyzerFunc func(opts *plugin.PolicyAnalyzerOptions) (plugin.Analyzer, error)
LoadAnalyzerWithHostFunc func(opts *plugin.PolicyAnalyzerOptions, host plugin.Host) (plugin.Analyzer, error)
)
type PluginOption func(p *PluginLoader)
func WithoutGrpc(p *PluginLoader) {
p.useGRPC = false
}
func WithGrpc(p *PluginLoader) {
p.useGRPC = true
}
type PluginLoader struct {
kind apitype.PluginKind
name string
version semver.Version
load LoadPluginFunc
loadWithHost LoadPluginWithHostFunc
useGRPC bool
}
type (
ProviderOption = PluginOption
ProviderLoader = PluginLoader
)
func NewProviderLoader(pkg tokens.Package, version semver.Version, load LoadProviderFunc,
opts ...ProviderOption,
) *ProviderLoader {
p := &ProviderLoader{
kind: apitype.ResourcePlugin,
name: string(pkg),
version: version,
load: func(_ interface{}) (interface{}, error) { return load() },
useGRPC: UseGrpcPluginsByDefault,
}
for _, o := range opts {
o(p)
}
return p
}
func NewProviderLoaderWithHost(pkg tokens.Package, version semver.Version,
load LoadProviderWithHostFunc, opts ...ProviderOption,
) *ProviderLoader {
p := &ProviderLoader{
kind: apitype.ResourcePlugin,
name: string(pkg),
version: version,
loadWithHost: func(_ interface{}, host plugin.Host) (interface{}, error) { return load(host) },
useGRPC: UseGrpcPluginsByDefault,
}
for _, o := range opts {
o(p)
}
return p
}
func NewAnalyzerLoader(name string, load LoadAnalyzerFunc, opts ...PluginOption) *PluginLoader {
p := &PluginLoader{
kind: apitype.AnalyzerPlugin,
name: name,
load: func(optsI interface{}) (interface{}, error) {
opts, _ := optsI.(*plugin.PolicyAnalyzerOptions)
return load(opts)
},
}
for _, o := range opts {
o(p)
}
return p
}
func NewAnalyzerLoaderWithHost(name string, load LoadAnalyzerWithHostFunc, opts ...PluginOption) *PluginLoader {
p := &PluginLoader{
kind: apitype.AnalyzerPlugin,
name: name,
loadWithHost: func(optsI interface{}, host plugin.Host) (interface{}, error) {
opts, _ := optsI.(*plugin.PolicyAnalyzerOptions)
return load(opts, host)
},
}
for _, o := range opts {
o(p)
}
return p
}
type nopCloserT int
func (nopCloserT) Close() error { return nil }
var nopCloser io.Closer = nopCloserT(0)
type grpcWrapper struct {
stop chan bool
}
func (w *grpcWrapper) Close() error {
go func() { w.stop <- true }()
return nil
}
func wrapProviderWithGrpc(provider plugin.Provider) (plugin.Provider, io.Closer, error) {
wrapper := &grpcWrapper{stop: make(chan bool)}
handle, err := rpcutil.ServeWithOptions(rpcutil.ServeOptions{
Cancel: wrapper.stop,
Init: func(srv *grpc.Server) error {
pulumirpc.RegisterResourceProviderServer(srv, plugin.NewProviderServer(provider))
return nil
},
Options: rpcutil.OpenTracingServerInterceptorOptions(nil),
})
if err != nil {
return nil, nil, fmt.Errorf("could not start resource provider service: %w", err)
}
conn, err := grpc.NewClient(
fmt.Sprintf("127.0.0.1:%v", handle.Port),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUnaryInterceptor(rpcutil.OpenTracingClientInterceptor()),
grpc.WithStreamInterceptor(rpcutil.OpenTracingStreamClientInterceptor()),
rpcutil.GrpcChannelOptions(),
)
if err != nil {
contract.IgnoreClose(wrapper)
return nil, nil, fmt.Errorf("could not connect to resource provider service: %w", err)
}
wrapped := plugin.NewProviderWithClient(nil, provider.Pkg(), pulumirpc.NewResourceProviderClient(conn), false)
return wrapped, wrapper, nil
}
type hostEngine struct {
pulumirpc.UnsafeEngineServer // opt out of forward compat
sink diag.Sink
statusSink diag.Sink
address string
stop chan bool
}
func (e *hostEngine) Log(_ context.Context, req *pulumirpc.LogRequest) (*emptypb.Empty, error) {
var sev diag.Severity
switch req.Severity {
case pulumirpc.LogSeverity_DEBUG:
sev = diag.Debug
case pulumirpc.LogSeverity_INFO:
sev = diag.Info
case pulumirpc.LogSeverity_WARNING:
sev = diag.Warning
case pulumirpc.LogSeverity_ERROR:
sev = diag.Error
default:
return nil, fmt.Errorf("Unrecognized logging severity: %v", req.Severity)
}
if req.Ephemeral {
e.statusSink.Logf(sev, diag.StreamMessage(resource.URN(req.Urn), req.Message, req.StreamId))
} else {
e.sink.Logf(sev, diag.StreamMessage(resource.URN(req.Urn), req.Message, req.StreamId))
}
return &emptypb.Empty{}, nil
}
func (e *hostEngine) GetRootResource(_ context.Context,
req *pulumirpc.GetRootResourceRequest,
) (*pulumirpc.GetRootResourceResponse, error) {
return nil, errors.New("unsupported")
}
func (e *hostEngine) SetRootResource(_ context.Context,
req *pulumirpc.SetRootResourceRequest,
) (*pulumirpc.SetRootResourceResponse, error) {
return nil, errors.New("unsupported")
}
func (e *hostEngine) StartDebugging(ctx context.Context,
req *pulumirpc.StartDebuggingRequest,
) (*emptypb.Empty, error) {
return nil, errors.New("unsupported")
}
type PluginHostFactory func() plugin.Host
type pluginHost struct {
pluginLoaders []*ProviderLoader
languageRuntime plugin.LanguageRuntime
sink diag.Sink
statusSink diag.Sink
engine *hostEngine
providers []plugin.Provider
analyzers []plugin.Analyzer
plugins map[interface{}]io.Closer
closed bool
m sync.Mutex
}
// NewPluginHostF returns a factory that produces a plugin host for an operation.
func NewPluginHostF(sink, statusSink diag.Sink, languageRuntimeF LanguageRuntimeFactory,
pluginLoaders ...*ProviderLoader,
) PluginHostFactory {
return func() plugin.Host {
var lr plugin.LanguageRuntime
if languageRuntimeF != nil {
lr = languageRuntimeF()
}
return NewPluginHost(sink, statusSink, lr, pluginLoaders...)
}
}
func NewPluginHost(sink, statusSink diag.Sink, languageRuntime plugin.LanguageRuntime,
pluginLoaders ...*ProviderLoader,
) plugin.Host {
engine := &hostEngine{
sink: sink,
statusSink: statusSink,
stop: make(chan bool),
}
handle, err := rpcutil.ServeWithOptions(rpcutil.ServeOptions{
Cancel: engine.stop,
Init: func(srv *grpc.Server) error {
pulumirpc.RegisterEngineServer(srv, engine)
return nil
},
Options: rpcutil.OpenTracingServerInterceptorOptions(nil),
})
if err != nil {
panic(fmt.Errorf("could not start engine service: %w", err))
}
engine.address = fmt.Sprintf("127.0.0.1:%v", handle.Port)
return &pluginHost{
pluginLoaders: pluginLoaders,
languageRuntime: languageRuntime,
sink: sink,
statusSink: statusSink,
engine: engine,
plugins: map[interface{}]io.Closer{},
}
}
func (host *pluginHost) isClosed() bool {
host.m.Lock()
defer host.m.Unlock()
return host.closed
}
func (host *pluginHost) plugin(kind apitype.PluginKind, name string, version *semver.Version,
opts interface{},
) (interface{}, error) {
var best *PluginLoader
for _, l := range host.pluginLoaders {
if l.kind != kind || l.name != name {
continue
}
if version != nil {
if l.version.EQ(*version) {
best = l
break
}
} else if best == nil || l.version.GT(best.version) {
best = l
}
}
if best == nil {
return nil, nil
}
load := best.load
if load == nil {
load = func(opts interface{}) (interface{}, error) {
return best.loadWithHost(opts, host)
}
}
plug, err := load(opts)
if err != nil {
return nil, err
}
closer := nopCloser
if best.useGRPC {
plug, closer, err = wrapProviderWithGrpc(plug.(plugin.Provider))
if err != nil {
return nil, err
}
}
host.m.Lock()
defer host.m.Unlock()
switch kind {
case apitype.AnalyzerPlugin:
host.analyzers = append(host.analyzers, plug.(plugin.Analyzer))
case apitype.ResourcePlugin:
host.providers = append(host.providers, plug.(plugin.Provider))
case apitype.LanguagePlugin, apitype.ConverterPlugin, apitype.ToolPlugin:
// Nothing to do for these to plugins.
}
host.plugins[plug] = closer
return plug, nil
}
func (host *pluginHost) Provider(descriptor workspace.PackageDescriptor) (plugin.Provider, error) {
if host.isClosed() {
return nil, ErrHostIsClosed
}
plug, err := host.plugin(apitype.ResourcePlugin, descriptor.Name, descriptor.Version, nil)
if err != nil {
return nil, err
}
if plug == nil {
v := "nil"
if descriptor.Version != nil {
v = descriptor.Version.String()
}
return nil, fmt.Errorf("Could not find plugin for (%s, %s)", descriptor.Name, v)
}
return plug.(plugin.Provider), nil
}
func (host *pluginHost) LanguageRuntime(root string, info plugin.ProgramInfo) (plugin.LanguageRuntime, error) {
if host.isClosed() {
return nil, ErrHostIsClosed
}
return host.languageRuntime, nil
}
func (host *pluginHost) SignalCancellation() error {
if host.isClosed() {
return ErrHostIsClosed
}
host.m.Lock()
defer host.m.Unlock()
var err error
for _, prov := range host.providers {
if pErr := prov.SignalCancellation(context.TODO()); pErr != nil {
err = pErr
}
}
for _, analyzer := range host.analyzers {
if aErr := analyzer.Cancel(context.TODO()); aErr != nil {
err = aErr
}
}
if host.languageRuntime != nil {
if lErr := host.languageRuntime.Cancel(); lErr != nil {
err = lErr
}
}
return err
}
func (host *pluginHost) Close() error {
if host.isClosed() {
return nil // Close is idempotent
}
host.m.Lock()
defer host.m.Unlock()
var err error
for _, closer := range host.plugins {
if pErr := closer.Close(); pErr != nil {
err = pErr
}
}
go func() { host.engine.stop <- true }()
host.closed = true
return err
}
func (host *pluginHost) ServerAddr() string {
return host.engine.address
}
func (host *pluginHost) Log(sev diag.Severity, urn resource.URN, msg string, streamID int32) {
if !host.isClosed() {
host.sink.Logf(sev, diag.StreamMessage(urn, msg, streamID))
}
}
func (host *pluginHost) LogStatus(sev diag.Severity, urn resource.URN, msg string, streamID int32) {
if !host.isClosed() {
host.statusSink.Logf(sev, diag.StreamMessage(urn, msg, streamID))
}
}
func (host *pluginHost) StartDebugging(info plugin.DebuggingInfo) error {
return nil
}
func (host *pluginHost) AttachDebugger(_ plugin.DebugSpec) bool {
return false
}
func (host *pluginHost) Analyzer(nm tokens.QName) (plugin.Analyzer, error) {
return host.PolicyAnalyzer(nm, "", nil)
}
func (host *pluginHost) CloseProvider(provider plugin.Provider) error {
if host.isClosed() {
return ErrHostIsClosed
}
host.m.Lock()
defer host.m.Unlock()
delete(host.plugins, provider)
return nil
}
func (host *pluginHost) EnsurePlugins(plugins []workspace.PluginSpec, kinds plugin.Flags) error {
if host.isClosed() {
return ErrHostIsClosed
}
return nil
}
func (host *pluginHost) ResolvePlugin(
spec workspace.PluginSpec,
) (*workspace.PluginInfo, error) {
plugins := slice.Prealloc[workspace.PluginInfo](len(host.pluginLoaders))
for _, v := range host.pluginLoaders {
if spec.Version == nil && v.name == spec.Name && v.kind == spec.Kind {
spec.Version = &v.version
}
p := workspace.PluginInfo{
Kind: v.kind,
Name: v.name,
Version: &v.version,
// Path and SchemaPath not set as these plugins aren't actually on disk.
// SchemaTime not set as caching is indefinite.
}
plugins = append(plugins, p)
}
var match *workspace.PluginInfo
if spec.Version != nil {
match = workspace.SelectCompatiblePlugin(plugins, spec)
}
if match == nil {
return nil, errors.New("could not locate a compatible plugin in deploytest, the makefile and " +
"& constructor of the plugin host must define the location of the schema")
}
return match, nil
}
func (host *pluginHost) GetRequiredPackages(
info plugin.ProgramInfo,
kinds plugin.Flags,
) ([]workspace.PackageDescriptor, error) {
return host.languageRuntime.GetRequiredPackages(info)
}
func (host *pluginHost) GetProjectPlugins() []workspace.ProjectPlugin {
return nil
}
func (host *pluginHost) PolicyAnalyzer(name tokens.QName, path string,
opts *plugin.PolicyAnalyzerOptions,
) (plugin.Analyzer, error) {
if host.isClosed() {
return nil, ErrHostIsClosed
}
plug, err := host.plugin(apitype.AnalyzerPlugin, string(name), nil, opts)
if err != nil || plug == nil {
return nil, err
}
return plug.(plugin.Analyzer), nil
}
func (host *pluginHost) ListAnalyzers() []plugin.Analyzer {
host.m.Lock()
defer host.m.Unlock()
return host.analyzers
}
// 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 deploytest
import (
"context"
"errors"
"github.com/blang/semver"
uuid "github.com/gofrs/uuid"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)
type Provider struct {
plugin.NotForwardCompatibleProvider
Name string
Package tokens.Package
Version semver.Version
Config resource.PropertyMap
configured bool
DialMonitorF func(ctx context.Context, endpoint string) (*ResourceMonitor, error)
CancelF func() error
HandshakeF func(context.Context, plugin.ProviderHandshakeRequest) (*plugin.ProviderHandshakeResponse, error)
ParameterizeF func(context.Context, plugin.ParameterizeRequest) (plugin.ParameterizeResponse, error)
GetSchemaF func(context.Context, plugin.GetSchemaRequest) (plugin.GetSchemaResponse, error)
CheckConfigF func(context.Context, plugin.CheckConfigRequest) (plugin.CheckConfigResponse, error)
DiffConfigF func(context.Context, plugin.DiffConfigRequest) (plugin.DiffConfigResponse, error)
ConfigureF func(context.Context, plugin.ConfigureRequest) (plugin.ConfigureResponse, error)
CheckF func(context.Context, plugin.CheckRequest) (plugin.CheckResponse, error)
DiffF func(context.Context, plugin.DiffRequest) (plugin.DiffResult, error)
CreateF func(context.Context, plugin.CreateRequest) (plugin.CreateResponse, error)
UpdateF func(context.Context, plugin.UpdateRequest) (plugin.UpdateResponse, error)
DeleteF func(context.Context, plugin.DeleteRequest) (plugin.DeleteResponse, error)
ReadF func(context.Context, plugin.ReadRequest) (plugin.ReadResponse, error)
ConstructF func(context.Context, plugin.ConstructRequest, *ResourceMonitor) (plugin.ConstructResponse, error)
InvokeF func(context.Context, plugin.InvokeRequest) (plugin.InvokeResponse, error)
CallF func(context.Context, plugin.CallRequest, *ResourceMonitor) (plugin.CallResponse, error)
GetMappingF func(context.Context, plugin.GetMappingRequest) (plugin.GetMappingResponse, error)
GetMappingsF func(context.Context, plugin.GetMappingsRequest) (plugin.GetMappingsResponse, error)
}
func (prov *Provider) Handshake(
ctx context.Context, req plugin.ProviderHandshakeRequest,
) (*plugin.ProviderHandshakeResponse, error) {
if prov.HandshakeF == nil {
return &plugin.ProviderHandshakeResponse{}, nil
}
return prov.HandshakeF(ctx, req)
}
func (prov *Provider) SignalCancellation(context.Context) error {
if prov.CancelF == nil {
return nil
}
return prov.CancelF()
}
func (prov *Provider) Close() error {
return nil
}
func (prov *Provider) Pkg() tokens.Package {
return prov.Package
}
func (prov *Provider) GetPluginInfo(context.Context) (workspace.PluginInfo, error) {
return workspace.PluginInfo{
Name: prov.Name,
Version: &prov.Version,
}, nil
}
func (prov *Provider) Parameterize(
ctx context.Context, params plugin.ParameterizeRequest,
) (plugin.ParameterizeResponse, error) {
if prov.ParameterizeF == nil {
return plugin.ParameterizeResponse{}, errors.New("no parameters")
}
return prov.ParameterizeF(ctx, params)
}
func (prov *Provider) GetSchema(
ctx context.Context,
request plugin.GetSchemaRequest,
) (plugin.GetSchemaResponse, error) {
if prov.GetSchemaF == nil {
return plugin.GetSchemaResponse{Schema: []byte("{}")}, nil
}
return prov.GetSchemaF(ctx, request)
}
func (prov *Provider) CheckConfig(
ctx context.Context, req plugin.CheckConfigRequest,
) (plugin.CheckConfigResponse, error) {
if prov.CheckConfigF == nil {
return plugin.CheckConfigResponse{Properties: req.News}, nil
}
return prov.CheckConfigF(ctx, req)
}
func (prov *Provider) DiffConfig(ctx context.Context, req plugin.DiffConfigRequest) (plugin.DiffConfigResponse, error) {
if prov.DiffConfigF == nil {
return plugin.DiffResult{}, nil
}
return prov.DiffConfigF(ctx, req)
}
func (prov *Provider) Configure(ctx context.Context, req plugin.ConfigureRequest) (plugin.ConfigureResponse, error) {
contract.Assertf(!prov.configured, "provider %v was already configured", prov.Name)
prov.configured = true
if prov.ConfigureF == nil {
prov.Config = req.Inputs
return plugin.ConfigureResponse{}, nil
}
return prov.ConfigureF(ctx, req)
}
func (prov *Provider) Check(ctx context.Context, req plugin.CheckRequest) (plugin.CheckResponse, error) {
contract.Requiref(req.RandomSeed != nil, "randomSeed", "must not be nil")
if prov.CheckF == nil {
return plugin.CheckResponse{Properties: req.News}, nil
}
return prov.CheckF(ctx, req)
}
func (prov *Provider) Create(ctx context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
if prov.CreateF == nil {
// generate a new uuid
uuid, err := uuid.NewV4()
if err != nil {
return plugin.CreateResponse{}, err
}
return plugin.CreateResponse{
ID: resource.ID(uuid.String()),
Properties: resource.PropertyMap{},
}, nil
}
return prov.CreateF(ctx, req)
}
func (prov *Provider) Diff(ctx context.Context, req plugin.DiffRequest) (plugin.DiffResponse, error) {
if prov.DiffF == nil {
return plugin.DiffResponse{}, nil
}
return prov.DiffF(ctx, req)
}
func (prov *Provider) Update(ctx context.Context, req plugin.UpdateRequest) (plugin.UpdateResponse, error) {
if prov.UpdateF == nil {
return plugin.UpdateResponse{Properties: req.NewInputs, Status: resource.StatusOK}, nil
}
return prov.UpdateF(ctx, req)
}
func (prov *Provider) Delete(ctx context.Context, req plugin.DeleteRequest) (plugin.DeleteResponse, error) {
if prov.DeleteF == nil {
return plugin.DeleteResponse{Status: resource.StatusOK}, nil
}
return prov.DeleteF(ctx, req)
}
func (prov *Provider) Read(ctx context.Context, req plugin.ReadRequest) (plugin.ReadResponse, error) {
contract.Assertf(req.URN != "", "Read URN was empty")
contract.Assertf(req.ID != "", "Read ID was empty")
if prov.ReadF == nil {
state := req.State
if state == nil {
state = resource.PropertyMap{}
}
inputs := req.Inputs
if inputs == nil {
inputs = resource.PropertyMap{}
}
return plugin.ReadResponse{
ReadResult: plugin.ReadResult{
ID: req.ID,
Outputs: state,
Inputs: inputs,
},
Status: resource.StatusOK,
}, nil
}
return prov.ReadF(ctx, req)
}
func (prov *Provider) Construct(ctx context.Context, req plugin.ConstructRequest) (plugin.ConstructResult, error) {
if prov.ConstructF == nil {
return plugin.ConstructResult{}, nil
}
dialMonitorImpl := dialMonitor
if prov.DialMonitorF != nil {
dialMonitorImpl = prov.DialMonitorF
}
monitor, err := dialMonitorImpl(ctx, req.Info.MonitorAddress)
if err != nil {
return plugin.ConstructResult{}, err
}
return prov.ConstructF(ctx, req, monitor)
}
func (prov *Provider) Invoke(ctx context.Context, req plugin.InvokeRequest) (plugin.InvokeResponse, error) {
if prov.InvokeF == nil {
return plugin.InvokeResponse{
Properties: resource.PropertyMap{},
}, nil
}
return prov.InvokeF(ctx, req)
}
func (prov *Provider) Call(ctx context.Context, req plugin.CallRequest) (plugin.CallResponse, error) {
if prov.CallF == nil {
return plugin.CallResult{}, nil
}
dialMonitorImpl := dialMonitor
if prov.DialMonitorF != nil {
dialMonitorImpl = prov.DialMonitorF
}
monitor, err := dialMonitorImpl(ctx, req.Info.MonitorAddress)
if err != nil {
return plugin.CallResult{}, err
}
return prov.CallF(ctx, req, monitor)
}
func (prov *Provider) GetMapping(
ctx context.Context,
req plugin.GetMappingRequest,
) (plugin.GetMappingResponse, error) {
if prov.GetMappingF == nil {
return plugin.GetMappingResponse{}, nil
}
return prov.GetMappingF(ctx, req)
}
func (prov *Provider) GetMappings(
ctx context.Context,
req plugin.GetMappingsRequest,
) (plugin.GetMappingsResponse, error) {
if prov.GetMappingsF == nil {
return plugin.GetMappingsResponse{}, nil
}
return prov.GetMappingsF(ctx, req)
}
// 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 deploytest
import (
"context"
"fmt"
"net/url"
"strconv"
"strings"
"time"
fxs "github.com/pgavlin/fx/v2/slices"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil"
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/structpb"
)
type ResourceMonitor struct {
conn *grpc.ClientConn
resmon pulumirpc.ResourceMonitorClient
supportsSecrets bool
supportsResourceReferences bool
}
func dialMonitor(ctx context.Context, endpoint string) (*ResourceMonitor, error) {
// Connect to the resource monitor and create an appropriate client.
conn, err := grpc.NewClient(
endpoint,
grpc.WithTransportCredentials(insecure.NewCredentials()),
rpcutil.GrpcChannelOptions(),
)
if err != nil {
return nil, fmt.Errorf("could not connect to resource monitor: %w", err)
}
resmon := pulumirpc.NewResourceMonitorClient(conn)
// Check feature support.
supportsSecrets, err := supportsFeature(ctx, resmon, "secrets")
if err != nil {
contract.IgnoreError(conn.Close())
return nil, fmt.Errorf("could not determine whether secrets are supported: %w", err)
}
supportsResourceReferences, err := supportsFeature(ctx, resmon, "resourceReferences")
if err != nil {
contract.IgnoreError(conn.Close())
return nil, fmt.Errorf("could not determine whether resource references are supported: %w", err)
}
// Fire up a resource monitor client and return.
return &ResourceMonitor{
conn: conn,
resmon: resmon,
supportsSecrets: supportsSecrets,
supportsResourceReferences: supportsResourceReferences,
}, nil
}
func supportsFeature(ctx context.Context, resmon pulumirpc.ResourceMonitorClient, id string) (bool, error) {
resp, err := resmon.SupportsFeature(ctx, &pulumirpc.SupportsFeatureRequest{Id: id})
if err != nil {
return false, err
}
return resp.GetHasSupport(), nil
}
func parseSourcePosition(raw string) (*pulumirpc.SourcePosition, error) {
u, err := url.Parse(raw)
if err != nil {
return nil, err
}
pos := pulumirpc.SourcePosition{
Uri: fmt.Sprintf("%v://%v", u.Scheme, u.Path),
}
line, col, _ := strings.Cut(u.Fragment, ",")
if line != "" {
l, err := strconv.ParseInt(line, 10, 32)
if err != nil {
return nil, err
}
//nolint:gosec // ParseInt will return an error if the size is too large.
pos.Line = int32(l)
}
if col != "" {
c, err := strconv.ParseInt(col, 10, 32)
if err != nil {
return nil, err
}
//nolint:gosec // ParseInt will return an error if the size is too large.
pos.Column = int32(c)
}
return &pos, nil
}
func marshalSourceInfo(
sourcePosition string,
stackTrace []resource.StackFrame,
) (_ *pulumirpc.SourcePosition, _ *pulumirpc.StackTrace, err error) {
var pos *pulumirpc.SourcePosition
if sourcePosition != "" {
pos, err = parseSourcePosition(sourcePosition)
if err != nil {
return nil, nil, err
}
}
var trace *pulumirpc.StackTrace
if len(stackTrace) != 0 {
frames, err := fxs.TryCollect(fxs.MapUnpack(stackTrace, func(f resource.StackFrame) (*pulumirpc.StackFrame, error) {
position, err := parseSourcePosition(f.SourcePosition)
if err != nil {
return nil, err
}
return &pulumirpc.StackFrame{Pc: position}, nil
}))
if err != nil {
return nil, nil, err
}
trace = &pulumirpc.StackTrace{Frames: frames}
}
return pos, trace, nil
}
func (rm *ResourceMonitor) Close() error {
return rm.conn.Close()
}
func NewResourceMonitor(resmon pulumirpc.ResourceMonitorClient) *ResourceMonitor {
return &ResourceMonitor{resmon: resmon}
}
type ResourceHook struct {
Name string
callback *pulumirpc.Callback
}
type ResourceHookBindings struct {
BeforeCreate []*ResourceHook
AfterCreate []*ResourceHook
BeforeUpdate []*ResourceHook
AfterUpdate []*ResourceHook
BeforeDelete []*ResourceHook
AfterDelete []*ResourceHook
}
type ResourceHookFunc func(ctx context.Context, urn resource.URN, id resource.ID, name string, typ tokens.Type,
newInputs, oldInpts, newOutputs, oldOutputs resource.PropertyMap) error
func (binding ResourceHookBindings) marshal() *pulumirpc.RegisterResourceRequest_ResourceHooksBinding {
m := &pulumirpc.RegisterResourceRequest_ResourceHooksBinding{}
for _, hook := range binding.BeforeCreate {
m.BeforeCreate = append(m.BeforeCreate, hook.Name)
}
for _, hook := range binding.AfterCreate {
m.AfterCreate = append(m.AfterCreate, hook.Name)
}
for _, hook := range binding.BeforeUpdate {
m.BeforeUpdate = append(m.BeforeUpdate, hook.Name)
}
for _, hook := range binding.AfterUpdate {
m.AfterUpdate = append(m.AfterUpdate, hook.Name)
}
for _, hook := range binding.BeforeDelete {
m.BeforeDelete = append(m.BeforeDelete, hook.Name)
}
for _, hook := range binding.AfterDelete {
m.AfterDelete = append(m.AfterDelete, hook.Name)
}
return m
}
func NewHook(monitor *ResourceMonitor, callbacks *CallbackServer, name string, f ResourceHookFunc, onDryRun bool,
) (*ResourceHook, error) {
req, err := prepareHook(callbacks, name, f, onDryRun)
if err != nil {
return nil, err
}
err = monitor.RegisterResourceHook(context.Background(), req)
if err != nil {
return nil, err
}
return &ResourceHook{
Name: name,
callback: req.Callback,
}, nil
}
func prepareHook(callbacks *CallbackServer, name string, f ResourceHookFunc, onDryRun bool) (
*pulumirpc.RegisterResourceHookRequest, error,
) {
wrapped := func(request []byte) (proto.Message, error) {
var req pulumirpc.ResourceHookRequest
err := proto.Unmarshal(request, &req)
if err != nil {
return nil, fmt.Errorf("unmarshaling request: %w", err)
}
var newInputs, oldInputs, newOutputs, oldOutputs resource.PropertyMap
mOpts := plugin.MarshalOptions{
KeepUnknowns: true,
KeepSecrets: true,
KeepResources: true,
KeepOutputValues: true,
}
if req.NewInputs != nil {
newInputs, err = plugin.UnmarshalProperties(req.NewInputs, mOpts)
if err != nil {
return nil, fmt.Errorf("unmarshaling new inputs: %w", err)
}
}
if req.OldInputs != nil {
oldInputs, err = plugin.UnmarshalProperties(req.OldInputs, mOpts)
if err != nil {
return nil, fmt.Errorf("unmarshaling old inputs: %w", err)
}
}
if req.NewOutputs != nil {
newOutputs, err = plugin.UnmarshalProperties(req.NewOutputs, mOpts)
if err != nil {
return nil, fmt.Errorf("unmarshaling new outputs: %w", err)
}
}
if req.OldOutputs != nil {
oldOutputs, err = plugin.UnmarshalProperties(req.OldOutputs, mOpts)
if err != nil {
return nil, fmt.Errorf("unmarshaling old outputs: %w", err)
}
}
if err := f(context.Background(), resource.URN(req.Urn), resource.ID(req.Id), req.Name, tokens.Type(req.Type),
newInputs, oldInputs, newOutputs, oldOutputs); err != nil {
return &pulumirpc.ResourceHookResponse{
Error: err.Error(),
}, nil
}
return &pulumirpc.ResourceHookResponse{}, nil
}
callback, err := callbacks.Allocate(wrapped)
if err != nil {
return nil, err
}
req := &pulumirpc.RegisterResourceHookRequest{
Name: name,
Callback: callback,
OnDryRun: onDryRun,
}
return req, nil
}
type ResourceOptions struct {
Parent resource.URN
Protect *bool
Dependencies []resource.URN
Provider string
Inputs resource.PropertyMap
PropertyDeps map[resource.PropertyKey][]resource.URN
DeleteBeforeReplace *bool
Version string
PluginDownloadURL string
PluginChecksums map[string][]byte
IgnoreChanges []string
ReplaceOnChanges []string
AliasURNs []resource.URN
Aliases []*pulumirpc.Alias
ImportID resource.ID
CustomTimeouts *resource.CustomTimeouts
RetainOnDelete *bool
DeletedWith resource.URN
SupportsPartialValues *bool
Remote bool
Providers map[string]string
AdditionalSecretOutputs []resource.PropertyKey
AliasSpecs bool
SourcePosition string
StackTrace []resource.StackFrame
ParentStackTraceHandle string
DisableSecrets bool
DisableResourceReferences bool
GrpcRequestHeaders map[string]string
Transforms []*pulumirpc.Callback
ResourceHookBindings ResourceHookBindings
SupportsResultReporting bool
PackageRef string
}
func (rm *ResourceMonitor) unmarshalProperties(props *structpb.Struct) (resource.PropertyMap, error) {
// Note that `Keep*` flags are set to `true` so the caller can detect secrets, resource refs, etc that are
// erroneously returned (e.g. secrets/resource refs that are returned even though the caller has not set
// the relevant `Accept*` to `true` above).
return plugin.UnmarshalProperties(props, plugin.MarshalOptions{
KeepUnknowns: true,
KeepSecrets: true,
KeepResources: true,
KeepOutputValues: true,
})
}
type RegisterResourceResponse struct {
URN resource.URN
ID resource.ID
Outputs resource.PropertyMap
Dependencies map[resource.PropertyKey][]resource.URN
Result pulumirpc.Result
}
func (rm *ResourceMonitor) RegisterResource(t tokens.Type, name string, custom bool,
options ...ResourceOptions,
) (*RegisterResourceResponse, error) {
var opts ResourceOptions
if len(options) > 0 {
opts = options[0]
}
if opts.Inputs == nil {
opts.Inputs = resource.PropertyMap{}
}
// marshal inputs
ins, err := plugin.MarshalProperties(opts.Inputs, plugin.MarshalOptions{
KeepUnknowns: true,
KeepSecrets: rm.supportsSecrets,
KeepResources: rm.supportsResourceReferences,
KeepOutputValues: opts.Remote,
})
if err != nil {
return nil, err
}
// marshal dependencies
deps := []string{}
for _, d := range opts.Dependencies {
deps = append(deps, string(d))
}
// marshal aliases
aliasStrings := []string{}
for _, a := range opts.AliasURNs {
aliasStrings = append(aliasStrings, string(a))
}
inputDeps := make(map[string]*pulumirpc.RegisterResourceRequest_PropertyDependencies)
for pk, pd := range opts.PropertyDeps {
pdeps := []string{}
for _, d := range pd {
pdeps = append(pdeps, string(d))
}
inputDeps[string(pk)] = &pulumirpc.RegisterResourceRequest_PropertyDependencies{
Urns: pdeps,
}
}
var timeouts *pulumirpc.RegisterResourceRequest_CustomTimeouts
if opts.CustomTimeouts != nil {
timeouts = &pulumirpc.RegisterResourceRequest_CustomTimeouts{
Create: prepareTestTimeout(opts.CustomTimeouts.Create),
Update: prepareTestTimeout(opts.CustomTimeouts.Update),
Delete: prepareTestTimeout(opts.CustomTimeouts.Delete),
}
}
deleteBeforeReplace := false
if opts.DeleteBeforeReplace != nil {
deleteBeforeReplace = *opts.DeleteBeforeReplace
}
supportsPartialValues := true
if opts.SupportsPartialValues != nil {
supportsPartialValues = *opts.SupportsPartialValues
}
additionalSecretOutputs := make([]string, len(opts.AdditionalSecretOutputs))
for i, v := range opts.AdditionalSecretOutputs {
additionalSecretOutputs[i] = string(v)
}
sourcePosition, stackTrace, err := marshalSourceInfo(opts.SourcePosition, opts.StackTrace)
if err != nil {
return nil, err
}
resourceHooks := opts.ResourceHookBindings.marshal()
requestInput := &pulumirpc.RegisterResourceRequest{
Type: string(t),
Name: name,
Custom: custom,
Parent: string(opts.Parent),
Protect: opts.Protect,
Dependencies: deps,
Provider: opts.Provider,
Object: ins,
PropertyDependencies: inputDeps,
DeleteBeforeReplace: deleteBeforeReplace,
DeleteBeforeReplaceDefined: opts.DeleteBeforeReplace != nil,
IgnoreChanges: opts.IgnoreChanges,
AcceptSecrets: !opts.DisableSecrets,
AcceptResources: !opts.DisableResourceReferences,
Version: opts.Version,
AliasURNs: aliasStrings,
ImportId: string(opts.ImportID),
CustomTimeouts: timeouts,
SupportsPartialValues: supportsPartialValues,
Remote: opts.Remote,
ReplaceOnChanges: opts.ReplaceOnChanges,
Providers: opts.Providers,
PluginDownloadURL: opts.PluginDownloadURL,
PluginChecksums: opts.PluginChecksums,
RetainOnDelete: opts.RetainOnDelete,
AdditionalSecretOutputs: additionalSecretOutputs,
Aliases: opts.Aliases,
DeletedWith: string(opts.DeletedWith),
AliasSpecs: opts.AliasSpecs,
SourcePosition: sourcePosition,
StackTrace: stackTrace,
ParentStackTraceHandle: opts.ParentStackTraceHandle,
Transforms: opts.Transforms,
SupportsResultReporting: opts.SupportsResultReporting,
PackageRef: opts.PackageRef,
Hooks: resourceHooks,
}
ctx := context.Background()
if len(opts.GrpcRequestHeaders) > 0 {
ctx = metadata.NewOutgoingContext(ctx, metadata.New(opts.GrpcRequestHeaders))
}
// submit request
resp, err := rm.resmon.RegisterResource(ctx, requestInput)
if err != nil {
return nil, err
}
// unmarshal outputs
outs, err := rm.unmarshalProperties(resp.Object)
if err != nil {
return nil, err
}
// unmarshal dependencies
depsMap := make(map[resource.PropertyKey][]resource.URN)
for k, p := range resp.PropertyDependencies {
var urns []resource.URN
for _, urn := range p.Urns {
urns = append(urns, resource.URN(urn))
}
depsMap[resource.PropertyKey(k)] = urns
}
return &RegisterResourceResponse{
URN: resource.URN(resp.Urn),
ID: resource.ID(resp.Id),
Outputs: outs,
Dependencies: depsMap,
Result: resp.Result,
}, nil
}
func (rm *ResourceMonitor) RegisterResourceOutputs(urn resource.URN, outputs resource.PropertyMap) error {
// marshal outputs
outs, err := plugin.MarshalProperties(outputs, plugin.MarshalOptions{
KeepUnknowns: true,
})
if err != nil {
return err
}
// submit request
_, err = rm.resmon.RegisterResourceOutputs(context.Background(), &pulumirpc.RegisterResourceOutputsRequest{
Urn: string(urn),
Outputs: outs,
})
return err
}
func (rm *ResourceMonitor) ReadResource(
t tokens.Type,
name string,
id resource.ID,
parent resource.URN,
inputs resource.PropertyMap,
provider,
version,
sourcePosition string,
stackTrace []resource.StackFrame,
parentStackTraceHandle string,
packageRef string,
) (resource.URN, resource.PropertyMap, error) {
// marshal inputs
ins, err := plugin.MarshalProperties(inputs, plugin.MarshalOptions{
KeepUnknowns: true,
KeepResources: true,
})
if err != nil {
return "", nil, err
}
sourcePos, stack, err := marshalSourceInfo(sourcePosition, stackTrace)
if err != nil {
return "", nil, err
}
// submit request
resp, err := rm.resmon.ReadResource(context.Background(), &pulumirpc.ReadResourceRequest{
Type: string(t),
Name: name,
Id: string(id),
Parent: string(parent),
Provider: provider,
Properties: ins,
Version: version,
SourcePosition: sourcePos,
StackTrace: stack,
ParentStackTraceHandle: parentStackTraceHandle,
PackageRef: packageRef,
})
if err != nil {
return "", nil, err
}
// unmarshal outputs
outs, err := rm.unmarshalProperties(resp.Properties)
if err != nil {
return "", nil, err
}
return resource.URN(resp.Urn), outs, nil
}
func (rm *ResourceMonitor) Invoke(tok tokens.ModuleMember, inputs resource.PropertyMap,
provider string, version string, packageRef string,
) (resource.PropertyMap, []*pulumirpc.CheckFailure, error) {
// marshal inputs
ins, err := plugin.MarshalProperties(inputs, plugin.MarshalOptions{
KeepUnknowns: true,
KeepResources: true,
})
if err != nil {
return nil, nil, err
}
// submit request
resp, err := rm.resmon.Invoke(context.Background(), &pulumirpc.ResourceInvokeRequest{
Tok: string(tok),
Provider: provider,
Args: ins,
Version: version,
PackageRef: packageRef,
})
if err != nil {
return nil, nil, err
}
// handle failures
if len(resp.Failures) != 0 {
return nil, resp.Failures, nil
}
// unmarshal outputs
outs, err := rm.unmarshalProperties(resp.Return)
if err != nil {
return nil, nil, err
}
return outs, nil, nil
}
func (rm *ResourceMonitor) Call(
tok tokens.ModuleMember,
args resource.PropertyMap,
argDependencies map[resource.PropertyKey][]resource.URN,
provider string,
version string,
packageRef string,
sourcePosition string,
stackTrace []resource.StackFrame,
parentStackTraceHandle string,
) (resource.PropertyMap, map[resource.PropertyKey][]resource.URN, []*pulumirpc.CheckFailure, error) {
sourcePos, stack, err := marshalSourceInfo(sourcePosition, stackTrace)
if err != nil {
return nil, nil, nil, err
}
// marshal inputs
mArgs, err := plugin.MarshalProperties(args, plugin.MarshalOptions{
KeepUnknowns: true,
KeepResources: true,
KeepSecrets: true,
KeepOutputValues: true,
})
if err != nil {
return nil, nil, nil, err
}
mArgDependencies := make(map[string]*pulumirpc.ResourceCallRequest_ArgumentDependencies)
for k, p := range argDependencies {
urns := make([]string, len(p))
for i, urn := range p {
urns[i] = string(urn)
}
mArgDependencies[string(k)] = &pulumirpc.ResourceCallRequest_ArgumentDependencies{
Urns: urns,
}
}
// submit request
resp, err := rm.resmon.Call(context.Background(), &pulumirpc.ResourceCallRequest{
Tok: string(tok),
Provider: provider,
Args: mArgs,
ArgDependencies: mArgDependencies,
Version: version,
PackageRef: packageRef,
SourcePosition: sourcePos,
StackTrace: stack,
ParentStackTraceHandle: parentStackTraceHandle,
})
if err != nil {
return nil, nil, nil, err
}
// handle failures
if len(resp.Failures) != 0 {
return nil, nil, resp.Failures, nil
}
// unmarshal outputs
outs, err := rm.unmarshalProperties(resp.Return)
if err != nil {
return nil, nil, nil, err
}
// unmarshal return deps
deps := make(map[resource.PropertyKey][]resource.URN)
for k, p := range resp.ReturnDependencies {
var urns []resource.URN
for _, urn := range p.Urns {
urns = append(urns, resource.URN(urn))
}
deps[resource.PropertyKey(k)] = urns
}
return outs, deps, nil, nil
}
func (rm *ResourceMonitor) RegisterStackTransform(callback *pulumirpc.Callback) error {
_, err := rm.resmon.RegisterStackTransform(context.Background(), callback)
return err
}
func (rm *ResourceMonitor) RegisterStackInvokeTransform(callback *pulumirpc.Callback) error {
_, err := rm.resmon.RegisterStackInvokeTransform(context.Background(), callback)
return err
}
func (rm *ResourceMonitor) RegisterPackage(pkg, version, downloadURL string, checksums map[string][]byte,
parameterization *pulumirpc.Parameterization,
) (string, error) {
resp, err := rm.resmon.RegisterPackage(context.Background(), &pulumirpc.RegisterPackageRequest{
Name: pkg,
Version: version,
DownloadUrl: downloadURL,
Checksums: checksums,
Parameterization: parameterization,
})
if err != nil {
return "", err
}
return resp.Ref, nil
}
func (rm *ResourceMonitor) SignalAndWaitForShutdown(ctx context.Context) error {
_, err := rm.resmon.SignalAndWaitForShutdown(ctx, &emptypb.Empty{})
return err
}
func (rm *ResourceMonitor) RegisterResourceHook(ctx context.Context, req *pulumirpc.RegisterResourceHookRequest,
) error {
_, err := rm.resmon.RegisterResourceHook(ctx, req)
return err
}
func prepareTestTimeout(timeout float64) string {
if timeout == 0 {
return ""
}
return time.Duration(timeout * float64(time.Second)).String()
}
// Copyright 2025, 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 deploytest
import (
"context"
"fmt"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil"
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
type ResourceStatus struct {
conn *grpc.ClientConn
client pulumirpc.ResourceStatusClient
}
func NewResourceStatus(address string) (*ResourceStatus, error) {
conn, err := grpc.NewClient(
address,
grpc.WithTransportCredentials(insecure.NewCredentials()),
rpcutil.GrpcChannelOptions(),
)
if err != nil {
return nil, fmt.Errorf("connecting to the resource status service: %w", err)
}
client := pulumirpc.NewResourceStatusClient(conn)
return &ResourceStatus{
conn: conn,
client: client,
}, nil
}
func (rs *ResourceStatus) Close() error {
return rs.conn.Close()
}
func (rs *ResourceStatus) PublishViewSteps(token string, steps []ViewStep) error {
marshaledSteps, err := slice.MapError(steps, rs.marshalStep)
if err != nil {
return fmt.Errorf("marshaling steps: %w", err)
}
req := &pulumirpc.PublishViewStepsRequest{
Token: token,
Steps: marshaledSteps,
}
_, err = rs.client.PublishViewSteps(context.Background(), req)
if err != nil {
return fmt.Errorf("publishing view steps: %w", err)
}
return nil
}
func (rs *ResourceStatus) marshalStep(step ViewStep) (*pulumirpc.ViewStep, error) {
keys := slice.Prealloc[string](len(step.Keys))
for _, key := range step.Keys {
keys = append(keys, string(key))
}
diffs := slice.Prealloc[string](len(step.Diffs))
for _, diff := range step.Diffs {
diffs = append(diffs, string(diff))
}
detailedDiff := rs.unmarshalDetailedDiff(step.DetailedDiff)
return &pulumirpc.ViewStep{
Op: rs.marshalOp(step.Op),
Status: rs.marshalStatus(step.Status),
Error: step.Error,
Old: rs.marshalState(step.Old),
New: rs.marshalState(step.New),
Keys: keys,
Diffs: diffs,
DetailedDiff: detailedDiff,
HasDetailedDiff: len(detailedDiff) > 0,
}, nil
}
func (rs *ResourceStatus) unmarshalDetailedDiff(m map[string]plugin.PropertyDiff) map[string]*pulumirpc.PropertyDiff {
if len(m) == 0 {
return nil
}
result := make(map[string]*pulumirpc.PropertyDiff)
for path, diff := range m {
var kind pulumirpc.PropertyDiff_Kind
switch diff.Kind {
case plugin.DiffAdd:
kind = pulumirpc.PropertyDiff_ADD
case plugin.DiffAddReplace:
kind = pulumirpc.PropertyDiff_ADD_REPLACE
case plugin.DiffDelete:
kind = pulumirpc.PropertyDiff_DELETE
case plugin.DiffDeleteReplace:
kind = pulumirpc.PropertyDiff_DELETE
case plugin.DiffUpdate:
kind = pulumirpc.PropertyDiff_UPDATE
case plugin.DiffUpdateReplace:
kind = pulumirpc.PropertyDiff_UPDATE_REPLACE
default:
panic(fmt.Errorf("unknown diff kind %v", diff.Kind))
}
result[path] = &pulumirpc.PropertyDiff{
Kind: kind,
InputDiff: diff.InputDiff,
}
}
return result
}
func (rs *ResourceStatus) marshalOp(op apitype.OpType) pulumirpc.ViewStep_Op {
switch op {
case apitype.OpSame:
return pulumirpc.ViewStep_SAME
case apitype.OpCreate:
return pulumirpc.ViewStep_CREATE
case apitype.OpUpdate:
return pulumirpc.ViewStep_UPDATE
case apitype.OpDelete:
return pulumirpc.ViewStep_DELETE
case apitype.OpReplace:
return pulumirpc.ViewStep_REPLACE
case apitype.OpCreateReplacement:
return pulumirpc.ViewStep_CREATE_REPLACEMENT
case apitype.OpDeleteReplaced:
return pulumirpc.ViewStep_DELETE_REPLACED
case apitype.OpRead:
return pulumirpc.ViewStep_READ
case apitype.OpReadReplacement:
return pulumirpc.ViewStep_READ_REPLACEMENT
case apitype.OpRefresh:
return pulumirpc.ViewStep_REFRESH
case apitype.OpReadDiscard:
return pulumirpc.ViewStep_READ_DISCARD
case apitype.OpDiscardReplaced:
return pulumirpc.ViewStep_DISCARD_REPLACED
case apitype.OpRemovePendingReplace:
return pulumirpc.ViewStep_REMOVE_PENDING_REPLACE
case apitype.OpImport:
return pulumirpc.ViewStep_IMPORT
case apitype.OpImportReplacement:
return pulumirpc.ViewStep_IMPORT_REPLACEMENT
default:
panic(fmt.Errorf("unknown op %v", op))
}
}
func (rs *ResourceStatus) marshalStatus(status resource.Status) pulumirpc.ViewStep_Status {
switch status {
case resource.StatusOK:
return pulumirpc.ViewStep_OK
case resource.StatusPartialFailure:
return pulumirpc.ViewStep_PARTIAL_FAILURE
case resource.StatusUnknown:
return pulumirpc.ViewStep_UNKNOWN
default:
panic(fmt.Errorf("unknown status %v", status))
}
}
func (rs *ResourceStatus) marshalState(state *ViewStepState) *pulumirpc.ViewStepState {
if state == nil {
return nil
}
inputs, err := plugin.MarshalProperties(state.Inputs, plugin.MarshalOptions{
KeepUnknowns: true,
KeepSecrets: true,
KeepResources: true,
})
if err != nil {
panic(fmt.Errorf("marshaling inputs: %w", err))
}
outputs, err := plugin.MarshalProperties(state.Outputs, plugin.MarshalOptions{
KeepUnknowns: true,
KeepSecrets: true,
KeepResources: true,
})
if err != nil {
panic(fmt.Errorf("marshaling outputs: %w", err))
}
return &pulumirpc.ViewStepState{
Type: string(state.Type),
Name: state.Name,
ParentType: string(state.ParentType),
ParentName: state.ParentName,
Inputs: inputs,
Outputs: outputs,
}
}
type ViewStep struct {
Op apitype.OpType
Status resource.Status
Error string
Old *ViewStepState
New *ViewStepState
Keys []resource.PropertyKey
Diffs []resource.PropertyKey
DetailedDiff map[string]plugin.PropertyDiff
}
type ViewStepState struct {
Type tokens.Type
Name string
ParentType tokens.Type
ParentName string
Inputs resource.PropertyMap
Outputs resource.PropertyMap
}
// Copyright 2016-2023, 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 deploytest
import (
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
)
type NoopSink struct {
LogfF func(sev diag.Severity, diag *diag.Diag, args ...interface{})
}
var _ diag.Sink = (*NoopSink)(nil)
func (s *NoopSink) Logf(sev diag.Severity, diag *diag.Diag, args ...interface{}) {
if s.LogfF != nil {
s.LogfF(sev, diag, args)
}
}
func (s *NoopSink) Debugf(diag *diag.Diag, args ...interface{}) {}
func (s *NoopSink) Infof(diag *diag.Diag, args ...interface{}) {}
func (s *NoopSink) Infoerrf(diag *diag.Diag, args ...interface{}) {}
func (s *NoopSink) Errorf(diag *diag.Diag, args ...interface{}) {}
func (s *NoopSink) Warningf(diag *diag.Diag, args ...interface{}) {}
// Copyright 2016-2023, 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.
//nolint:revive // Legacy package name we don't want to change
package util
import (
"runtime/debug"
"github.com/blang/semver"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)
// SetKnownPluginDownloadURL sets the PluginDownloadURL for the given PluginSpec if it's a known plugin.
// Returns true if it filled in the URL.
func SetKnownPluginDownloadURL(spec *workspace.PluginSpec) bool {
// If the download url is already set don't touch it
if spec.PluginDownloadURL != "" {
return false
}
if spec.Kind == apitype.ResourcePlugin {
for _, plugin := range pulumiversePlugins {
if spec.Name == plugin {
spec.PluginDownloadURL = "github://api.github.com/pulumiverse"
return true
}
}
}
return false
}
// SetKnownPluginVersion sets the Version for the given PluginSpec if it's a known plugin.
// Returns true if it filled in the version.
func SetKnownPluginVersion(spec *workspace.PluginSpec) bool {
// If the version is already set don't touch it
if spec.Version != nil {
return false
}
if spec.Kind == apitype.ConverterPlugin && spec.Name == "yaml" {
// By default use the version of yaml we've linked to. N.B. This has to be tested manually because
// ReadBuildInfo doesn't return anything in test builds (https://github.com/golang/go/issues/33976).
info, ok := debug.ReadBuildInfo()
contract.Assertf(ok, "expected to be able to read build info")
for _, dep := range info.Deps {
if dep.Path == "github.com/pulumi/pulumi-yaml" {
v, err := semver.ParseTolerant(dep.Version)
contract.AssertNoErrorf(err, "expected to be able to parse version for yaml got %q", dep.Version)
spec.Version = &v
return true
}
}
}
return false
}
// Copyright 2016-2024, 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 workspace
import (
"path/filepath"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)
// Context is an interface that represents the context of a workspace. It provides access to loading projects and
// plugins.
type Context interface {
// ReadProject attempts to detect and read a Pulumi project for the current workspace. If the
// project is successfully detected and read, it is returned along with the path to its containing
// directory, which will be used as the root of the project's Pulumi program.
ReadProject() (*workspace.Project, string, error)
// GetStoredCredentials returns any credentials stored on the local machine.
GetStoredCredentials() (workspace.Credentials, error)
}
var Instance Context = &workspaceContext{}
type workspaceContext struct{}
func (c *workspaceContext) ReadProject() (*workspace.Project, string, error) {
proj, path, err := workspace.DetectProjectAndPath()
if err != nil {
return nil, "", err
}
return proj, filepath.Dir(path), nil
}
func (c *workspaceContext) GetStoredCredentials() (workspace.Credentials, error) {
return workspace.GetStoredCredentials()
}
// Copyright 2024, 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 workspace
import (
"os"
"github.com/pulumi/pulumi/sdk/v3/go/common/env"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)
// GetCurrentCloudURL returns the URL of the cloud we are currently connected to. This may be empty if we
// have not logged in. Note if PULUMI_BACKEND_URL is set, the corresponding value is returned
// instead irrespective of the backend for current project or stored credentials.
func GetCurrentCloudURL(ws Context, e env.Env, project *workspace.Project) (string, error) {
// Allow PULUMI_BACKEND_URL to override the current cloud URL selection
if backend := e.GetString(env.BackendURL); backend != "" {
return backend, nil
}
var url string
if project != nil {
if project.Backend != nil {
url = project.Backend.URL
}
}
if url == "" {
creds, err := ws.GetStoredCredentials()
if err != nil {
return "", err
}
url = creds.Current
}
return url, nil
}
// GetCloudInsecure returns if this cloud url is saved as one that should use insecure transport.
func GetCloudInsecure(ws Context, cloudURL string) bool {
insecure := false
creds, err := ws.GetStoredCredentials()
// If this errors just assume insecure == false
if err == nil {
if account, has := creds.Accounts[cloudURL]; has {
insecure = account.Insecure
}
}
return insecure
}
func GetBackendConfigDefaultOrg(project *workspace.Project) (string, error) {
config, err := workspace.GetPulumiConfig()
if err != nil && !os.IsNotExist(err) {
return "", err
}
// TODO: This should use injected interfaces, not the global instances.
backendURL, err := GetCurrentCloudURL(Instance, env.Global(), project)
if err != nil {
return "", err
}
if beConfig, ok := config.BackendConfig[backendURL]; ok {
if beConfig.DefaultOrg != "" {
return beConfig.DefaultOrg, nil
}
}
return "", nil
}
// Copyright 2016-2024, 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 workspace
import (
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)
type MockContext struct {
ReadProjectF func() (*workspace.Project, string, error)
GetStoredCredentialsF func() (workspace.Credentials, error)
}
func (c *MockContext) ReadProject() (*workspace.Project, string, error) {
if c.ReadProjectF != nil {
return c.ReadProjectF()
}
return nil, "", workspace.ErrProjectNotFound
}
func (c *MockContext) GetStoredCredentials() (workspace.Credentials, error) {
if c.GetStoredCredentialsF != nil {
return c.GetStoredCredentialsF()
}
return workspace.Credentials{}, nil
}
// Copyright 2016-2023, 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 workspace
import (
"context"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"time"
"github.com/blang/semver"
"github.com/pulumi/pulumi/pkg/v3/util"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/archive"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/fsutil"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)
// InstallPluginError is returned by InstallPlugin if we couldn't install the plugin
type InstallPluginError struct {
// The specification of the plugin to install
Spec workspace.PluginSpec
// The underlying error that occurred during the download or install.
Err error
}
func (err *InstallPluginError) Error() string {
var server string
if err.Spec.PluginDownloadURL != "" {
server = " --server " + err.Spec.PluginDownloadURL
}
if err.Spec.Version != nil {
return fmt.Sprintf("Could not automatically download and install %[1]s plugin 'pulumi-%[1]s-%[2]s'"+
" at version v%[3]s"+
", install the plugin using `pulumi plugin install %[1]s %[2]s v%[3]s%[4]s`: %[5]v",
err.Spec.Kind, err.Spec.Name, err.Spec.Version, server, err.Err)
}
return fmt.Sprintf("Could not automatically download and install %[1]s plugin 'pulumi-%[1]s-%[2]s'"+
", install the plugin using `pulumi plugin install %[1]s %[2]s%[3]s`: %[4]v",
err.Spec.Kind, err.Spec.Name, server, err.Err)
}
func (err *InstallPluginError) Unwrap() error {
return err.Err
}
func InstallPlugin(ctx context.Context, pluginSpec workspace.PluginSpec,
log func(sev diag.Severity, msg string),
) (*semver.Version, error) {
util.SetKnownPluginDownloadURL(&pluginSpec)
util.SetKnownPluginVersion(&pluginSpec)
if pluginSpec.Version == nil {
var err error
pluginSpec.Version, err = pluginSpec.GetLatestVersion(ctx)
if err != nil {
return nil, fmt.Errorf("could not find latest version for provider %s: %w", pluginSpec.Name, err)
}
}
wrapper := func(stream io.ReadCloser, size int64) io.ReadCloser {
// Log at info but to stderr so we don't pollute stdout for commands like `package get-schema`
log(diag.Infoerr, "Downloading provider: "+pluginSpec.Name)
return stream
}
retry := func(err error, attempt int, limit int, delay time.Duration) {
log(diag.Warning, fmt.Sprintf("error downloading provider: %s\n"+
"Will retry in %v [%d/%d]", err, delay, attempt, limit))
}
logging.V(1).Infof("Automatically downloading provider %s", pluginSpec.Name)
downloadedFile, err := workspace.DownloadToFile(ctx, pluginSpec, wrapper, retry)
if err != nil {
return nil, &InstallPluginError{
Spec: pluginSpec,
Err: fmt.Errorf("error downloading provider %s to file: %w", pluginSpec.Name, err),
}
}
logging.V(1).Infof("Automatically installing provider %s", pluginSpec.Name)
err = InstallPluginContent(context.Background(), pluginSpec, tarPlugin{downloadedFile}, false)
if err != nil {
return nil, &InstallPluginError{
Spec: pluginSpec,
Err: fmt.Errorf("error installing provider %s: %w", pluginSpec.Name, err),
}
}
return pluginSpec.Version, nil
}
type PluginContent interface {
io.Closer
writeToDir(pathToDir string) error
}
func SingleFilePlugin(f *os.File, spec workspace.PluginSpec) PluginContent {
return singleFilePlugin{F: f, Kind: spec.Kind, Name: spec.Name}
}
type singleFilePlugin struct {
F *os.File
Kind apitype.PluginKind
Name string
}
func (p singleFilePlugin) writeToDir(finalDir string) error {
bytes, err := io.ReadAll(p.F)
if err != nil {
return err
}
finalPath := filepath.Join(finalDir, fmt.Sprintf("pulumi-%s-%s", p.Kind, p.Name))
if runtime.GOOS == "windows" {
finalPath += ".exe"
}
// We are writing an executable.
return os.WriteFile(finalPath, bytes, 0o700) //nolint:gosec
}
func (p singleFilePlugin) Close() error {
return p.F.Close()
}
func TarPlugin(tgz io.ReadCloser) PluginContent {
return tarPlugin{Tgz: tgz}
}
type tarPlugin struct {
Tgz io.ReadCloser
}
func (p tarPlugin) Close() error {
return p.Tgz.Close()
}
func (p tarPlugin) writeToDir(finalPath string) error {
return archive.ExtractTGZ(p.Tgz, finalPath)
}
func DirPlugin(rootPath string) PluginContent {
return dirPlugin{Root: rootPath}
}
type dirPlugin struct {
Root string
}
func (p dirPlugin) Close() error {
return nil
}
func (p dirPlugin) writeToDir(dstRoot string) error {
return filepath.WalkDir(p.Root, func(srcPath string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
relPath := strings.TrimPrefix(srcPath, p.Root)
dstPath := filepath.Join(dstRoot, relPath)
if srcPath == p.Root {
return nil
}
if d.IsDir() {
return os.Mkdir(dstPath, 0o700)
}
src, err := os.Open(srcPath)
if err != nil {
return err
}
info, err := d.Info()
if err != nil {
return err
}
bytes, err := io.ReadAll(src)
if err != nil {
return err
}
return os.WriteFile(dstPath, bytes, info.Mode())
})
}
// installLock acquires a file lock used to prevent concurrent installs.
func installLock(spec workspace.PluginSpec) (unlock func(), err error) {
finalDir, err := spec.DirPath()
if err != nil {
return nil, err
}
lockFilePath := finalDir + ".lock"
if err := os.MkdirAll(filepath.Dir(lockFilePath), 0o700); err != nil {
return nil, fmt.Errorf("creating plugin root: %w", err)
}
mutex := fsutil.NewFileMutex(lockFilePath)
if err := mutex.Lock(); err != nil {
return nil, err
}
return func() {
contract.IgnoreError(mutex.Unlock())
}, nil
}
// InstallPluginContent installs a plugin's tarball into the cache. It validates that plugin names are in the expected
// format. Previous versions of Pulumi extracted the tarball to a temp directory first, and then renamed the temp
// directory to the final directory. The rename operation fails often enough on Windows due to aggressive virus scanners
// opening files in the temp directory. To address this, we now extract the tarball directly into the final directory,
// and use file locks to prevent concurrent installs.
//
// Each plugin has its own file lock, with the same name as the plugin directory, with a `.lock` suffix.
// During installation an empty file with a `.partial` suffix is created, indicating that installation is in-progress.
// The `.partial` file is deleted when installation is complete, indicating that the plugin has finished installing.
// If a failure occurs during installation, the `.partial` file will remain, indicating the plugin wasn't fully
// installed. The next time the plugin is installed, the old installation directory will be removed and replaced with
// a fresh installation.
func InstallPluginContent(ctx context.Context, spec workspace.PluginSpec, content PluginContent, reinstall bool) error {
defer contract.IgnoreClose(content)
// Fetch the directory into which we will expand this tarball.
finalDir, err := spec.DirPath()
if err != nil {
return err
}
// Create a file lock file at <pluginsdir>/<kind>-<name>-<version>.lock.
unlock, err := installLock(spec)
if err != nil {
return err
}
defer unlock()
// Cleanup any temp dirs from failed installations of this plugin from previous versions of Pulumi.
if err := cleanupTempDirs(finalDir); err != nil {
// We don't want to fail the installation if there was an error cleaning up these old temp dirs.
// Instead, log the error and continue on.
logging.V(5).Infof("Install: Error cleaning up temp dirs: %s", err)
}
// Get the partial file path (e.g. <pluginsdir>/<kind>-<name>-<version>.partial).
partialFilePath, err := spec.PartialFilePath()
if err != nil {
return err
}
// Check whether the directory exists while we were waiting on the lock.
_, finalDirStatErr := os.Stat(finalDir)
if finalDirStatErr == nil {
_, partialFileStatErr := os.Stat(partialFilePath)
if partialFileStatErr != nil {
if !os.IsNotExist(partialFileStatErr) {
return partialFileStatErr
}
if !reinstall {
// finalDir exists, there's no partial file, and we're not reinstalling, so the plugin is already
// installed.
return nil
}
}
// Either the partial file exists--meaning a previous attempt at installing the plugin failed--or we're
// deliberately reinstalling the plugin. Delete finalDir so we can try installing again. There's no need to
// delete the partial file since we'd just be recreating it again below anyway.
if err := os.RemoveAll(finalDir); err != nil {
return err
}
} else if !os.IsNotExist(finalDirStatErr) {
return finalDirStatErr
}
// Create an empty partial file to indicate installation is in-progress.
if err := os.WriteFile(partialFilePath, nil, 0o600); err != nil {
return err
}
// Create the final directory.
if err := os.MkdirAll(finalDir, 0o700); err != nil {
return err
}
if err := content.writeToDir(finalDir); err != nil {
return err
}
// Even though we deferred closing the tarball at the beginning of this function, go ahead and explicitly close
// it now since we're finished extracting it, to prevent subsequent output from being displayed oddly with
// the progress bar.
contract.IgnoreClose(content)
err = spec.InstallDependencies(ctx)
if err != nil {
return err
}
// Installation is complete. Remove the partial file.
return os.Remove(partialFilePath)
}
// installingPluginRegexp matches the name of temporary folders. Previous versions of Pulumi first extracted
// plugins to a temporary folder with a suffix of `.tmpXXXXXX` (where `XXXXXX`) is a random number, from
// os.CreateTemp. We should ignore these folders.
var installingPluginRegexp = regexp.MustCompile(`\.tmp[0-9]+$`)
// cleanupTempDirs cleans up leftover temp dirs from failed installs with previous versions of Pulumi.
func cleanupTempDirs(finalDir string) error {
dir := filepath.Dir(finalDir)
infos, err := os.ReadDir(dir)
if err != nil {
return err
}
for _, info := range infos {
// Temp dirs have a suffix of `.tmpXXXXXX` (where `XXXXXX`) is a random number,
// from os.CreateTemp.
if info.IsDir() && installingPluginRegexp.MatchString(info.Name()) {
path := filepath.Join(dir, info.Name())
if err := os.RemoveAll(path); err != nil {
return fmt.Errorf("cleaning up temp dir %s: %w", path, err)
}
}
}
return nil
}
// Copyright 2016-2023, 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 workspace
import (
"errors"
"regexp"
"strings"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
)
const (
defaultProjectName = "project"
)
// ValidateProjectName ensures a project name is valid, if it is not it returns an error with a message suitable
// for display to an end user.
func ValidateProjectName(s string) error {
if err := tokens.ValidateProjectName(s); err != nil {
return err
}
// This is needed to stop cyclic imports in DotNet projects
if strings.ToLower(s) == "pulumi" || strings.HasPrefix(strings.ToLower(s), "pulumi.") {
return errors.New("project name must not be `Pulumi` and must not start with the prefix `Pulumi.` " +
"to avoid collision with standard libraries")
}
return nil
}
// ValueOrSanitizedDefaultProjectName returns the value or a sanitized valid project name
// based on defaultNameToSanitize.
func ValueOrSanitizedDefaultProjectName(name string, projectName string, defaultNameToSanitize string) string {
// If we have a name, use it.
if name != "" {
return name
}
// If the project already has a name that isn't a replacement string, use it.
if projectName != "${PROJECT}" {
return projectName
}
// Otherwise, get a sanitized version of `defaultNameToSanitize`.
return getValidProjectName(defaultNameToSanitize)
}
// ValueOrDefaultProjectDescription returns the value or defaultDescription.
func ValueOrDefaultProjectDescription(
description string, projectDescription string, defaultDescription string,
) string {
// If we have a description, use it.
if description != "" {
return description
}
// If the project already has a description that isn't a replacement string, use it.
if projectDescription != "${DESCRIPTION}" {
return projectDescription
}
// Otherwise, use the default, which may be an empty string.
return defaultDescription
}
// getValidProjectName returns a valid project name based on the passed-in name.
func getValidProjectName(name string) string {
// If the name is valid, return it.
if ValidateProjectName(name) == nil {
return name
}
// Strip any invalid chars from the name.
r := regexp.MustCompile("[^a-zA-Z0-9_.-]")
name = r.ReplaceAllString(name, "")
// See if the name is now valid
if ValidateProjectName(name) == nil {
return name
}
// If we couldn't come up with a valid project name, fallback to a default.
return defaultProjectName
}
// ValidateProjectDescription ensures a project description name is valid, if it is not it returns an error with a
// message suitable for display to an end user.
func ValidateProjectDescription(s string) error {
const maxTagValueLength = 256
if len(s) > maxTagValueLength {
return errors.New("a project description must be 256 characters or less")
}
return nil
}
// 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 colors
import (
"bytes"
"fmt"
"io"
"strings"
"github.com/rivo/uniseg"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
const (
colorLeft = "<{%"
colorRight = "%}>"
)
type Color = string
var disableColorization bool
func command(s string) string {
return colorLeft + s + colorRight
}
// TrimPartialCommand returns the input string with any partial colorization command trimmed off of the right end of
// the string.
func TrimPartialCommand(s string) string {
// First check for a partial left delimiter at the end of the string.
partialDelimLeft := colorLeft
if len(partialDelimLeft) > len(s) {
partialDelimLeft = partialDelimLeft[:len(s)]
}
for len(partialDelimLeft) > 0 {
trailer := s[len(s)-len(partialDelimLeft):]
if trailer == partialDelimLeft {
return s[:len(s)-len(partialDelimLeft)]
}
partialDelimLeft = partialDelimLeft[:len(partialDelimLeft)-1]
}
// Next check for a complete left delimiter. If there no complete left delimiter, just return the string as-is.
lastDelimLeft := strings.LastIndex(s, colorLeft)
if lastDelimLeft == -1 {
return s
}
// If there is a complete left delimiter, look for a matching complete right delimiter. If there is a match, return
// the string as-is.
if strings.Contains(s[lastDelimLeft:], colorRight) {
return s
}
// Otherwise, return the string up to but not including the incomplete left delimiter.
return s[:lastDelimLeft]
}
func Colorize(s fmt.Stringer) string {
return colorizeText(s.String(), Always, -1)
}
func writeCodes(w io.StringWriter, codes ...string) {
_, err := w.WriteString("\x1b[")
contract.IgnoreError(err)
_, err = w.WriteString(strings.Join(codes, ";"))
contract.IgnoreError(err)
_, err = w.WriteString("m")
contract.IgnoreError(err)
}
func writeDirective(w io.StringWriter, c Colorization, directive Color) {
if disableColorization || c == Never {
return
}
if c == Raw {
_, err := w.WriteString(directive)
contract.IgnoreError(err)
return
}
switch directive {
case Reset: // command("reset")
writeCodes(w, "0")
case Bold: // command("bold")
writeCodes(w, "1")
case Underline: // command("underline")
writeCodes(w, "4")
case Red: // command("fg 1")
writeCodes(w, "38", "5", "1")
case Green: // command("fg 2")
writeCodes(w, "38", "5", "2")
case Yellow: // command("fg 3")
writeCodes(w, "38", "5", "3")
case Blue: // command("fg 4")
writeCodes(w, "38", "5", "4")
case Magenta: // command("fg 5")
writeCodes(w, "38", "5", "5")
case Cyan: // command("fg 6")
writeCodes(w, "38", "5", "6")
case BrightRed: // command("fg 9")
writeCodes(w, "38", "5", "9")
case BrightGreen: // command("fg 10")
writeCodes(w, "38", "5", "10")
case BrightBlue: // command("fg 12")
writeCodes(w, "38", "5", "12")
case BrightMagenta: // command("fg 13")
writeCodes(w, "38", "5", "13")
case BrightCyan: // command("fg 14")
writeCodes(w, "38", "5", "14")
case RedBackground: // command("bg 1")
writeCodes(w, "48", "5", "1")
case GreenBackground: // command("bg 2")
writeCodes(w, "48", "5", "2")
case YellowBackground: // command("bg 3")
writeCodes(w, "48", "5", "3")
case BlueBackground: // command("bg 4")
writeCodes(w, "48", "5", "4")
case Black: // command("fg 0") // Only use with background colors.
writeCodes(w, "38", "5", "0")
case BrightBlack: // command("fg 8")
writeCodes(w, "38", "5", "8")
default:
contract.Failf("Unrecognized color code: %q", directive)
}
}
type iterator struct {
input string
}
func (it *iterator) next(text, directive *string) bool {
if len(it.input) == 0 {
return false
}
// Do we have another directive to process?
nextDirectiveStart := strings.Index(it.input, colorLeft)
if nextDirectiveStart == -1 {
*text, *directive, it.input = it.input, "", ""
return true
}
// Copy the text up to but not including the delimiter into the buffer.
*text = it.input[:nextDirectiveStart]
// If we have a start delimiter but no end delimiter, terminate. The partial command will not be present in the
// output. Make sure we look for the for end delimiter _after_ the start delimiter.
nextDirectiveEnd := strings.Index(it.input[nextDirectiveStart:], colorRight)
if nextDirectiveEnd != -1 {
// Correct the index given we searched starting from nextDirectiveStart
nextDirectiveEnd += nextDirectiveStart
*directive = it.input[nextDirectiveStart : nextDirectiveEnd+len(colorRight)]
it.input = it.input[nextDirectiveEnd+len(colorRight):]
} else {
*directive, it.input = "", ""
}
return true
}
func colorizeText(s string, c Colorization, maxWidth int) string {
var buf bytes.Buffer
width, reset := 0, false
i := iterator{s}
var text, directive string
for i.next(&text, &directive) {
// If the text is the entire original string, return it as-is.
if len(text) == len(s) {
if maxWidth >= 0 {
return clampString(text, maxWidth)
}
return text
}
if buf.Cap() < len(text) {
buf.Grow(len(text))
}
if maxWidth >= 0 {
graphemes := uniseg.NewGraphemes(text)
for graphemes.Next() {
if width == maxWidth {
if reset {
writeDirective(&buf, c, Reset)
}
return buf.String()
}
start, end := graphemes.Positions()
_, err := buf.WriteString(text[start:end])
contract.IgnoreError(err)
width++
}
} else {
_, err := buf.WriteString(text)
contract.IgnoreError(err)
}
if directive != "" {
writeDirective(&buf, c, directive)
}
reset = directive != Reset
}
return buf.String()
}
func measureText(s string) int {
width := 0
i := iterator{s}
var text, directive string
for i.next(&text, &directive) {
width += uniseg.StringWidth(text)
}
return width
}
func clampString(s string, maxWidth int) string {
width, end := 0, 0
graphemes := uniseg.NewGraphemes(s)
for graphemes.Next() && graphemes.Width() <= maxWidth-width {
_, end = graphemes.Positions()
width += graphemes.Width()
}
return s[:end]
}
// Highlight takes an input string, a sequence of commands, and replaces all occurrences of that string with
// a "highlighted" version surrounded by those commands and a final reset afterwards.
func Highlight(s, text, commands string) string {
return strings.ReplaceAll(s, text, commands+text+Reset)
}
var (
Reset = command("reset")
Bold = command("bold")
Underline = command("underline")
)
// Basic colors.
var (
Red = command("fg 1")
Green = command("fg 2")
Yellow = command("fg 3")
Blue = command("fg 4")
Magenta = command("fg 5")
Cyan = command("fg 6")
BrightRed = command("fg 9")
BrightGreen = command("fg 10")
BrightBlue = command("fg 12")
BrightMagenta = command("fg 13")
BrightCyan = command("fg 14")
RedBackground = command("bg 1")
GreenBackground = command("bg 2")
YellowBackground = command("bg 3")
BlueBackground = command("bg 4")
// We explicitly do not expose blacks/whites. They're problematic given that we don't know what
// terminal settings the user has. Best to avoid them and not run into contrast problems.
Black = command("fg 0") // Only use with background colors.
// White = command("fg 7")
BrightBlack = command("fg 8")
// BrightYellow = command("fg 11")
// BrightWhite = command("fg 15")
)
// Special predefined colors for logical conditions.
var (
SpecImportant = Yellow // for particularly noteworthy messages.
// for notes that can be skimmed or aren't very important. Just use the standard terminal text
// color.
SpecUnimportant = Reset
SpecDebug = SpecUnimportant // for debugging.
SpecInfo = Magenta // for information.
SpecError = Red // for errors.
SpecWarning = Yellow // for warnings.
SpecHeadline = BrightMagenta + Bold // for headings in the CLI.
SpecSubHeadline = Bold // for subheadings in the CLI.
SpecPrompt = Cyan + Bold // for prompting the user.
SpecAttention = BrightRed // for messages that are meant to grab attention.
// for simple notes. Just use the standard terminal text color.
SpecNote = Reset
SpecCreate = Green // for adds (in the diff sense).
SpecUpdate = Yellow // for changes (in the diff sense).
SpecReplace = BrightMagenta // for replacements (in the diff sense).
SpecDelete = Red // for deletes (in the diff sense).
SpecCreateReplacement = BrightGreen // for replacement creates (in the diff sense).
SpecDeleteReplaced = BrightRed // for replacement deletes (in the diff sense).
SpecRead = BrightCyan // for reads
)
// 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 colors
import "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
type Colorization string
const (
// Always colorizes text.
Always Colorization = "always"
// Never colorizes text.
Never Colorization = "never"
// Raw returns text with the raw control sequences, rather than colorizing them.
Raw Colorization = "raw"
)
// Colorize conditionally colorizes the given string based on the kind of colorization selected.
func (c Colorization) Colorize(v string) string {
return c.ColorizeWithMaxWidth(v, -1)
}
// ColorizeWithMaxWidth conditionally colorizes the given string based on the kind of colorization selected.
// The result will contain no more than maxWidth user-perceived characters (grapheme clusters).
func (c Colorization) ColorizeWithMaxWidth(v string, maxWidth int) string {
switch c {
case Raw:
// Don't touch the string. Output control sequences as is.
return v
case Always:
// Convert the control sequences into appropriate console escapes for the platform we're on.
return colorizeText(v, Always, maxWidth)
case Never:
return colorizeText(v, Never, maxWidth)
default:
contract.Failf("Unrecognized colorization mode: %v", c)
return ""
}
}
// TrimColorizedString takes a string with embedded color tags and returns a new string (still with
// embedded color tags) such that the number of user-perceived characters (grapheme clusters) in
// the result is no greater than maxWidth. This is useful for scenarios where the string has to be
// printed in a a context where there is a max allowed width. In these scenarios, we can't just
// measure the length of the string as the embedded color tags would count against it, even though
// they end up with no length when actually interpreted by the console.
func TrimColorizedString(v string, maxWidth int) string {
return colorizeText(v, Raw, maxWidth)
}
// MeasureColorizedString measures the number of user-perceived characters (grapheme clusters) in the
// given string with embedded color tags. Color tags do not contribute to the total.
func MeasureColorizedString(v string) int {
return measureText(v)
}
// 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 diag
import (
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
)
// ID is a unique diagnostics identifier.
type ID int
// Diag is an instance of an error or warning generated by the compiler.
type Diag struct {
URN resource.URN // Resource this diagnostics is associated with. Empty if not associated with any resource.
ID ID // a unique identifier for this diagnostic.
Message string // a human-friendly message for this diagnostic.
Raw bool // true if this diagnostic should not be formatted when displayed.
// An ID used to collate a stream of conceptually sequential messages. 0 means that the message
// is not part of any sequential message stream.
StreamID int32
}
// Message returns an anonymous diagnostic message without any source or ID information.
func Message(urn resource.URN, msg string) *Diag {
return &Diag{URN: urn, Message: msg}
}
// RawMessage returns an anonymous diagnostic message without any source or ID information that will not be rendered
// with Sprintf.
func RawMessage(urn resource.URN, msg string) *Diag {
return &Diag{URN: urn, Message: msg, Raw: true}
}
// StreamMessage returns an anonymous diagnostic message without any source or ID information that
// is associated with the given stream ID. Displays can use this ID to combine all the messages
// from a single stream into an entire message, while still rendering the pieces as they come in.
func StreamMessage(urn resource.URN, msg string, streamID int32) *Diag {
return &Diag{URN: urn, Message: msg, Raw: true, StreamID: streamID}
}
// 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 diag
import (
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
)
// newError registers a new error message underneath the given id.
func newError(urn resource.URN, id ID, message string) *Diag {
return &Diag{URN: urn, ID: id, Message: message}
}
// Plan and apply errors are in the [2000,3000) range.
func GetResourceOperationFailedError(urn resource.URN) *Diag {
return newError(urn, 2000, "%v")
}
func GetDuplicateResourceURNError(urn resource.URN) *Diag {
return newError(urn, 2001, "Duplicate resource URN '%v'; try giving it a unique name")
}
func GetResourceInvalidError(urn resource.URN) *Diag {
return newError(urn, 2002, "%v resource '%v' has a problem: %v")
}
func GetResourcePropertyInvalidValueError(urn resource.URN) *Diag {
return newError(urn, 2003, "%v resource '%v': property %v value %v has a problem: %v")
}
func GetPreviewFailedError(urn resource.URN) *Diag {
return newError(urn, 2005, "Preview failed: %v")
}
func GetBadProviderError(urn resource.URN) *Diag {
return newError(urn, 2006, "bad provider reference '%v' for resource '%v': %v")
}
func GetUnknownProviderError(urn resource.URN) *Diag {
return newError(urn, 2007, "unknown provider '%v' for resource '%v'")
}
func GetDuplicateResourceAliasError(urn resource.URN) *Diag {
return newError(urn, 2008,
"Duplicate resource alias '%v' applied to resource with URN '%v' conflicting with resource with URN '%v'",
)
}
func GetTargetCouldNotBeFoundError() *Diag {
return newError("", 2010, "Target '%v' could not be found in the stack.")
}
func GetTargetCouldNotBeFoundDidYouForgetError() *Diag {
return newError("", 2011, "Target '%v' could not be found in the stack. "+
"Did you forget to escape $ in your shell?")
}
func GetCannotDeleteParentResourceWithoutAlsoDeletingChildError(urn resource.URN) *Diag {
return newError(urn, 2012, "Cannot delete parent resource '%v' without also deleting child '%v'.")
}
func GetResourceWillBeCreatedButWasNotSpecifiedInTargetList(urn resource.URN) *Diag {
return newError(urn, 2013, `Resource '%v' depends on '%v' which was was not specified in --target list.`)
}
func GetResourceWillBeDestroyedButWasNotSpecifiedInTargetList(urn resource.URN) *Diag {
return newError(urn, 2014, `Resource '%v' will be destroyed but was not specified in --target list.
Either include resource in --target list or pass --target-dependents to proceed.`)
}
func GetDefaultProviderDenied(urn resource.URN) *Diag {
return newError(urn, 2015, `Default provider for '%v' disabled. '%v' must use an explicit provider.`)
}
func GetDuplicateResourceAliasedError(urn resource.URN) *Diag {
return newError(urn, 2016,
"Duplicate resource URN '%v' conflicting with alias on resource with URN '%v'",
)
}
func GetCallFailedError() *Diag {
return newError("", 2017, "call to function '%v' failed: %v")
}
// Copyright 2025, 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 diag
import (
"sync"
)
// MockSink is a thread safe mock implementation of the Sink interface that just records all the messages.
type MockSink struct {
lock sync.Mutex
Messages map[Severity][]MockMessage
}
type MockMessage struct {
Diag *Diag
Args []interface{}
}
func (d *MockSink) Logf(sev Severity, dia *Diag, args ...interface{}) {
d.lock.Lock()
defer d.lock.Unlock()
if d.Messages == nil {
d.Messages = make(map[Severity][]MockMessage)
}
d.Messages[sev] = append(d.Messages[sev], MockMessage{
Diag: dia,
Args: args,
})
}
func (d *MockSink) Debugf(dia *Diag, args ...interface{}) {
d.Logf(Debug, dia, args...)
}
func (d *MockSink) Infof(dia *Diag, args ...interface{}) {
d.Logf(Info, dia, args...)
}
func (d *MockSink) Infoerrf(dia *Diag, args ...interface{}) {
d.Logf(Infoerr, dia, args...)
}
func (d *MockSink) Errorf(dia *Diag, args ...interface{}) {
d.Logf(Error, dia, args...)
}
func (d *MockSink) Warningf(dia *Diag, args ...interface{}) {
d.Logf(Warning, dia, args...)
}
// 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 diag
import (
"bytes"
"fmt"
"io"
"sync"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
)
// Sink facilitates pluggable diagnostics messages.
type Sink interface {
// Logf issues a log message.
Logf(sev Severity, diag *Diag, args ...interface{})
// Debugf issues a debugging message.
Debugf(diag *Diag, args ...interface{})
// Infof issues an informational message (to stdout).
Infof(diag *Diag, args ...interface{})
// Infoerrf issues an informational message (to stderr).
Infoerrf(diag *Diag, args ...interface{})
// Errorf issues a new error diagnostic.
Errorf(diag *Diag, args ...interface{})
// Warningf issues a new warning diagnostic.
Warningf(diag *Diag, args ...interface{})
}
// Severity dictates the kind of diagnostic.
type Severity string
const (
Debug Severity = "debug"
Info Severity = "info"
Infoerr Severity = "info#err"
Warning Severity = "warning"
Error Severity = "error"
)
// FormatOptions controls the output style and content.
type FormatOptions struct {
Color colors.Colorization // how output should be colorized.
Debug bool // if true, debugging will be output to stdout.
}
// DefaultSink returns a default sink that simply logs output to stderr/stdout.
func DefaultSink(stdout io.Writer, stderr io.Writer, opts FormatOptions) Sink {
contract.Requiref(stdout != nil, "stdout", "must not be nil")
contract.Requiref(stderr != nil, "stderr", "must not be nil")
stdoutMu := &sync.Mutex{}
stderrMu := &sync.Mutex{}
func() {
defer func() {
// The == check below can panic if stdout and stderr are not comparable.
// If that happens, ignore the panic and use separate mutexes.
_ = recover()
}()
if stdout == stderr {
// If stdout and stderr point to the same stream,
// use the same mutex for them.
stderrMu = stdoutMu
}
}()
// Wrap the stdout and stderr writers in a mutex
// to ensure that we don't interleave output.
stdout = &syncWriter{Writer: stdout, mu: stdoutMu}
stderr = &syncWriter{Writer: stderr, mu: stderrMu}
// Discard debug output by default unless requested.
debug := io.Discard
if opts.Debug {
debug = stdout
}
return newDefaultSink(opts, map[Severity]io.Writer{
Debug: debug,
Info: stdout,
Infoerr: stderr,
Error: stderr,
Warning: stderr,
})
}
func newDefaultSink(opts FormatOptions, writers map[Severity]io.Writer) *defaultSink {
contract.Assertf(writers[Debug] != nil, "Writer for %v must be set", Debug)
contract.Assertf(writers[Info] != nil, "Writer for %v must be set", Info)
contract.Assertf(writers[Infoerr] != nil, "Writer for %v must be set", Infoerr)
contract.Assertf(writers[Error] != nil, "Writer for %v must be set", Error)
contract.Assertf(writers[Warning] != nil, "Writer for %v must be set", Warning)
contract.Assertf(opts.Color != "", "FormatOptions.Color must be set")
return &defaultSink{
opts: opts,
writers: writers,
}
}
const DefaultSinkIDPrefix = "PU"
// defaultSink is the default sink which logs output to stderr/stdout.
type defaultSink struct {
opts FormatOptions // a set of options that control output style and content.
writers map[Severity]io.Writer // the writers to use for each kind of diagnostic severity.
}
func (d *defaultSink) Logf(sev Severity, diag *Diag, args ...interface{}) {
switch sev {
case Debug:
d.Debugf(diag, args...)
case Info:
d.Infof(diag, args...)
case Infoerr:
d.Infoerrf(diag, args...)
case Warning:
d.Warningf(diag, args...)
case Error:
d.Errorf(diag, args...)
default:
contract.Failf("Unrecognized severity: %v", sev)
}
}
func (d *defaultSink) createMessage(sev Severity, diag *Diag, args ...interface{}) string {
prefix, msg := d.Stringify(sev, diag, args...)
return prefix + msg
}
func (d *defaultSink) Debugf(diag *Diag, args ...interface{}) {
// For debug messages, write both to the glogger and a stream, if there is one.
logging.V(3).Infof(diag.Message, args...)
msg := d.createMessage(Debug, diag, args...)
if logging.V(9) {
logging.V(9).Infof("defaultSink::Debug(%v)", msg[:len(msg)-1])
}
d.print(Debug, msg)
}
func (d *defaultSink) Infof(diag *Diag, args ...interface{}) {
msg := d.createMessage(Info, diag, args...)
if logging.V(5) {
logging.V(5).Infof("defaultSink::Info(%v)", msg[:len(msg)-1])
}
d.print(Info, msg)
}
func (d *defaultSink) Infoerrf(diag *Diag, args ...interface{}) {
msg := d.createMessage(Info /* not Infoerr, just "info: "*/, diag, args...)
if logging.V(5) {
logging.V(5).Infof("defaultSink::Infoerr(%v)", msg[:len(msg)-1])
}
d.print(Infoerr, msg)
}
func (d *defaultSink) Errorf(diag *Diag, args ...interface{}) {
msg := d.createMessage(Error, diag, args...)
if logging.V(5) {
logging.V(5).Infof("defaultSink::Error(%v)", msg[:len(msg)-1])
}
d.print(Error, msg)
}
func (d *defaultSink) Warningf(diag *Diag, args ...interface{}) {
msg := d.createMessage(Warning, diag, args...)
if logging.V(5) {
logging.V(5).Infof("defaultSink::Warning(%v)", msg[:len(msg)-1])
}
d.print(Warning, msg)
}
func (d *defaultSink) print(sev Severity, msg string) {
fmt.Fprint(d.writers[sev], msg)
}
func (d *defaultSink) Stringify(sev Severity, diag *Diag, args ...interface{}) (string, string) {
var prefix bytes.Buffer
if sev != Info && sev != Infoerr {
// Unless it's an ordinary stdout message, prepend the message category's prefix (error/warning).
switch sev {
case Debug:
prefix.WriteString(colors.SpecDebug)
case Error:
prefix.WriteString(colors.SpecError)
case Warning:
prefix.WriteString(colors.SpecWarning)
case Info, Infoerr:
// We'll never get here, but the linter doesn't recognize that.
default:
contract.Failf("Unrecognized diagnostic severity: %v", sev)
}
prefix.WriteString(string(sev))
prefix.WriteString(": ")
prefix.WriteString(colors.Reset)
}
// Finally, actually print the message itself.
var buffer bytes.Buffer
buffer.WriteString(colors.SpecNote)
if diag.Raw {
buffer.WriteString(diag.Message)
} else {
fmt.Fprintf(&buffer, diag.Message, args...)
}
buffer.WriteString(colors.Reset)
buffer.WriteRune('\n')
// Ensure that any sensitive data we know about is filtered out preemptively.
filtered := logging.FilterString(buffer.String())
// If colorization was requested, compile and execute the directives now.
return d.opts.Color.Colorize(prefix.String()), d.opts.Color.Colorize(filtered)
}
// syncWriter wraps an io.Writer and ensures that all writes are synchronized
// with a mutex.
type syncWriter struct {
io.Writer
mu *sync.Mutex
}
func (w *syncWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
return w.Writer.Write(p)
}
// Copyright 2016-2023, 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.
// A small library for creating consistent and documented environmental variable accesses.
//
// Public environmental variables should be declared as a module level variable.
package env
import (
"github.com/pulumi/pulumi/sdk/v3/go/common/util/env"
)
// Re-export some types and functions from the env library.
type Env = env.Env
type MapStore = env.MapStore
func NewEnv(s env.Store) env.Env { return env.NewEnv(s) }
// Global is the environment defined by environmental variables.
func Global() env.Env {
return env.NewEnv(env.Global)
}
// That Pulumi is running in experimental mode.
//
// This is our standard gate for an existing feature that's not quite ready to be stable
// and publicly consumed.
var Experimental = env.Bool("EXPERIMENTAL", "Enable experimental options and commands.")
var SkipUpdateCheck = env.Bool("SKIP_UPDATE_CHECK", "Disable checking for a new version of pulumi.")
var Dev = env.Bool("DEV", "Enable features for hacking on pulumi itself.")
var SkipCheckpoints = env.Bool("SKIP_CHECKPOINTS", "Skip saving state checkpoints and only save "+
"the final deployment. See #10668.")
var APIURL = env.String("API", "The URL to use for the Pulumi service.")
var DebugCommands = env.Bool("DEBUG_COMMANDS", "List commands helpful for debugging pulumi itself.")
var EnableLegacyDiff = env.Bool("ENABLE_LEGACY_DIFF", "")
var EnableLegacyRefreshDiff = env.Bool("ENABLE_LEGACY_REFRESH_DIFF",
"Use legacy refresh diff behaviour, in which only output changes are "+
"reported and changes against the desired state are not calculated.")
var DisableProviderPreview = env.Bool("DISABLE_PROVIDER_PREVIEW", "")
var DisableResourceReferences = env.Bool("DISABLE_RESOURCE_REFERENCES", "")
var DisableOutputValues = env.Bool("DISABLE_OUTPUT_VALUES", "")
var ErrorOutputString = env.Bool("ERROR_OUTPUT_STRING", "Throw an error instead "+
"of returning a string on attempting to convert an Output to a string")
var IgnoreAmbientPlugins = env.Bool("IGNORE_AMBIENT_PLUGINS",
"Discover additional plugins by examining $PATH.")
var DisableAutomaticPluginAcquisition = env.Bool("DISABLE_AUTOMATIC_PLUGIN_ACQUISITION",
"Disables the automatic installation of missing plugins.")
var SkipConfirmations = env.Bool("SKIP_CONFIRMATIONS",
`Whether or not confirmation prompts should be skipped. This should be used by pass any requirement
that a --yes parameter has been set for non-interactive scenarios.
This should NOT be used to bypass protections for destructive operations, such as those that will
fail without a --force parameter.`)
var DebugGRPC = env.String("DEBUG_GRPC", `Enables debug tracing of Pulumi gRPC internals.
The variable should be set to the log file to which gRPC debug traces will be sent.`)
var GitSSHPassphrase = env.String("GITSSH_PASSPHRASE",
"The passphrase to use with Git operations that use SSH.", env.Secret)
var ErrorOnDependencyCycles = env.Bool("ERROR_ON_DEPENDENCY_CYCLES",
"Whether or not to error when dependency cycles are detected.")
var SkipVersionCheck = env.Bool("AUTOMATION_API_SKIP_VERSION_CHECK",
"If set skip validating the version number reported by the CLI.")
var ContinueOnError = env.Bool("CONTINUE_ON_ERROR",
"Continue to perform the update/destroy operation despite the occurrence of errors.")
var BackendURL = env.String("BACKEND_URL",
"Set the backend that will be used instead of the currently logged in backend or the current project's backend.")
var SuppressCopilotLink = env.Bool("SUPPRESS_COPILOT_LINK",
"Suppress showing the 'explainFailure' link to Copilot in the CLI output.")
var CopilotEnabled = env.Bool("COPILOT",
"Enable Pulumi Copilot's assistance for improved CLI experience and insights.")
// TODO: This is a soft-release feature and will be removed after the feature flag is launched
// https://github.com/pulumi/pulumi/issues/19065
var CopilotSummaryModel = env.String("COPILOT_SUMMARY_MODEL",
"The LLM model to use for the Copilot summary in diagnostics. Allowed values: 'gpt-4o-mini', 'gpt-4o'.")
// TODO: This is a soft-release feature and will be removed after the feature flag is launched
// https://github.com/pulumi/pulumi/issues/19065
var CopilotSummaryMaxLen = env.Int("COPILOT_SUMMARY_MAXLEN",
"Max allowed length of Copilot summary in diagnostics. Allowed values are from 20 to 1920.")
var FallbackToStateSecretsManager = env.Bool("FALLBACK_TO_STATE_SECRETS_MANAGER",
"Use the snapshot secrets manager as a fallback when the stack configuration is missing or incomplete.")
var Parallel = env.Int("PARALLEL",
"Allow P resource operations to run in parallel at once (1 for no parallelism)")
var AccessToken = env.String("ACCESS_TOKEN",
"The access token used to authenticate with the Pulumi Service.")
var DisableSecretCache = env.Bool("DISABLE_SECRET_CACHE",
"Disable caching encryption operations for unchanged stack secrets.")
var ParallelDiff = env.Bool("PARALLEL_DIFF",
"Enable running diff calculations in parallel.")
var RunProgram = env.Bool("RUN_PROGRAM",
"Run the Pulumi program for refresh and destroy operations. This is the same as passing --run-program=true.")
// List of overrides for Plugin Download URLs. The expected format is `regexp=URL`, and multiple pairs can
// be specified separated by commas, e.g. `regexp1=URL1,regexp2=URL2`
//
// For example, when set to "^https://foo=https://bar,^github://=https://buzz", HTTPS plugin URLs that start with
// "foo" will use https://bar as the download URL and plugins hosted on github will use https://buzz
//
// Note that named regular expression groups can be used to capture parts of URLs and then reused for building
// redirects. For example
// ^github://api.github.com/(?P<org>[^/]+)/(?P<repo>[^/]+)=https://foo.com/downloads/${org}/${repo}
// will capture any GitHub-hosted plugin and redirect to its corresponding folder under https://foo.com/downloads
var PluginDownloadURLOverrides = env.String("PLUGIN_DOWNLOAD_URL_OVERRIDES", "")
// By default `pulumi preview --json` emits a "PreviewDigest" JSON object to stdout. Setting this envvar changes
// the behavior of `pulumi preview --json` to match the behavior of `pulumi up|destroy|refresh --json`, that is,
// to stream JSON events to stdout.
var EnableStreamingJSONPreview = env.Bool("ENABLE_STREAMING_JSON_PREVIEW",
"Enables streaming JSON events to stdout for preview operations when the --json flag is specified.")
// Environment variables that affect the DIY backend.
var (
DIYBackendNoLegacyWarning = env.Bool("DIY_BACKEND_NO_LEGACY_WARNING",
"Disables the warning about legacy stack files mixed with project-scoped stack files.",
env.Alternative("SELF_MANAGED_STATE_NO_LEGACY_WARNING"))
DIYBackendLegacyLayout = env.Bool("DIY_BACKEND_LEGACY_LAYOUT",
"Uses the legacy layout for new buckets, which currently default to project-scoped stacks.",
env.Alternative("SELF_MANAGED_STATE_LEGACY_LAYOUT"))
DIYBackendGzip = env.Bool("DIY_BACKEND_GZIP",
"Enables gzip compression when writing state files.",
env.Alternative("SELF_MANAGED_STATE_GZIP"))
DIYBackendRetainCheckpoints = env.Bool("DIY_BACKEND_RETAIN_CHECKPOINTS",
"If set every checkpoint will be duplicated to a timestamped file.",
env.Alternative("RETAIN_CHECKPOINTS"))
DIYBackendDisableCheckpointBackups = env.Bool("DIY_BACKEND_DISABLE_CHECKPOINT_BACKUPS",
"If set checkpoint backups will not be written the to the backup folder.",
env.Alternative("DISABLE_CHECKPOINT_BACKUPS"))
DIYBackendParallel = env.Int("DIY_BACKEND_PARALLEL",
"Number of parallel operations when fetching stacks and resources from the DIY backend.")
)
// Environment variables which affect Pulumi AI integrations
var (
AIServiceEndpoint = env.String("AI_SERVICE_ENDPOINT", "Endpoint for Pulumi AI service")
)
var DisableValidation = env.Bool(
"DISABLE_VALIDATION",
`Disables format validation of system inputs.
Currently this disables validation of the following formats:
- Stack names
This should only be used in cases where current data does not conform to the format and either cannot be migrated
without using the system itself, or show that the validation is too strict. Over time entries in the list above will be
removed and enforced to be validated.`)
var DisableRegistryResolve = env.Bool("DISABLE_REGISTRY_RESOLVE", "Use the Pulumi Registry to resolve package names")
// Copyright 2022-2024, 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 resource
import (
"fmt"
"strings"
)
type Alias struct {
URN URN
Name string
Type string
Project string
Stack string
Parent URN
NoParent bool
}
func (a *Alias) GetURN() URN {
if a.URN != "" {
return a.URN
}
return CreateURN(a.Name, a.Type, a.Parent, a.Project, a.Stack)
}
// CreateURN computes a URN from the combination of a resource name, resource type, and optional parent,
func CreateURN(name string, t string, parent URN, project string, stack string) URN {
createURN := func(parent URN, stack string, project string, t string, name string) URN {
parentString := string(parent)
var parentPrefix string
if parent == "" {
parentPrefix = "urn:pulumi:" + stack + "::" + project + "::"
} else {
ix := strings.LastIndex(parentString, "::")
if ix == -1 {
panic(fmt.Sprintf("Expected 'parent' string '%s' to contain '::'", parent))
}
parentPrefix = parentString[0:ix] + "$"
}
return URN(parentPrefix + t + "::" + name)
}
return createURN(parent, stack, project, t, name)
}
// 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 archive
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"math"
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"time"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/asset"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/sig"
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/httputil"
)
const (
// BookkeepingDir is the name of our bookkeeping folder, we store state here (like .git for git).
// Copied from workspace.BookkeepingDir to break import cycle.
BookkeepingDir = ".pulumi"
)
// Archive is a serialized archive reference. It is a union: thus, only one of its fields will be non-nil. Several
// helper routines exist as members in order to easily interact with archives of different kinds.
type Archive struct {
// Sig is the unique archive type signature (see properties.go).
Sig string `json:"4dabf18193072939515e22adb298388d" yaml:"4dabf18193072939515e22adb298388d"`
// Hash contains the SHA256 hash of the archive's contents.
Hash string `json:"hash,omitempty" yaml:"hash,omitempty"`
// Assets, when non-nil, is a collection of other assets/archives.
Assets map[string]interface{} `json:"assets,omitempty" yaml:"assets,omitempty"`
// Path is a non-empty string representing a path to a file on the current filesystem, for file archives.
Path string `json:"path,omitempty" yaml:"path,omitempty"`
// URI is a non-empty URI (file://, http://, https://, etc), for URI-backed archives.
URI string `json:"uri,omitempty" yaml:"uri,omitempty"`
}
const (
ArchiveSig = sig.ArchiveSig
ArchiveHashProperty = "hash" // the dynamic property for an archive's hash.
ArchiveAssetsProperty = "assets" // the dynamic property for an archive's assets.
ArchivePathProperty = "path" // the dynamic property for an archive's path.
ArchiveURIProperty = "uri" // the dynamic property for an archive's URI.
)
func FromAssetsWithWD(assets map[string]interface{}, wd string) (*Archive, error) {
if assets == nil {
// when provided assets are nil, create an empty archive
assets = make(map[string]interface{})
}
// Ensure all elements are either assets or archives.
for _, a := range assets {
switch t := a.(type) {
case *asset.Asset, *Archive:
// ok
default:
return &Archive{}, fmt.Errorf("type %v is not a valid archive element", t)
}
}
a := &Archive{Sig: ArchiveSig, Assets: assets}
err := a.EnsureHashWithWD(wd)
return a, err
}
func FromAssets(assets map[string]interface{}) (*Archive, error) {
wd, err := os.Getwd()
if err != nil {
return nil, err
}
return FromAssetsWithWD(assets, wd)
}
func FromPath(path string) (*Archive, error) {
wd, err := os.Getwd()
if err != nil {
return nil, err
}
return FromPathWithWD(path, wd)
}
func FromPathWithWD(path string, wd string) (*Archive, error) {
if path == "" {
return nil, errors.New("path cannot be empty when constructing a path archive")
}
a := &Archive{Sig: ArchiveSig, Path: path}
err := a.EnsureHashWithWD(wd)
return a, err
}
func FromURI(uri string) (*Archive, error) {
if uri == "" {
return nil, errors.New("uri cannot be empty when constructing a URI archive")
}
a := &Archive{Sig: ArchiveSig, URI: uri}
err := a.EnsureHash()
return a, err
}
func (a *Archive) IsAssets() bool {
if a.IsPath() || a.IsURI() {
return false
}
if len(a.Assets) > 0 {
return true
}
// We can't easily tell the difference between an Archive that really is empty and one that has no contents at all.
// If we have a hash we can check if that's the "zero hash" and if so then we know the assets is just empty. If the
// hash does not equal the empty hash then we know this is a _placeholder_ archive where the contents are just
// currently not known. If we don't have a hash then we can't tell the difference and assume it's just empty.
if a.Hash == "" || a.Hash == "5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" {
return true
}
return false
}
func (a *Archive) IsPath() bool { return a.Path != "" }
func (a *Archive) IsURI() bool { return a.URI != "" }
func (a *Archive) GetAssets() (map[string]interface{}, bool) {
if a.IsAssets() {
return a.Assets, true
}
return nil, false
}
func (a *Archive) GetPath() (string, bool) {
if a.IsPath() {
return a.Path, true
}
return "", false
}
func (a *Archive) GetURI() (string, bool) {
if a.IsURI() {
return a.URI, true
}
return "", false
}
// GetURIURL returns the underlying URI as a parsed URL, provided it is one. If there was an error parsing the URI, it
// will be returned as a non-nil error object.
func (a *Archive) GetURIURL() (*url.URL, bool, error) {
if uri, isuri := a.GetURI(); isuri {
url, err := url.Parse(uri)
if err != nil {
return nil, true, err
}
return url, true, nil
}
return nil, false, nil
}
// Equals returns true if a is value-equal to other. In this case, value equality is determined only by the hash: even
// if the contents of two archives come from different sources, they are treated as equal if their hashes match.
// Similarly, if the contents of two archives come from the same source but the archives have different hashes, the
// archives are not equal.
func (a *Archive) Equals(other *Archive) bool {
if a == nil {
return other == nil
} else if other == nil {
return false
}
// If we can't get a hash for both archives, treat them as differing.
if err := a.EnsureHash(); err != nil {
return false
}
if err := other.EnsureHash(); err != nil {
return false
}
return a.Hash == other.Hash
}
// Serialize returns a weakly typed map that contains the right signature for serialization purposes.
func (a *Archive) Serialize() map[string]interface{} {
result := map[string]interface{}{
sig.Key: ArchiveSig,
}
if a.Hash != "" {
result[ArchiveHashProperty] = a.Hash
}
if a.Assets != nil {
assets := make(map[string]interface{})
for k, v := range a.Assets {
switch t := v.(type) {
case *asset.Asset:
assets[k] = t.Serialize()
case *Archive:
assets[k] = t.Serialize()
default:
contract.Failf("Unrecognized asset map type %v", reflect.TypeOf(t))
}
}
result[ArchiveAssetsProperty] = assets
}
if a.Path != "" {
result[ArchivePathProperty] = a.Path
}
if a.URI != "" {
result[ArchiveURIProperty] = a.URI
}
return result
}
// DeserializeArchive checks to see if the map contains an archive, using its signature, and if so deserializes it.
func Deserialize(obj map[string]interface{}) (*Archive, bool, error) {
// If not an archive, return false immediately.
if obj[sig.Key] != ArchiveSig {
return &Archive{}, false, nil
}
var hash string
if v, has := obj[ArchiveHashProperty]; has {
h, ok := v.(string)
if !ok {
return &Archive{}, false, fmt.Errorf("unexpected archive hash of type %T", v)
}
hash = h
}
if path, has := obj[ArchivePathProperty]; has {
pathValue, ok := path.(string)
if !ok {
return &Archive{}, false, fmt.Errorf("unexpected archive path of type %T", path)
}
if pathValue != "" {
pathArchive := &Archive{Sig: ArchiveSig, Path: pathValue, Hash: hash}
return pathArchive, true, nil
}
}
if uri, has := obj[ArchiveURIProperty]; has {
uriValue, ok := uri.(string)
if !ok {
return &Archive{}, false, fmt.Errorf("unexpected archive URI of type %T", uri)
}
if uriValue != "" {
uriArchive := &Archive{Sig: ArchiveSig, URI: uriValue, Hash: hash}
return uriArchive, true, nil
}
}
if assetsMap, has := obj[ArchiveAssetsProperty]; has {
m, ok := assetsMap.(map[string]interface{})
if !ok {
return &Archive{}, false, fmt.Errorf("unexpected archive contents of type %T", assetsMap)
}
assets := make(map[string]interface{})
for k, elem := range m {
switch t := elem.(type) {
case *asset.Asset:
assets[k] = t
case *Archive:
assets[k] = t
case map[string]interface{}:
a, isa, err := asset.Deserialize(t)
if err != nil {
return &Archive{}, false, err
} else if isa {
assets[k] = a
} else {
arch, isarch, err := Deserialize(t)
if err != nil {
return &Archive{}, false, err
} else if !isarch {
return &Archive{}, false, fmt.Errorf("archive member '%v' is not an asset or archive", k)
}
assets[k] = arch
}
default:
return &Archive{}, false, fmt.Errorf("archive member '%v' is not an asset or archive", k)
}
}
assetArchive := &Archive{Sig: ArchiveSig, Assets: assets, Hash: hash}
return assetArchive, true, nil
}
// if we reached here, it means the archive is empty,
// we didn't find a non-zero path, non-zero uri nor non-nil assets
// we will consider this to be an empty assets archive then with zero assets
return &Archive{Sig: ArchiveSig, Assets: make(map[string]interface{}), Hash: hash}, true, nil
}
// HasContents indicates whether or not an archive's contents can be read.
func (a *Archive) HasContents() bool {
return a.IsAssets() || a.IsPath() || a.IsURI()
}
// Reader presents the contents of an archive as a stream of named blobs.
type Reader interface {
// Next returns the name and contents of the next member of the archive. If there are no more members in the
// archive, this function returns ("", nil, io.EOF). The blob returned by a call to Next() must be read in full
// before the next call to Next().
Next() (string, *asset.Blob, error)
// Close terminates the stream.
Close() error
}
// Open returns an ArchiveReader that can be used to iterate over the named blobs that comprise the archive.
func (a *Archive) Open() (Reader, error) {
wd, err := os.Getwd()
if err != nil {
return nil, err
}
return a.OpenWithWD(wd)
}
// Open returns an ArchiveReader that can be used to iterate over the named blobs that comprise the archive.
func (a *Archive) OpenWithWD(wd string) (Reader, error) {
contract.Assertf(a.HasContents(), "cannot read an archive that has no contents")
if a.IsAssets() {
return a.readAssets(wd)
} else if a.IsPath() {
return a.readPath(wd)
} else if a.IsURI() {
return a.readURI()
}
return nil, errors.New("unrecognized archive type")
}
// assetsArchiveReader is used to read an Assets archive.
type assetsArchiveReader struct {
assets map[string]interface{}
keys []string
archive Reader
archiveRoot string
wd string
}
func (r *assetsArchiveReader) Next() (string, *asset.Blob, error) {
for {
// If we're currently flattening out a subarchive, first check to see if it has any more members. If it does,
// return the next member.
if r.archive != nil {
name, blob, err := r.archive.Next()
switch {
case err == io.EOF:
// The subarchive is complete. Nil it out and continue on.
r.archive = nil
case err != nil:
// The subarchive produced a legitimate error; return it.
return "", nil, err
default:
// The subarchive produced a valid blob. Return it.
return filepath.Join(r.archiveRoot, name), blob, nil
}
}
// If there are no more members in this archive, return io.EOF.
if len(r.keys) == 0 {
return "", nil, io.EOF
}
// Fetch the next key in the archive and slice it off of the list.
name := r.keys[0]
r.keys = r.keys[1:]
switch t := r.assets[name].(type) {
case *asset.Asset:
// An asset can be produced directly.
blob, err := t.ReadWithWD(r.wd)
if err != nil {
return "", nil, fmt.Errorf("failed to expand archive asset '%v': %w", name, err)
}
return name, blob, nil
case *Archive:
// An archive must be flattened into its constituent blobs. Open the archive for reading and loop.
archive, err := t.OpenWithWD(r.wd)
if err != nil {
return "", nil, fmt.Errorf("failed to expand sub-archive '%v': %w", name, err)
}
r.archive = archive
r.archiveRoot = name
}
}
}
func (r *assetsArchiveReader) Close() error {
if r.archive != nil {
return r.archive.Close()
}
return nil
}
func (a *Archive) readAssets(wd string) (Reader, error) {
// To read a map-based archive, just produce a map from each asset to its associated reader.
m, isassets := a.GetAssets()
contract.Assertf(isassets, "Expected an asset map-based archive")
// Calculate and sort the list of member names s.t. it is deterministically orderered.
keys := slice.Prealloc[string](len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
r := &assetsArchiveReader{
assets: m,
keys: keys,
wd: wd,
}
return r, nil
}
// directoryArchiveReader is used to read an archive that is represented by a directory in the host filesystem.
type directoryArchiveReader struct {
directoryPath string
assetPaths []string
}
func (r *directoryArchiveReader) Next() (string, *asset.Blob, error) {
// If there are no more members in this archive, return io.EOF.
if len(r.assetPaths) == 0 {
return "", nil, io.EOF
}
// Fetch the next path in the archive and slice it off of the list.
assetPath := r.assetPaths[0]
r.assetPaths = r.assetPaths[1:]
// Crop the asset's path s.t. it is relative to the directory path.
name, err := filepath.Rel(r.directoryPath, assetPath)
if err != nil {
return "", nil, err
}
name = filepath.Clean(name)
// Replace Windows separators with Linux ones (ToSlash is a no-op on Linux)
name = filepath.ToSlash(name)
// Open and return the blob.
blob, err := (&asset.Asset{Path: assetPath}).Read()
if err != nil {
return "", nil, err
}
return name, blob, nil
}
func (r *directoryArchiveReader) Close() error {
return nil
}
func (a *Archive) readPath(wd string) (Reader, error) {
// To read a path-based archive, read that file and use its extension to ascertain what format to use.
path, ispath := a.GetPath()
contract.Assertf(ispath, "Expected a path-based asset")
if !filepath.IsAbs(path) {
path = filepath.Join(wd, path)
}
format := detectArchiveFormat(path)
if format == NotArchive {
// If not an archive, it could be a directory; if so, simply expand it out uncompressed as an archive.
info, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("couldn't read archive path '%v': %w", path, err)
} else if !info.IsDir() {
return nil, fmt.Errorf("'%v' is neither a recognized archive type nor a directory", path)
}
// Accumulate the list of asset paths. This list is ordered deterministically by filepath.Walk.
assetPaths := []string{}
if walkerr := filepath.Walk(path, func(filePath string, f os.FileInfo, fileerr error) error {
// If there was an error, exit.
if fileerr != nil {
return fileerr
}
// If this is a .pulumi directory, we will skip this by default.
// TODO[pulumi/pulumi#122]: when we support .pulumiignore, this will be customizable.
if f.Name() == BookkeepingDir {
if f.IsDir() {
return filepath.SkipDir
}
return nil
}
// If this was a directory, skip it.
if f.IsDir() {
return nil
}
// If this is a symlink and it points at a directory, skip it. Otherwise continue along. This will mean
// that the file will be added to the list of files to archive. When you go to read this archive, you'll
// get a copy of the file (instead of a symlink) to some other file in the archive.
if f.Mode()&os.ModeSymlink != 0 {
fileInfo, statErr := os.Stat(filePath)
if statErr != nil {
return statErr
}
if fileInfo.IsDir() {
return nil
}
}
// Otherwise, add this asset to the list of paths and keep going.
assetPaths = append(assetPaths, filePath)
return nil
}); walkerr != nil {
return nil, walkerr
}
r := &directoryArchiveReader{
directoryPath: path,
assetPaths: assetPaths,
}
return r, nil
}
// Otherwise, it's an archive file, and we will go ahead and open it up and read it.
file, err := os.Open(path)
if err != nil {
return nil, err
}
return readArchive(file, format)
}
func (a *Archive) readURI() (Reader, error) {
// To read a URI-based archive, fetch the contents remotely and use the extension to pick the format to use.
url, isURL, err := a.GetURIURL()
if err != nil {
return nil, err
}
contract.Assertf(isURL, "Expected a URI-based asset")
format := detectArchiveFormat(url.Path)
if format == NotArchive {
// IDEA: support (a) hints and (b) custom providers that default to certain formats.
return nil, fmt.Errorf("file at URL '%v' is not a recognized archive format", url)
}
ar, err := a.openURLStream(url)
if err != nil {
return nil, err
}
return readArchive(ar, format)
}
func (a *Archive) openURLStream(url *url.URL) (io.ReadCloser, error) {
switch s := url.Scheme; s {
case "http", "https":
resp, err := httputil.GetWithRetry(url.String(), http.DefaultClient)
if err != nil {
return nil, err
}
return resp.Body, nil
case "file":
contract.Assertf(url.Host == "", "file:// URIs cannot have a host: %v", url)
contract.Assertf(url.User == nil, "file:// URIs cannot have a user: %v", url)
contract.Assertf(url.RawQuery == "", "file:// URIs cannot have a query string: %v", url)
contract.Assertf(url.Fragment == "", "file:// URIs cannot have a fragment: %v", url)
return os.Open(url.Path)
default:
return nil, fmt.Errorf("Unrecognized or unsupported URI scheme: %v", s)
}
}
// Bytes fetches the archive contents as a byte slices. This is almost certainly the least efficient way to deal with
// the underlying streaming capabilities offered by assets and archives, but can be used in a pinch to interact with
// APIs that demand []bytes.
func (a *Archive) Bytes(format Format) ([]byte, error) {
var data bytes.Buffer
if err := a.Archive(format, &data); err != nil {
return nil, err
}
return data.Bytes(), nil
}
// Archive produces a single archive stream in the desired format. It prefers to return the archive with as little
// copying as is feasible, however if the desired format is different from the source, it will need to translate.
func (a *Archive) Archive(format Format, w io.Writer) error {
wd, err := os.Getwd()
if err != nil {
return err
}
return a.ArchiveWithWD(format, w, wd)
}
// Archive produces a single archive stream in the desired format. It prefers to return the archive with as little
// copying as is feasible, however if the desired format is different from the source, it will need to translate.
func (a *Archive) ArchiveWithWD(format Format, w io.Writer, wd string) error {
// If the source format is the same, just return that.
if sf, ss, err := a.ReadSourceArchiveWithWD(wd); sf != NotArchive && sf == format {
if err != nil {
return err
}
_, err := io.Copy(w, ss)
return err
}
switch format {
case TarArchive:
return a.archiveTar(w, wd)
case TarGZIPArchive:
return a.archiveTarGZIP(w, wd)
case ZIPArchive:
return a.archiveZIP(w, wd)
default:
contract.Failf("Illegal archive type: %v", format)
return nil
}
}
// addNextFileToTar adds the next file in the given archive to the given tar file. Returns io.EOF if the archive
// contains no more files.
func addNextFileToTar(r Reader, tw *tar.Writer, seenFiles map[string]bool) error {
file, data, err := r.Next()
if err != nil {
return err
}
defer contract.IgnoreClose(data)
// It's possible to run into the same file multiple times in the list of archives we're passed.
// For example, if there is an archive pointing to foo/bar and an archive pointing to
// foo/bar/baz/quux. Because of this only include the file the first time we see it.
if _, has := seenFiles[file]; has {
return nil
}
seenFiles[file] = true
sz := data.Size()
if err = tw.WriteHeader(&tar.Header{
Name: file,
Mode: 0o600,
Size: sz,
}); err != nil {
return err
}
n, err := io.Copy(tw, data)
if err == tar.ErrWriteTooLong {
return fmt.Errorf("incorrect blob size for %v: expected %v, got %v: %w", file, sz, n, err)
}
// tar expect us to write all the bytes we said we would write. If copy doesn't write Size bytes to the
// tar file then we'll get a "missed writing X bytes" error on the next call to WriteHeader or Flush.
if n != sz {
return fmt.Errorf("incorrect blob size for %v: expected %v, got %v", file, sz, n)
}
return err
}
func (a *Archive) archiveTar(w io.Writer, wd string) error {
// Open the archive.
reader, err := a.OpenWithWD(wd)
if err != nil {
return err
}
defer contract.IgnoreClose(reader)
// Now actually emit the contents, file by file.
tw := tar.NewWriter(w)
seenFiles := make(map[string]bool)
for err == nil {
err = addNextFileToTar(reader, tw, seenFiles)
}
if err != io.EOF {
return err
}
return tw.Close()
}
func (a *Archive) archiveTarGZIP(w io.Writer, wd string) error {
z := gzip.NewWriter(w)
return a.archiveTar(z, wd)
}
// addNextFileToZIP adds the next file in the given archive to the given ZIP file. Returns io.EOF if the archive
// contains no more files.
func addNextFileToZIP(r Reader, zw *zip.Writer, seenFiles map[string]bool) error {
file, data, err := r.Next()
if err != nil {
return err
}
defer contract.IgnoreClose(data)
// It's possible to run into the same file multiple times in the list of archives we're passed.
// For example, if there is an archive pointing to foo/bar and an archive pointing to
// foo/bar/baz/quux. Because of this only include the file the first time we see it.
if _, has := seenFiles[file]; has {
return nil
}
seenFiles[file] = true
sz := data.Size()
fh := &zip.FileHeader{
// These are the two fields set by zw.Create()
Name: file,
Method: zip.Deflate,
}
// Set a nonzero -- but constant -- modification time. Otherwise, some agents (e.g. Azure
// websites) can't extract the resulting archive. The date is comfortably after 1980 because
// the ZIP format includes a date representation that starts at 1980.
fh.Modified = time.Date(1990, time.January, 1, 0, 0, 0, 0, time.UTC)
fw, err := zw.CreateHeader(fh)
if err != nil {
return err
}
n, err := io.Copy(fw, data)
if err != nil {
return err
}
if n != sz {
return fmt.Errorf("incorrect blob size for %v: expected %v, got %v", file, sz, n)
}
return nil
}
func (a *Archive) archiveZIP(w io.Writer, wd string) error {
// Open the archive.
reader, err := a.OpenWithWD(wd)
if err != nil {
return err
}
defer contract.IgnoreClose(reader)
// Now actually emit the contents, file by file.
zw := zip.NewWriter(w)
seenFiles := make(map[string]bool)
for err == nil {
err = addNextFileToZIP(reader, zw, seenFiles)
}
if err != io.EOF {
return err
}
return zw.Close()
}
// ReadSourceArchive returns a stream to the underlying archive, if there is one.
func (a *Archive) ReadSourceArchive() (Format, io.ReadCloser, error) {
wd, err := os.Getwd()
if err != nil {
return NotArchive, nil, err
}
return a.ReadSourceArchiveWithWD(wd)
}
// ReadSourceArchiveWithWD returns a stream to the underlying archive, if there is one.
func (a *Archive) ReadSourceArchiveWithWD(wd string) (Format, io.ReadCloser, error) {
if path, ispath := a.GetPath(); ispath {
if !filepath.IsAbs(path) {
path = filepath.Join(wd, path)
}
if format := detectArchiveFormat(path); format != NotArchive {
f, err := os.Open(path)
return format, f, err
}
} else if url, isurl, urlerr := a.GetURIURL(); urlerr == nil && isurl {
if format := detectArchiveFormat(url.Path); format != NotArchive {
s, err := a.openURLStream(url)
return format, s, err
}
}
return NotArchive, nil, nil
}
// EnsureHash computes the SHA256 hash of the archive's contents and stores it on the object.
func (a *Archive) EnsureHash() error {
wd, err := os.Getwd()
if err != nil {
return err
}
return a.EnsureHashWithWD(wd)
}
// EnsureHash computes the SHA256 hash of the archive's contents and stores it on the object.
func (a *Archive) EnsureHashWithWD(wd string) error {
contract.Requiref(wd != "", "wd", "must not be empty")
if a.Hash == "" {
hash := sha256.New()
// Attempt to compute the hash in the most efficient way. First try to open the archive directly and copy it
// to the hash. This avoids traversing any of the contents and just treats it as a byte stream.
f, r, err := a.ReadSourceArchiveWithWD(wd)
if err != nil {
return err
}
if f != NotArchive && r != nil {
defer contract.IgnoreClose(r)
_, err = io.Copy(hash, r)
if err != nil {
return err
}
} else {
// Otherwise, it's not an archive; we'll need to transform it into one. Pick tar since it avoids
// any superfluous compression which doesn't actually help us in this situation.
err := a.ArchiveWithWD(TarArchive, hash, wd)
if err != nil {
return err
}
}
// Finally, encode the resulting hash as a string and we're done.
a.Hash = hex.EncodeToString(hash.Sum(nil))
}
return nil
}
// Format indicates what archive and/or compression format an archive uses.
type Format int
const (
NotArchive = iota // not an archive.
TarArchive // a POSIX tar archive.
TarGZIPArchive // a POSIX tar archive that has been subsequently compressed using GZip.
ZIPArchive // a multi-file ZIP archive.
JARArchive // a Java JAR file
)
// ArchiveExts maps from a file extension and its associated archive and/or compression format.
var ArchiveExts = map[string]Format{
".tar": TarArchive,
".tgz": TarGZIPArchive,
".tar.gz": TarGZIPArchive,
".zip": ZIPArchive,
".jar": JARArchive,
}
// detectArchiveFormat takes a path and infers its archive format based on the file extension.
func detectArchiveFormat(path string) Format {
for ext, typ := range ArchiveExts {
if strings.HasSuffix(path, ext) {
return typ
}
}
return NotArchive
}
// readArchive takes a stream to an existing archive and returns a map of names to readers for the inner assets.
// The routine returns an error if something goes wrong and, no matter what, closes the stream before returning.
func readArchive(ar io.ReadCloser, format Format) (Reader, error) {
switch format {
case TarArchive:
return readTarArchive(ar)
case TarGZIPArchive:
return readTarGZIPArchive(ar)
case ZIPArchive, JARArchive:
// Unfortunately, the ZIP archive reader requires ReaderAt functionality. If it's a file, we can recover this
// with a simple stat. Otherwise, we will need to go ahead and make a copy in memory.
var ra io.ReaderAt
var sz int64
if f, isf := ar.(*os.File); isf {
stat, err := f.Stat()
if err != nil {
return nil, err
}
ra = f
sz = stat.Size()
} else if data, err := io.ReadAll(ar); err != nil {
return nil, err
} else {
ra = bytes.NewReader(data)
sz = int64(len(data))
}
return readZIPArchive(ra, sz)
default:
contract.Failf("Illegal archive type: %v", format)
return nil, nil
}
}
// tarArchiveReader is used to read an archive that is stored in tar format.
type tarArchiveReader struct {
ar io.ReadCloser
tr *tar.Reader
}
func (r *tarArchiveReader) Next() (string, *asset.Blob, error) {
for {
file, err := r.tr.Next()
if err != nil {
return "", nil, err
}
switch file.Typeflag {
case tar.TypeDir:
continue // skip directories
case tar.TypeReg:
// Return the tar reader for this file's contents.
data := asset.NewRawBlob(io.NopCloser(r.tr), file.Size)
name := filepath.Clean(file.Name)
return name, data, nil
default:
contract.Failf("Unrecognized tar header typeflag: %v", file.Typeflag)
}
}
}
func (r *tarArchiveReader) Close() error {
return r.ar.Close()
}
func readTarArchive(ar io.ReadCloser) (Reader, error) {
r := &tarArchiveReader{
ar: ar,
tr: tar.NewReader(ar),
}
return r, nil
}
func readTarGZIPArchive(ar io.ReadCloser) (Reader, error) {
// First decompress the GZIP stream.
gz, err := gzip.NewReader(ar)
if err != nil {
return nil, err
}
// Now read the tarfile.
return readTarArchive(gz)
}
// zipArchiveReader is used to read an archive that is stored in ZIP format.
type zipArchiveReader struct {
ar io.ReaderAt
zr *zip.Reader
index int
}
func (r *zipArchiveReader) Next() (string, *asset.Blob, error) {
for r.index < len(r.zr.File) {
file := r.zr.File[r.index]
r.index++
// Skip directories, since they aren't included in TAR and other archives above.
if file.FileInfo().IsDir() {
continue
}
// Open the next file and return its blob.
body, err := file.Open()
if err != nil {
return "", nil, fmt.Errorf("failed to read ZIP inner file %v: %w", file.Name, err)
}
if file.UncompressedSize64 > math.MaxInt64 {
return "", nil, fmt.Errorf("file %v is too large to read", file.Name)
}
//nolint:gosec // uint64 -> int64 overflow is checked above.
blob := asset.NewRawBlob(body, int64(file.UncompressedSize64))
name := filepath.Clean(file.Name)
return name, blob, nil
}
return "", nil, io.EOF
}
func (r *zipArchiveReader) Close() error {
if c, ok := r.ar.(io.Closer); ok {
return c.Close()
}
return nil
}
func readZIPArchive(ar io.ReaderAt, size int64) (Reader, error) {
zr, err := zip.NewReader(ar, size)
if err != nil {
return nil, fmt.Errorf("failed to read ZIP: %w", err)
}
r := &zipArchiveReader{
ar: ar,
zr: zr,
}
return r, nil
}
// 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 resource
// The contents of this file have been moved. The logic behind assets and archives now
// lives in "github.com/pulumi/pulumi/sdk/v3/go/common/resource/asset" and
// "github.com/pulumi/pulumi/sdk/v3/go/common/resource/archive", respectively. This file
// exists to fulfill backwards-compatibility requirements. No new declarations should be
// added here.
import (
"io"
"os"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/archive"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/asset"
)
const (
// BookkeepingDir is the name of our bookkeeping folder, we store state here (like .git for git).
// Copied from workspace.BookkeepingDir to break import cycle.
BookkeepingDir = ".pulumi"
)
type (
Asset = asset.Asset
Blob = asset.Blob
Archive = archive.Archive
ArchiveFormat = archive.Format
Reader = archive.Reader
)
const (
AssetSig = asset.AssetSig
AssetHashProperty = asset.AssetHashProperty
AssetTextProperty = asset.AssetTextProperty
AssetPathProperty = asset.AssetPathProperty
AssetURIProperty = asset.AssetURIProperty
ArchiveSig = archive.ArchiveSig
ArchiveHashProperty = archive.ArchiveHashProperty // the dynamic property for an archive's hash.
ArchiveAssetsProperty = archive.ArchiveAssetsProperty // the dynamic property for an archive's assets.
ArchivePathProperty = archive.ArchivePathProperty // the dynamic property for an archive's path.
ArchiveURIProperty = archive.ArchiveURIProperty // the dynamic property for an archive's URI.
)
// NewTextAsset produces a new asset and its corresponding SHA256 hash from the given text.
func NewTextAsset(text string) (*Asset, error) { return asset.FromText(text) }
// NewPathAsset produces a new asset and its corresponding SHA256 hash from the given filesystem path.
func NewPathAsset(path string) (*Asset, error) { return asset.FromPath(path) }
// NewPathAsset produces a new asset and its corresponding SHA256 hash from the given filesystem path.
func NewPathAssetWithWD(path string, cwd string) (*Asset, error) {
return asset.FromPathWithWD(path, cwd)
}
// NewURIAsset produces a new asset and its corresponding SHA256 hash from the given network URI.
func NewURIAsset(uri string) (*Asset, error) { return asset.FromURI(uri) }
// DeserializeAsset checks to see if the map contains an asset, using its signature, and if so deserializes it.
func DeserializeAsset(obj map[string]interface{}) (*Asset, bool, error) {
return asset.Deserialize(obj)
}
// NewByteBlob creates a new byte blob.
func NewByteBlob(data []byte) *Blob { return asset.NewByteBlob(data) }
// NewFileBlob creates a new asset blob whose size is known thanks to stat.
func NewFileBlob(f *os.File) (*Blob, error) { return asset.NewFileBlob(f) }
// NewReadCloserBlob turn any old ReadCloser into an Blob, usually by making a copy.
func NewReadCloserBlob(r io.ReadCloser) (*Blob, error) { return asset.NewReadCloserBlob(r) }
func NewAssetArchive(assets map[string]interface{}) (*Archive, error) {
return archive.FromAssets(assets)
}
func NewAssetArchiveWithWD(assets map[string]interface{}, wd string) (*Archive, error) {
return archive.FromAssetsWithWD(assets, wd)
}
func NewPathArchive(path string) (*Archive, error) {
return archive.FromPath(path)
}
func NewPathArchiveWithWD(path string, wd string) (*Archive, error) {
return archive.FromPathWithWD(path, wd)
}
func NewURIArchive(uri string) (*Archive, error) {
return archive.FromURI(uri)
}
// DeserializeArchive checks to see if the map contains an archive, using its signature, and if so deserializes it.
func DeserializeArchive(obj map[string]interface{}) (*Archive, bool, error) {
return archive.Deserialize(obj)
}
const (
NotArchive = archive.NotArchive // not an archive.
TarArchive = archive.TarArchive // a POSIX tar archive.
// a POSIX tar archive that has been subsequently compressed using GZip.
TarGZIPArchive = archive.TarGZIPArchive
ZIPArchive = archive.ZIPArchive // a multi-file ZIP archive.
JARArchive = archive.JARArchive // a Java JAR file
)
// ArchiveExts maps from a file extension and its associated archive and/or compression format.
var ArchiveExts = archive.ArchiveExts
// 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 asset
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/sig"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/httputil"
)
// Asset is a serialized asset reference. It is a union: thus, at most one of its fields will be non-nil. Several helper
// routines exist as members in order to easily interact with the assets referenced by an instance of this type.
type Asset struct {
// Sig is the unique asset type signature (see properties.go).
Sig string `json:"4dabf18193072939515e22adb298388d" yaml:"4dabf18193072939515e22adb298388d"`
// Hash is the SHA256 hash of the asset's contents.
Hash string `json:"hash,omitempty" yaml:"hash,omitempty"`
// Text is set to a non-empty value for textual assets.
Text string `json:"text,omitempty" yaml:"text,omitempty"`
// Path will contain a non-empty path to the file on the current filesystem for file assets.
Path string `json:"path,omitempty" yaml:"path,omitempty"`
// URI will contain a non-empty URI (file://, http://, https://, or custom) for URI-backed assets.
URI string `json:"uri,omitempty" yaml:"uri,omitempty"`
}
const (
AssetSig = sig.AssetSig
AssetHashProperty = "hash" // the dynamic property for an asset's hash.
AssetTextProperty = "text" // the dynamic property for an asset's text.
AssetPathProperty = "path" // the dynamic property for an asset's path.
AssetURIProperty = "uri" // the dynamic property for an asset's URI.
)
// FromText produces a new asset and its corresponding SHA256 hash from the given text.
func FromText(text string) (*Asset, error) {
a := &Asset{Sig: AssetSig, Text: text}
// Special case the empty string otherwise EnsureHash will fail.
if text == "" {
a.Hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
err := a.EnsureHash()
return a, err
}
// FromPath produces a new asset and its corresponding SHA256 hash from the given filesystem path.
func FromPath(path string) (*Asset, error) {
a := &Asset{Sig: AssetSig, Path: path}
err := a.EnsureHash()
return a, err
}
// FromPathWithWD produces a new asset and its corresponding SHA256 hash from the given filesystem path.
func FromPathWithWD(path string, wd string) (*Asset, error) {
a := &Asset{Sig: AssetSig, Path: path}
err := a.EnsureHashWithWD(wd)
return a, err
}
// FromURI produces a new asset and its corresponding SHA256 hash from the given network URI.
func FromURI(uri string) (*Asset, error) {
a := &Asset{Sig: AssetSig, URI: uri}
err := a.EnsureHash()
return a, err
}
func (a *Asset) IsText() bool {
if a.IsPath() || a.IsURI() {
return false
}
if a.Text != "" {
return true
}
// We can't easily tell the difference between an Asset that really has the empty string as its text and one that
// has no text at all. If we have a hash we can check if that's the "zero hash" and if so then we know the text is
// just empty. If the hash does not equal the empty hash then we know this is a _placeholder_ asset where the text is
// just currently not known. If we don't have a hash then we can't tell the difference and assume it's just empty.
if a.Hash == "" || a.Hash == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
return true
}
return false
}
func (a *Asset) IsPath() bool { return a.Path != "" }
func (a *Asset) IsURI() bool { return a.URI != "" }
func (a *Asset) GetText() (string, bool) {
if a.IsText() {
return a.Text, true
}
return "", false
}
func (a *Asset) GetPath() (string, bool) {
if a.IsPath() {
return a.Path, true
}
return "", false
}
func (a *Asset) GetURI() (string, bool) {
if a.IsURI() {
return a.URI, true
}
return "", false
}
// GetURIURL returns the underlying URI as a parsed URL, provided it is one. If there was an error parsing the URI, it
// will be returned as a non-nil error object.
func (a *Asset) GetURIURL() (*url.URL, bool, error) {
if uri, isuri := a.GetURI(); isuri {
url, err := url.Parse(uri)
if err != nil {
return nil, true, err
}
return url, true, nil
}
return nil, false, nil
}
// Equals returns true if a is value-equal to other. In this case, value equality is determined only by the hash: even
// if the contents of two assets come from different sources, they are treated as equal if their hashes match.
// Similarly, if the contents of two assets come from the same source but the assets have different hashes, the assets
// are not equal.
func (a *Asset) Equals(other *Asset) bool {
if a == nil {
return other == nil
} else if other == nil {
return false
}
// If we can't get a hash for both assets, treat them as differing.
if err := a.EnsureHash(); err != nil {
return false
}
if err := other.EnsureHash(); err != nil {
return false
}
return a.Hash == other.Hash
}
// Serialize returns a weakly typed map that contains the right signature for serialization purposes.
func (a *Asset) Serialize() map[string]interface{} {
result := map[string]interface{}{
sig.Key: AssetSig,
}
if a.Hash != "" {
result[AssetHashProperty] = a.Hash
}
if a.Text != "" {
result[AssetTextProperty] = a.Text
}
if a.Path != "" {
result[AssetPathProperty] = a.Path
}
if a.URI != "" {
result[AssetURIProperty] = a.URI
}
return result
}
// DeserializeAsset checks to see if the map contains an asset, using its signature, and if so deserializes it.
func Deserialize(obj map[string]interface{}) (*Asset, bool, error) {
// If not an asset, return false immediately.
if obj[sig.Key] != AssetSig {
return &Asset{}, false, nil
}
// Else, deserialize the possible fields.
var hash string
if v, has := obj[AssetHashProperty]; has {
h, ok := v.(string)
if !ok {
return &Asset{}, false, fmt.Errorf("unexpected asset hash of type %T", v)
}
hash = h
}
var text string
if v, has := obj[AssetTextProperty]; has {
t, ok := v.(string)
if !ok {
return &Asset{}, false, fmt.Errorf("unexpected asset text of type %T", v)
}
text = t
}
var path string
if v, has := obj[AssetPathProperty]; has {
p, ok := v.(string)
if !ok {
return &Asset{}, false, fmt.Errorf("unexpected asset path of type %T", v)
}
path = p
}
var uri string
if v, has := obj[AssetURIProperty]; has {
u, ok := v.(string)
if !ok {
return &Asset{}, false, fmt.Errorf("unexpected asset URI of type %T", v)
}
uri = u
}
return &Asset{Sig: AssetSig, Hash: hash, Text: text, Path: path, URI: uri}, true, nil
}
// HasContents indicates whether or not an asset's contents can be read.
func (a *Asset) HasContents() bool {
return a.IsText() || a.IsPath() || a.IsURI()
}
// Bytes returns the contents of the asset as a byte slice.
func (a *Asset) Bytes() ([]byte, error) {
// If this is a text asset, just return its bytes directly.
if text, istext := a.GetText(); istext {
return []byte(text), nil
}
blob, err := a.Read()
if err != nil {
return nil, err
}
return io.ReadAll(blob)
}
// Read begins reading an asset.
func (a *Asset) Read() (*Blob, error) {
wd, err := os.Getwd()
if err != nil {
return nil, err
}
return a.ReadWithWD(wd)
}
// ReadWithWD begins reading an asset.
func (a *Asset) ReadWithWD(wd string) (*Blob, error) {
if a.IsText() {
return a.readText()
} else if a.IsPath() {
return a.readPath(wd)
} else if a.IsURI() {
return a.readURI()
}
return nil, errors.New("unrecognized asset type")
}
func (a *Asset) readText() (*Blob, error) {
text, istext := a.GetText()
contract.Assertf(istext, "Expected a text-based asset")
return NewByteBlob([]byte(text)), nil
}
func (a *Asset) readPath(wd string) (*Blob, error) {
path, ispath := a.GetPath()
contract.Assertf(ispath, "Expected a path-based asset")
if !filepath.IsAbs(path) {
path = filepath.Join(wd, path)
}
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to open asset file '%v': %w", path, err)
}
// Do a quick check to make sure it's a file, so we can fail gracefully if someone passes a directory.
info, err := file.Stat()
if err != nil {
contract.IgnoreClose(file)
return nil, fmt.Errorf("failed to stat asset file '%v': %w", path, err)
}
if info.IsDir() {
contract.IgnoreClose(file)
return nil, fmt.Errorf("asset path '%v' is a directory; try using an archive", path)
}
blob := &Blob{
rd: file,
sz: info.Size(),
}
return blob, nil
}
func (a *Asset) readURI() (*Blob, error) {
url, isURL, err := a.GetURIURL()
if err != nil {
return nil, err
}
contract.Assertf(isURL, "Expected a URI-based asset")
switch s := url.Scheme; s {
case "http", "https":
resp, err := httputil.GetWithRetry(url.String(), http.DefaultClient)
if err != nil {
return nil, err
}
return NewReadCloserBlob(resp.Body)
case "file":
contract.Assertf(url.User == nil, "file:// URIs cannot have a user: %v", url)
contract.Assertf(url.RawQuery == "", "file:// URIs cannot have a query string: %v", url)
contract.Assertf(url.Fragment == "", "file:// URIs cannot have a fragment: %v", url)
if url.Host != "" && url.Host != "localhost" {
return nil, fmt.Errorf("file:// host '%v' not supported (only localhost)", url.Host)
}
f, err := os.Open(url.Path)
if err != nil {
return nil, err
}
return NewFileBlob(f)
default:
return nil, fmt.Errorf("Unrecognized or unsupported URI scheme: %v", s)
}
}
// EnsureHash computes the SHA256 hash of the asset's contents and stores it on the object.
func (a *Asset) EnsureHash() error {
if a.Hash == "" {
blob, err := a.Read()
if err != nil {
return err
}
defer contract.IgnoreClose(blob)
hash := sha256.New()
n, err := io.Copy(hash, blob)
if err != nil {
return err
}
if n != blob.Size() {
return fmt.Errorf("incorrect blob size: expected %v, got %v", blob.Size(), n)
}
a.Hash = hex.EncodeToString(hash.Sum(nil))
}
return nil
}
// EnsureHash computes the SHA256 hash of the asset's contents and stores it on the object.
func (a *Asset) EnsureHashWithWD(wd string) error {
if a.Hash == "" {
blob, err := a.ReadWithWD(wd)
if err != nil {
return err
}
defer contract.IgnoreClose(blob)
hash := sha256.New()
n, err := io.Copy(hash, blob)
if err != nil {
return err
}
if n != blob.Size() {
return fmt.Errorf("incorrect blob size: expected %v, got %v", blob.Size(), n)
}
a.Hash = hex.EncodeToString(hash.Sum(nil))
}
return nil
}
// Blob is a blob that implements ReadCloser and offers Len functionality.
type Blob struct {
rd io.ReadCloser // an underlying reader.
sz int64 // the size of the blob.
}
func (blob *Blob) Close() error { return blob.rd.Close() }
func (blob *Blob) Read(p []byte) (int, error) { return blob.rd.Read(p) }
func (blob *Blob) Size() int64 { return blob.sz }
// NewByteBlob creates a new byte blob.
func NewByteBlob(data []byte) *Blob {
return &Blob{
rd: io.NopCloser(bytes.NewReader(data)),
sz: int64(len(data)),
}
}
// NewFileBlob creates a new asset blob whose size is known thanks to stat.
func NewFileBlob(f *os.File) (*Blob, error) {
stat, err := f.Stat()
if err != nil {
return nil, err
}
return &Blob{
rd: f,
sz: stat.Size(),
}, nil
}
// NewReadCloserBlob turn any old ReadCloser into an Blob, usually by making a copy.
func NewReadCloserBlob(r io.ReadCloser) (*Blob, error) {
if f, isf := r.(*os.File); isf {
// If it's a file, we can "fast path" the asset creation without making a copy.
return NewFileBlob(f)
}
// Otherwise, read it all in, and create a blob out of that.
defer contract.IgnoreClose(r)
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
return NewByteBlob(data), nil
}
func NewRawBlob(r io.ReadCloser, size int64) *Blob {
return &Blob{rd: r, sz: size}
}
// Copyright 2021 Google LLC
//
// 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 config
import (
"context"
"encoding/json"
)
func FuzzConfig(data []byte) int {
if len(data) != 32 {
return -1
}
crypter := NewSymmetricCrypter(make([]byte, 32))
_, _ = crypter.EncryptValue(context.Background(), string(data))
_, _ = crypter.DecryptValue(context.Background(), string(data))
return 1
}
func fuuzRoundtripKey(m Key, marshal func(v interface{}) ([]byte, error),
unmarshal func([]byte, interface{}) error) (Key, error) {
b, err := marshal(m)
if err != nil {
return Key{}, err
}
var newM Key
err = unmarshal(b, &newM)
return newM, err
}
func FuzzParseKey(data []byte) int {
k, err := ParseKey(string(data))
if err != nil {
return 0
}
fuuzRoundtripKey(k, json.Marshal, json.Unmarshal)
return 1
}
// Copyright 2016-2022, 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 config
import (
"context"
"crypto/aes"
"crypto/cipher"
cryptorand "crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"strings"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"golang.org/x/crypto/pbkdf2"
)
// Encrypter encrypts plaintext into its encrypted ciphertext.
type Encrypter interface {
EncryptValue(ctx context.Context, plaintext string) (string, error)
// BatchEncrypt supports encryption of multiple secrets in a single batch request, if supported by the implementation.
// Returns a list of encrypted values in the same order as the input secrets.
// Each secret is encrypted individually and duplicate secret values will result in different ciphertexts.
BatchEncrypt(ctx context.Context, secrets []string) ([]string, error)
}
// Decrypter decrypts encrypted ciphertext to its plaintext representation.
type Decrypter interface {
DecryptValue(ctx context.Context, ciphertext string) (string, error)
// BatchDecrypt supports decryption of secrets in a single batch round-trip, where supported.
// Returns a list of decrypted values in the same order as the input ciphertexts.
BatchDecrypt(ctx context.Context, ciphertexts []string) ([]string, error)
}
// Crypter can both encrypt and decrypt values.
type Crypter interface {
Encrypter
Decrypter
}
// A nopCrypter simply returns the ciphertext as-is.
type nopCrypter struct{}
var (
NopDecrypter Decrypter = nopCrypter{}
NopEncrypter Encrypter = nopCrypter{}
)
func (nopCrypter) DecryptValue(ctx context.Context, ciphertext string) (string, error) {
return ciphertext, nil
}
func (nopCrypter) BatchDecrypt(ctx context.Context, ciphertexts []string) ([]string, error) {
return DefaultBatchDecrypt(ctx, NopDecrypter, ciphertexts)
}
func (nopCrypter) EncryptValue(ctx context.Context, plaintext string) (string, error) {
return plaintext, nil
}
func (nopCrypter) BatchEncrypt(ctx context.Context, secrets []string) ([]string, error) {
return DefaultBatchEncrypt(ctx, NopEncrypter, secrets)
}
// BlindingCrypter returns a Crypter that instead of decrypting or encrypting data, just returns "[secret]", it can
// be used when you want to display configuration information to a user but don't want to prompt for a password
// so secrets will not be decrypted or encrypted.
var BlindingCrypter Crypter = blindingCrypter{}
// NewBlindingDecrypter returns a blinding decrypter.
func NewBlindingDecrypter() Decrypter {
return blindingCrypter{}
}
type blindingCrypter struct{}
func (b blindingCrypter) DecryptValue(ctx context.Context, _ string) (string, error) {
return "[secret]", nil
}
func (b blindingCrypter) EncryptValue(ctx context.Context, plaintext string) (string, error) {
return "[secret]", nil
}
func (b blindingCrypter) BatchEncrypt(ctx context.Context, secrets []string) ([]string, error) {
return DefaultBatchEncrypt(ctx, b, secrets)
}
func (b blindingCrypter) BatchDecrypt(ctx context.Context, ciphertexts []string) ([]string, error) {
return DefaultBatchDecrypt(ctx, b, ciphertexts)
}
// NewPanicCrypter returns a new config crypter that will panic if used.
func NewPanicCrypter() Crypter {
return &panicCrypter{}
}
type panicCrypter struct{}
func (p panicCrypter) EncryptValue(ctx context.Context, _ string) (string, error) {
panic("attempt to encrypt value")
}
func (p panicCrypter) BatchEncrypt(ctx context.Context, _ []string) ([]string, error) {
panic("attempt to batch encrypt values")
}
func (p panicCrypter) DecryptValue(ctx context.Context, _ string) (string, error) {
panic("attempt to decrypt value")
}
func (p panicCrypter) BatchDecrypt(ctx context.Context, ciphertexts []string) ([]string, error) {
panic("attempt to batch decrypt values")
}
type errorCrypter struct {
err string
}
func NewErrorCrypter(err string) Crypter {
return &errorCrypter{err}
}
func (e errorCrypter) EncryptValue(ctx context.Context, _ string) (string, error) {
return "", fmt.Errorf("failed to encrypt: %s", e.err)
}
func (e errorCrypter) BatchEncrypt(ctx context.Context, _ []string) ([]string, error) {
return nil, fmt.Errorf("failed to batch encrypt: %s", e.err)
}
func (e errorCrypter) DecryptValue(ctx context.Context, _ string) (string, error) {
return "", fmt.Errorf("failed to decrypt: %s", e.err)
}
func (e errorCrypter) BatchDecrypt(ctx context.Context, _ []string) ([]string, error) {
return nil, fmt.Errorf("failed to batch decrypt: %s", e.err)
}
// NewSymmetricCrypter creates a crypter that encrypts and decrypts values using AES-256-GCM. The nonce is stored with
// the value itself as a pair of base64 values separated by a colon and a version tag `v1` is prepended.
func NewSymmetricCrypter(key []byte) Crypter {
contract.Requiref(len(key) == SymmetricCrypterKeyBytes, "key", "AES-256-GCM needs a 32 byte key")
return &symmetricCrypter{key}
}
// NewSymmetricCrypterFromPassphrase uses a passphrase and salt to generate a key, and then returns a crypter using it.
func NewSymmetricCrypterFromPassphrase(phrase string, salt []byte) Crypter {
// Generate a key using PBKDF2 to slow down attempts to crack it. 1,000,000 iterations was chosen because it
// took a little over a second on an i7-7700HQ Quad Core processor
key := pbkdf2.Key([]byte(phrase), salt, 1000000, SymmetricCrypterKeyBytes, sha256.New)
return NewSymmetricCrypter(key)
}
// SymmetricCrypterKeyBytes is the required key size in bytes.
const SymmetricCrypterKeyBytes = 32
type symmetricCrypter struct {
key []byte
}
func (s symmetricCrypter) EncryptValue(ctx context.Context, value string) (string, error) {
secret, nonce := encryptAES256GCGM(value, s.key)
return fmt.Sprintf("v1:%s:%s",
base64.StdEncoding.EncodeToString(nonce), base64.StdEncoding.EncodeToString(secret)), nil
}
func (s symmetricCrypter) BatchEncrypt(ctx context.Context, secrets []string) ([]string, error) {
return DefaultBatchEncrypt(ctx, s, secrets)
}
func (s symmetricCrypter) DecryptValue(ctx context.Context, value string) (string, error) {
vals := strings.Split(value, ":")
if len(vals) != 3 {
return "", errors.New("bad value")
}
if vals[0] != "v1" {
return "", errors.New("unknown value version")
}
nonce, err := base64.StdEncoding.DecodeString(vals[1])
if err != nil {
return "", fmt.Errorf("bad value: %w", err)
}
ciphertext, err := base64.StdEncoding.DecodeString(vals[2])
if err != nil {
return "", fmt.Errorf("bad value: %w", err)
}
contract.Requiref(len(s.key) == SymmetricCrypterKeyBytes, "key", "AES-256-GCM needs a 32 byte key")
block, err := aes.NewCipher(s.key)
contract.AssertNoErrorf(err, "error creating AES cipher")
aesgcm, err := cipher.NewGCM(block)
contract.AssertNoErrorf(err, "error creating AES-GCM cipher")
if len(nonce) != aesgcm.NonceSize() {
return "", errors.New("bad value: nonce size is incorrect")
}
msg, err := aesgcm.Open(nil, nonce, ciphertext, nil)
return string(msg), err
}
func (s symmetricCrypter) BatchDecrypt(ctx context.Context, ciphertexts []string) ([]string, error) {
return DefaultBatchDecrypt(ctx, s, ciphertexts)
}
// encryptAES256GCGM returns the ciphertext and the generated nonce
func encryptAES256GCGM(plaintext string, key []byte) ([]byte, []byte) {
contract.Requiref(len(key) == SymmetricCrypterKeyBytes, "key", "AES-256-GCM needs a 32 byte key")
nonce := make([]byte, 12)
_, err := cryptorand.Read(nonce)
contract.Assertf(err == nil, "could not read from system random source")
block, err := aes.NewCipher(key)
contract.AssertNoErrorf(err, "error creating AES cipher")
aesgcm, err := cipher.NewGCM(block)
contract.AssertNoErrorf(err, "error creating AES-GCM cipher")
msg := aesgcm.Seal(nil, nonce, []byte(plaintext), nil)
return msg, nonce
}
// Crypter that just adds a prefix to the plaintext string when encrypting,
// and removes the prefix from the ciphertext when decrypting, for use in tests.
type prefixCrypter struct {
prefix string
}
func newPrefixCrypter(prefix string) Crypter {
return prefixCrypter{prefix: prefix}
}
func (c prefixCrypter) DecryptValue(ctx context.Context, ciphertext string) (string, error) {
return strings.TrimPrefix(ciphertext, c.prefix), nil
}
func (c prefixCrypter) EncryptValue(ctx context.Context, plaintext string) (string, error) {
return c.prefix + plaintext, nil
}
func (c prefixCrypter) BatchEncrypt(ctx context.Context, secrets []string) ([]string, error) {
return DefaultBatchEncrypt(ctx, c, secrets)
}
func (c prefixCrypter) BatchDecrypt(ctx context.Context, ciphertexts []string) ([]string, error) {
return DefaultBatchDecrypt(ctx, c, ciphertexts)
}
// DefaultBatchEncrypt encrypts a list of plaintexts. Each plaintext is encrypted sequentially. The returned
// list of ciphertexts is in the same order as the input list. This should only be used by implementers of Encrypter
// to implement their BatchEncrypt method in cases where they can't do more efficient than just individual operations.
func DefaultBatchEncrypt(ctx context.Context, encrypter Encrypter, plaintexts []string) ([]string, error) {
if len(plaintexts) == 0 {
return nil, nil
}
encrypted := make([]string, len(plaintexts))
for i, secret := range plaintexts {
enc, err := encrypter.EncryptValue(ctx, secret)
if err != nil {
return nil, err
}
encrypted[i] = enc
}
return encrypted, nil
}
// DefaultBatchDecrypt decrypts a list of ciphertexts. Each ciphertext is decrypted individually. The returned
// map maps from ciphertext to plaintext. This should only be used by implementers of Decrypter to implement
// their BatchDecrypt method in cases where they can't do more efficient than just individual decryptions.
func DefaultBatchDecrypt(ctx context.Context,
decrypter Decrypter, ciphertexts []string,
) ([]string, error) {
if len(ciphertexts) == 0 {
return nil, nil
}
decrypted := make([]string, len(ciphertexts))
for i, ct := range ciphertexts {
pt, err := decrypter.DecryptValue(ctx, ct)
if err != nil {
return nil, err
}
decrypted[i] = pt
}
return decrypted, nil
}
type base64Crypter struct{}
// Base64Crypter is a Crypter that "encrypts" by encoding the string to base64.
var Base64Crypter Crypter = &base64Crypter{}
func (c *base64Crypter) EncryptValue(ctx context.Context, s string) (string, error) {
return base64.StdEncoding.EncodeToString([]byte(s)), nil
}
func (c *base64Crypter) BatchEncrypt(ctx context.Context, secrets []string) ([]string, error) {
return DefaultBatchEncrypt(ctx, c, secrets)
}
func (c *base64Crypter) DecryptValue(ctx context.Context, s string) (string, error) {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return "", err
}
return string(b), nil
}
func (c *base64Crypter) BatchDecrypt(ctx context.Context, ciphertexts []string) ([]string, error) {
return DefaultBatchDecrypt(ctx, c, ciphertexts)
}
// 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 config
import (
"encoding/json"
"fmt"
"strings"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
type Key struct {
namespace string
name string
}
// MustMakeKey constructs a config.Key for a given namespace and name. The namespace may not contain a `:`
func MustMakeKey(namespace string, name string) Key {
contract.Requiref(!strings.Contains(namespace, ":"), "namespace", "may not contain a colon")
return Key{namespace: namespace, name: name}
}
// MustParseKey creates a config.Key from a string. The string must be of the form
// `<namespace>:<name>`.
func MustParseKey(s string) Key {
key, err := ParseKey(s)
contract.AssertNoErrorf(err, "failed to parse key %s", s)
return key
}
func ParseKey(s string) (Key, error) {
// Keys can take on of two forms:
//
// - <namespace>:<name> (the preferred form)
// - <namespace>:config:<name> (compat with an old requirement that every config value be in the "config" module)
//
// Where <namespace> and <name> may be any string of characters, excluding ':'.
switch strings.Count(s, ":") {
case 1:
idx := strings.Index(s, ":")
return Key{namespace: s[:idx], name: s[idx+1:]}, nil
case 2:
if mm, err := tokens.ParseModuleMember(s); err == nil {
if mm.Module().Name() == "config" {
return Key{
namespace: mm.Module().Package().String(),
name: mm.Name().String(),
}, nil
}
}
}
return Key{}, fmt.Errorf("could not parse %s as a configuration key "+
"(configuration keys should be of the form `<namespace>:<name>`)", s)
}
func (k *Key) Namespace() string {
return k.namespace
}
func (k *Key) Name() string {
return k.name
}
func (k Key) MarshalJSON() ([]byte, error) {
return json.Marshal(k.String())
}
func (k *Key) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("could not unmarshal key: %w", err)
}
pk, err := ParseKey(s)
if err != nil {
return err
}
k.namespace = pk.namespace
k.name = pk.name
return nil
}
func (k Key) MarshalYAML() (interface{}, error) {
return k.String(), nil
}
func (k *Key) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string
if err := unmarshal(&s); err != nil {
return fmt.Errorf("could not unmarshal key: %w", err)
}
pk, err := ParseKey(s)
if err != nil {
return err
}
k.namespace = pk.namespace
k.name = pk.name
return nil
}
func (k Key) String() string {
return k.namespace + ":" + k.name
}
type KeyArray []Key
func (k KeyArray) Len() int {
return len(k)
}
func (k KeyArray) Less(i int, j int) bool {
if k[i].namespace != k[j].namespace {
return strings.Compare(k[i].namespace, k[j].namespace) == -1
}
return strings.Compare(k[i].name, k[j].name) == -1
}
func (k KeyArray) Swap(i int, j int) {
k[i], k[j] = k[j], k[i]
}
// 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 config
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
)
// Map is a bag of config stored in the settings file.
type Map map[Key]Value
// Decrypt returns the configuration as a map from module member to decrypted value.
func (m Map) Decrypt(decrypter Decrypter) (map[Key]string, error) {
r := map[Key]string{}
for k, c := range m {
v, err := c.Value(decrypter)
if err != nil {
return nil, err
}
r[k] = v
}
return r, nil
}
func (m Map) Copy(decrypter Decrypter, encrypter Encrypter) (Map, error) {
newConfig := make(Map)
for k, c := range m {
val, err := c.Copy(decrypter, encrypter)
if err != nil {
return nil, err
}
newConfig[k] = val
}
return newConfig, nil
}
// SecureKeys returns a list of keys that have secure values.
func (m Map) SecureKeys() []Key {
var keys []Key
for k, v := range m {
if v.Secure() {
keys = append(keys, k)
}
}
return keys
}
// HasSecureValue returns true if the config map contains a secure (encrypted) value.
func (m Map) HasSecureValue() bool {
for _, v := range m {
if v.Secure() {
return true
}
}
return false
}
// AsDecryptedPropertyMap returns the config as a property map, with secret values decrypted.
func (m Map) AsDecryptedPropertyMap(ctx context.Context, decrypter Decrypter) (resource.PropertyMap, error) {
pm := resource.PropertyMap{}
for k, v := range m {
newV, err := v.coerceObject()
if err != nil {
return resource.PropertyMap{}, err
}
plaintext, err := newV.toDecryptedPropertyValue(ctx, decrypter)
if err != nil {
return resource.PropertyMap{}, err
}
pm[resource.PropertyKey(k.String())] = plaintext
}
return pm, nil
}
// Get gets the value for a given key. If path is true, the key's name portion is treated as a path.
func (m Map) Get(k Key, path bool) (_ Value, ok bool, err error) {
// If the key isn't a path, go ahead and lookup the value.
if !path {
v, ok := m[k]
return v, ok, nil
}
// Otherwise, parse the path and get the new config key.
p, configKey, err := parseKeyPath(k)
if err != nil {
return Value{}, false, err
}
// If we only have a single path segment, go ahead and lookup the value.
root, ok := m[configKey]
if len(p) == 1 {
return root, ok, nil
}
obj, err := root.unmarshalObject()
if err != nil {
return Value{}, false, err
}
objValue, ok, err := obj.Get(p[1:])
if !ok || err != nil {
return Value{}, ok, err
}
v, err := objValue.marshalValue()
if err != nil {
return v, false, err
}
return v, true, nil
}
// Remove removes the value for a given key. If path is true, the key's name portion is treated as a path.
func (m Map) Remove(k Key, path bool) error {
// If the key isn't a path, go ahead and delete it and return.
if !path {
delete(m, k)
return nil
}
// Otherwise, parse the path and get the new config key.
p, configKey, err := parseKeyPath(k)
if err != nil {
return err
}
// If we only have a single path segment, delete the key and return.
root, ok := m[configKey]
if len(p) == 1 {
delete(m, configKey)
return nil
}
if !ok {
return nil
}
obj, err := root.unmarshalObject()
if err != nil {
return err
}
err = obj.Delete(p[1:], p[1:])
if err != nil {
return err
}
root, err = obj.marshalValue()
if err != nil {
return err
}
m[configKey] = root
return nil
}
// Set sets the value for a given key. If path is true, the key's name portion is treated as a path.
func (m Map) Set(k Key, v Value, path bool) error {
// If the key isn't a path, go ahead and set the value and return.
if !path {
m[k] = v
return nil
}
// Otherwise, parse the path and get the new config key.
p, configKey, err := parseKeyPath(k)
if err != nil {
return err
}
var newV object
if len(p) > 1 || v.typ != TypeUnknown {
newV, err = v.coerceObject()
if err != nil {
return err
}
}
if len(p) == 1 {
m[configKey] = v
return nil
}
var obj object
if root, ok := m[configKey]; ok {
obj, err = root.unmarshalObject()
if err != nil {
return err
}
} else {
obj = object{value: newContainer(p[1])}
}
err = obj.Set(p[:1], p[1:], newV)
if err != nil {
return err
}
root, err := obj.marshalValue()
if err != nil {
return err
}
m[configKey] = root
return nil
}
func (m Map) MarshalJSON() ([]byte, error) {
rawMap := make(map[string]Value, len(m))
for k, v := range m {
rawMap[k.String()] = v
}
return json.Marshal(rawMap)
}
func (m *Map) UnmarshalJSON(b []byte) error {
rawMap := make(map[string]Value)
if err := json.Unmarshal(b, &rawMap); err != nil {
return fmt.Errorf("could not unmarshal map: %w", err)
}
newMap := make(Map, len(rawMap))
for k, v := range rawMap {
pk, err := ParseKey(k)
if err != nil {
return fmt.Errorf("could not unmarshal map: %w", err)
}
newMap[pk] = v
}
*m = newMap
return nil
}
func (m Map) MarshalYAML() (interface{}, error) {
rawMap := make(map[string]Value, len(m))
for k, v := range m {
rawMap[k.String()] = v
}
return rawMap, nil
}
func (m *Map) UnmarshalYAML(unmarshal func(interface{}) error) error {
rawMap := make(map[string]Value)
if err := unmarshal(&rawMap); err != nil {
return fmt.Errorf("could not unmarshal map: %w", err)
}
newMap := make(Map, len(rawMap))
for k, v := range rawMap {
pk, err := ParseKey(k)
if err != nil {
return fmt.Errorf("could not unmarshal map: %w", err)
}
newMap[pk] = v
}
*m = newMap
return nil
}
// parseKeyPath returns the property paths in the key and a new config key with the first
// path segment as the name.
func parseKeyPath(k Key) (resource.PropertyPath, Key, error) {
// Parse the path, which will be in the name portion of the key.
p, err := resource.ParsePropertyPathStrict(k.Name())
if err != nil {
return nil, Key{}, fmt.Errorf("invalid config key path: %w", err)
}
if len(p) == 0 {
return nil, Key{}, errors.New("empty config key path")
}
// Create a new key that has the first path segment as the name.
firstKey, ok := p[0].(string)
if !ok {
return nil, Key{}, errors.New("first path segement of config key must be a string")
}
if firstKey == "" {
return nil, Key{}, errors.New("config key is empty")
}
configKey := MustMakeKey(k.Namespace(), firstKey)
return p, configKey, nil
}
// Copyright 2016-2022, 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 config
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"time"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
var errSecureReprReserved = errors.New(`maps with the single key "secure" are reserved`)
// object is the internal object representation of a single config value. All operations on Value first decode the
// Value's string representation into its object representation. Secure strings are stored in objects as ciphertext.
type object struct {
value any
}
// objectType describes the types of values that may be stored in the value field of an object
type objectType interface {
bool | int64 | uint64 | float64 | string | CiphertextSecret | []object | map[string]object
}
// newObject creates a new object with the given representation.
func newObject[T objectType](v T) object {
return object{value: v}
}
// Secure returns true if the receiver is a secure string or a composite value that contains a secure string.
func (c object) Secure() bool {
switch v := c.value.(type) {
case []object:
for _, v := range v {
if v.Secure() {
return true
}
}
return false
case map[string]object:
for _, v := range v {
if v.Secure() {
return true
}
}
return false
case CiphertextSecret:
return true
default:
return false
}
}
// Decrypt decrypts any ciphertexts within the object and returns appropriately-shaped Plaintext values.
func (c object) Decrypt(ctx context.Context, decrypter Decrypter) (Plaintext, error) {
return c.decrypt(ctx, nil, decrypter)
}
func (c object) decrypt(ctx context.Context, path resource.PropertyPath, decrypter Decrypter) (Plaintext, error) {
switch v := c.value.(type) {
case bool:
return NewPlaintext(v), nil
case int64:
return NewPlaintext(v), nil
case uint64:
return NewPlaintext(v), nil
case float64:
return NewPlaintext(v), nil
case string:
return NewPlaintext(v), nil
case CiphertextSecret:
plaintext, err := v.Decrypt(ctx, decrypter)
if err != nil {
return Plaintext{}, fmt.Errorf("%v: %w", path, err)
}
return NewPlaintext(plaintext), nil
case []object:
vs := make([]Plaintext, len(v))
for i, v := range v {
pv, err := v.decrypt(ctx, append(path, i), decrypter)
if err != nil {
return Plaintext{}, err
}
vs[i] = pv
}
return NewPlaintext(vs), nil
case map[string]object:
vs := make(map[string]Plaintext, len(v))
for k, v := range v {
pv, err := v.decrypt(ctx, append(path, k), decrypter)
if err != nil {
return Plaintext{}, err
}
vs[k] = pv
}
return NewPlaintext(vs), nil
case nil:
return Plaintext{}, nil
default:
contract.Failf("unexpected value of type %T", v)
return Plaintext{}, nil
}
}
// Merge merges the receiver onto the given base using JSON merge patch semantics. Merge does not modify the receiver or
// the base.
func (c object) Merge(base object) object {
if co, ok := c.value.(map[string]object); ok {
if bo, ok := base.value.(map[string]object); ok {
mo := make(map[string]object, len(co))
for k, v := range bo {
mo[k] = v
}
for k, v := range co {
mo[k] = v.Merge(mo[k])
}
return newObject(mo)
}
}
return c
}
// Get gets the member value at path. The path to the receiver is prefix.
func (c object) Get(path resource.PropertyPath) (_ object, ok bool, err error) {
if len(path) == 0 {
return c, true, nil
}
switch v := c.value.(type) {
case []object:
index, ok := path[0].(int)
if !ok || index < 0 || index >= len(v) {
return object{}, false, nil
}
elem := v[index]
return elem.Get(path[1:])
case map[string]object:
key, ok := path[0].(string)
if !ok {
return object{}, false, nil
}
elem, ok := v[key]
if !ok {
return object{}, false, nil
}
return elem.Get(path[1:])
default:
return object{}, false, nil
}
}
// Delete deletes the member value at path. The path to the receiver is prefix.
func (c *object) Delete(prefix, path resource.PropertyPath) error {
if len(path) == 0 {
return nil
}
prefix = append(prefix, path[0])
switch v := c.value.(type) {
case []object:
index, ok := path[0].(int)
if !ok || index < 0 || index >= len(v) {
return nil
}
if len(path) == 1 {
c.value = append(v[:index], v[index+1:]...)
return nil
}
elem := &v[index]
return elem.Delete(prefix, path[1:])
case map[string]object:
key, ok := path[0].(string)
if !ok {
return nil
}
// If we're deleting a property from this object, make sure that the result won't be mistaken for a secure
// value when it is encoded. Secure values are encoded as `{"secure": "ciphertext"}`.
if len(path) == 1 {
if len(v) == 2 {
keys := make([]string, 0, 2)
for k := range v {
if k != key {
keys = append(keys, k)
}
}
if len(keys) == 1 && keys[0] == "secure" {
if _, ok := v["secure"].value.(string); ok {
return fmt.Errorf("%v: %w", prefix, errSecureReprReserved)
}
}
}
delete(v, key)
return nil
}
elem, ok := v[key]
if !ok {
return nil
}
err := elem.Delete(prefix, path[1:])
v[key] = elem
return err
default:
return nil
}
}
func newContainer(accessor any) any {
switch accessor := accessor.(type) {
case int:
return make([]object, accessor+1)
case string:
return make(map[string]object)
default:
contract.Failf("unexpected accessor kind %T", accessor)
return nil
}
}
// Set sets the member value at path to new. The path to the receiver is prefix.
func (c *object) Set(prefix, path resource.PropertyPath, new object) error {
if len(path) == 0 {
*c = new
return nil
}
// Check the type of the receiver and create a new container if allowed.
switch c.value.(type) {
case []object, map[string]object:
// OK
case nil:
// This value is nil. Create a new container ny inferring the container type (i.e. array or object) from the
// accessor at the head of the path.
c.value = newContainer(path[0])
default:
// COMPAT: If this is the first level, we create a new container and overwrite the old value rather than issuing
// a type error.
if len(prefix) == 1 {
c.value = newContainer(path[0])
} else {
switch path[0].(type) {
case int:
return fmt.Errorf("%v: expected an array", prefix)
case string:
return fmt.Errorf("%v: expected a map", prefix)
default:
contract.Failf("unreachable")
return nil
}
}
}
prefix = append(prefix, path[0])
switch v := c.value.(type) {
case []object:
index, ok := path[0].(int)
if !ok {
return fmt.Errorf("%v: key for an array must be an int", prefix)
}
if index < 0 || index > len(v) {
return fmt.Errorf("%v: array index out of range", prefix)
}
if index == len(v) {
v = append(v, object{})
c.value = v
}
elem := &v[index]
return elem.Set(prefix, path[1:], new)
case map[string]object:
key, ok := path[0].(string)
if !ok {
return fmt.Errorf("%v: key for a map must be a string", prefix)
}
// If we're adding a property tothis object, make sure that the result won't be mistaken for a secure
// value when it is encoded. Secure values are encoded as `{"secure": "ciphertext"}`.
if len(path) == 1 && len(v) == 0 && key == "secure" {
if _, ok := new.value.(string); ok {
return errSecureReprReserved
}
}
elem := v[key]
err := elem.Set(prefix, path[1:], new)
v[key] = elem
return err
default:
contract.Failf("unreachable")
return nil
}
}
// SecureValues returns the plaintext values for any secure strings contained in the receiver.
func (c object) SecureValues(dec Decrypter) ([]string, error) {
switch v := c.value.(type) {
case []object:
var values []string
for _, v := range v {
vs, err := v.SecureValues(dec)
if err != nil {
return nil, err
}
values = append(values, vs...)
}
return values, nil
case map[string]object:
var values []string
for _, v := range v {
vs, err := v.SecureValues(dec)
if err != nil {
return nil, err
}
values = append(values, vs...)
}
return values, nil
case CiphertextSecret:
plaintext, err := v.Decrypt(context.TODO(), dec)
if err != nil {
return nil, err
}
return []string{string(plaintext)}, nil
default:
return nil, nil
}
}
// marshalValue converts the receiver into a Value.
func (c object) marshalValue() (v Value, err error) {
v.value, v.secure, v.object, err = c.MarshalString()
return
}
// marshalObjectValue converts the receiver into a shape that is compatible with Value.ToObject().
func (c object) marshalObjectValue(root bool) any {
switch v := c.value.(type) {
case []object:
vs := make([]any, len(v))
for i, v := range v {
vs[i] = v.marshalObjectValue(false)
}
return vs
case map[string]object:
vs := make(map[string]any, len(v))
for k, v := range v {
vs[k] = v.marshalObjectValue(false)
}
return vs
case CiphertextSecret:
if !root {
return map[string]any{"secure": v.value}
}
return v.value
default:
return c.value
}
}
// MarshalString returns the receiver's string representation. The string representation is accompanied by bools that
// indicate whether the receiver is secure and whether it is an object.
func (c object) MarshalString() (text string, secure, object bool, err error) {
switch v := c.value.(type) {
case bool, int64, uint64, float64:
bytes, err := c.MarshalJSON()
return string(bytes), false, false, err
case string:
return v, false, false, nil
case CiphertextSecret:
return v.value, true, false, nil
default:
bytes, err := c.MarshalJSON()
if err != nil {
return "", false, false, err
}
return string(bytes), c.Secure(), true, nil
}
}
// UnmarshalString unmarshals the string representation accompanied by secure and object metadata into the receiver.
func (c *object) UnmarshalString(text string, secure, object bool) error {
if !object {
if secure {
c.value = CiphertextSecret{text}
} else {
c.value = text
}
return nil
}
return c.UnmarshalJSON([]byte(text))
}
func (c object) MarshalJSON() ([]byte, error) {
return json.Marshal(c.marshalObject())
}
func (c *object) UnmarshalJSON(b []byte) error {
dec := json.NewDecoder(bytes.NewReader(b))
dec.UseNumber()
var v any
err := dec.Decode(&v)
if err != nil {
return err
}
*c, err = unmarshalObject(v)
return err
}
func (c object) MarshalYAML() (any, error) {
return c.marshalObject(), nil
}
func (c *object) UnmarshalYAML(unmarshal func(any) error) error {
var v any
err := unmarshal(&v)
if err != nil {
return err
}
*c, err = unmarshalObject(v)
return err
}
// unmarshalObject unmarshals a raw JSON or YAML value into an object. json.Number values are converted to int64 if
// possible and float64 otherwise.
func unmarshalObject(v any) (object, error) {
switch v := v.(type) {
case bool:
return newObject(v), nil
case json.Number:
if i, err := v.Int64(); err == nil {
return newObject(i), nil
}
f, err := v.Float64()
if err == nil {
return newObject(f), nil
}
return object{}, fmt.Errorf("unrepresentable number %v: %w", v, err)
case int:
return newObject(int64(v)), nil
case uint64:
return newObject(v), nil
case int64:
return newObject(v), nil
case float64:
return newObject(v), nil
case string:
return newObject(v), nil
case time.Time:
return newObject(v.String()), nil
case map[string]any:
if ok, ciphertext := isSecureValue(v); ok {
return newObject(ciphertext), nil
}
m := make(map[string]object, len(v))
for k, v := range v {
sv, err := unmarshalObject(v)
if err != nil {
return object{}, err
}
m[k] = sv
}
return newObject(m), nil
case map[any]any:
m := make(map[string]any, len(v))
for k, v := range v {
m[fmt.Sprintf("%v", k)] = v
}
return unmarshalObject(m)
case []any:
a := make([]object, len(v))
for i, v := range v {
sv, err := unmarshalObject(v)
if err != nil {
return object{}, err
}
a[i] = sv
}
return newObject(a), nil
case nil:
return object{}, nil
default:
contract.Failf("unexpected wire type %T", v)
return object{}, nil
}
}
// marshalObject returns the value that should be passed to the JSON or YAML packages when marshaling the receiver.
func (c object) marshalObject() any {
if ciphertext, ok := c.value.(CiphertextSecret); ok {
type secureValue struct {
Secure string `json:"secure" yaml:"secure"`
}
return secureValue{Secure: ciphertext.value}
}
return c.value
}
// isSecureValue returns true if the object is a `map[string]any` of length one with a "secure" property of type string.
func isSecureValue(v any) (bool, CiphertextSecret) {
if m, isMap := v.(map[string]any); isMap && len(m) == 1 {
if val, hasSecureKey := m["secure"]; hasSecureKey {
if valString, isString := val.(string); isString {
return true, CiphertextSecret{valString}
}
}
}
return false, CiphertextSecret{}
}
func (c object) toDecryptedPropertyValue(ctx context.Context, decrypter Decrypter) (resource.PropertyValue, error) {
plaintext, err := c.Decrypt(ctx, decrypter)
if err != nil {
return resource.PropertyValue{}, err
}
return plaintext.PropertyValue(), nil
}
// coerce attempts to coerce v to a boolean or number value. Returns the coerced value and true if coercion succeeds
// and (nil, false) otherwise.
//
// The coercion rules are:
// - "false" and "true" coerce to false and true, respectively
// - strings of base-10 digits that do not begin with '0' are coerced to int64 or uint64
func coerce(v string) (any, bool) {
// If "false" or "true", return the boolean value.
switch v {
case "false":
return false, true
case "true":
return true, true
}
// If the value has more than one character and starts with "0", return the value as-is
// so values like "0123456" are saved as a string (without stripping any leading zeros)
// rather than as the integer 123456.
if len(v) > 1 && v[0] == '0' {
return nil, false
}
// If it's convertible to an int, return the int.
if i, err := strconv.ParseInt(v, 10, 64); err == nil {
return i, true
}
if i, err := strconv.ParseUint(v, 10, 64); err == nil {
return i, true
}
return nil, false
}
// Copyright 2016-2022, 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 config
import (
"context"
"encoding/json"
"fmt"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
// PlaintextType describes the allowed types for a Plaintext.
type PlaintextType interface {
bool | int64 | uint64 | float64 | string | PlaintextSecret | []Plaintext | map[string]Plaintext
}
// Plaintext is a single plaintext config value.
type Plaintext struct {
value any
}
// NewPlaintext creates a new plaintext config value.
func NewPlaintext[T PlaintextType](v T) Plaintext {
if m, ok := any(v).(map[string]Plaintext); ok && len(m) == 1 {
if _, ok := m["secure"].Value().(string); ok {
contract.Failf("%s", errSecureReprReserved.Error())
}
}
return Plaintext{value: v}
}
// Secure returns true if the receiver is a secure string or a composite value that contains a secure string.
func (c Plaintext) Secure() bool {
switch v := c.Value().(type) {
case []Plaintext:
for _, v := range v {
if v.Secure() {
return true
}
}
return false
case map[string]Plaintext:
for _, v := range v {
if v.Secure() {
return true
}
}
return false
case PlaintextSecret:
return true
default:
return false
}
}
// Value returns the inner plaintext value.
//
// The returned value satisfies the PlaintextType constraint.
func (c Plaintext) Value() any {
return c.value
}
// GoValue returns the inner plaintext value as a plain Go value:
//
// - secure strings are mapped to their plaintext
// - []Plaintext values are mapped to []any values
// - map[string]Plaintext values are mapped to map[string]any values
func (c Plaintext) GoValue() any {
switch v := c.Value().(type) {
case []Plaintext:
vs := make([]any, len(v))
for i, v := range v {
vs[i] = v.GoValue()
}
return vs
case map[string]Plaintext:
vs := make(map[string]any, len(v))
for k, v := range v {
vs[k] = v.GoValue()
}
return vs
case PlaintextSecret:
return string(v)
default:
return v
}
}
// PropertyValue converts a plaintext value into a resource.PropertyValue.
func (c Plaintext) PropertyValue() resource.PropertyValue {
var prop resource.PropertyValue
switch v := c.Value().(type) {
case bool:
prop = resource.NewProperty(v)
case int64:
prop = resource.NewProperty(float64(v))
case uint64:
prop = resource.NewProperty(float64(v))
case float64:
prop = resource.NewProperty(v)
case string:
prop = resource.NewProperty(v)
case PlaintextSecret:
prop = resource.MakeSecret(resource.NewProperty(string(v)))
case []Plaintext:
vs := make([]resource.PropertyValue, len(v))
for i, v := range v {
vs[i] = v.PropertyValue()
}
prop = resource.NewProperty(vs)
case map[string]Plaintext:
vs := make(resource.PropertyMap, len(v))
for k, v := range v {
vs[resource.PropertyKey(k)] = v.PropertyValue()
}
prop = resource.NewProperty(vs)
case nil:
prop = resource.NewNullProperty()
default:
contract.Failf("unexpected value of type %T", v)
return resource.PropertyValue{}
}
return prop
}
// Encrypt converts the receiver as a Value. All secure strings in the result are encrypted using encrypter.
func (c Plaintext) Encrypt(ctx context.Context, encrypter Encrypter) (Value, error) {
obj, err := c.encrypt(ctx, nil, encrypter)
if err != nil {
return Value{}, err
}
return obj.marshalValue()
}
// encrypt converts the receiver to an object. All secure strings in the result are encrypted using encrypter.
func (c Plaintext) encrypt(ctx context.Context, path resource.PropertyPath, encrypter Encrypter) (object, error) {
switch v := c.Value().(type) {
case nil:
return object{}, nil
case bool:
return newObject(v), nil
case int64:
return newObject(v), nil
case uint64:
return newObject(v), nil
case float64:
return newObject(v), nil
case string:
return newObject(v), nil
case PlaintextSecret:
ciphertext, err := v.Encrypt(ctx, encrypter)
if err != nil {
return object{}, fmt.Errorf("%v: %w", path, err)
}
return newObject(ciphertext), nil
case []Plaintext:
vs := make([]object, len(v))
for i, v := range v {
ev, err := v.encrypt(ctx, append(path, i), encrypter)
if err != nil {
return object{}, err
}
vs[i] = ev
}
return newObject(vs), nil
case map[string]Plaintext:
vs := make(map[string]object, len(v))
for k, v := range v {
ev, err := v.encrypt(ctx, append(path, k), encrypter)
if err != nil {
return object{}, err
}
vs[k] = ev
}
return newObject(vs), nil
default:
contract.Failf("unexpected plaintext of type %T", v)
return object{}, nil
}
}
// marshalText returns the text representation of the plaintext.
func (c Plaintext) marshalText() (string, error) {
switch v := c.Value().(type) {
case string:
return v, nil
case PlaintextSecret:
return string(v), nil
}
bytes, err := json.Marshal(c.GoValue())
if err != nil {
return "", err
}
return string(bytes), nil
}
func (c Plaintext) MarshalJSON() ([]byte, error) {
contract.Failf("plaintext must be encrypted before marshaling")
return nil, nil
}
func (c *Plaintext) UnmarshalJSON(b []byte) error {
contract.Failf("plaintext cannot be unmarshaled")
return nil
}
func (c Plaintext) MarshalYAML() (any, error) {
contract.Failf("plaintext must be encrypted before marshaling")
return nil, nil
}
func (c *Plaintext) UnmarshalYAML(unmarshal func(any) error) error {
contract.Failf("plaintext cannot be unmarshaled")
return nil
}
// Copyright 2016-2022, 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 config
import "context"
// A CiphertextSecret is a secret config value represented as ciphertext.
type CiphertextSecret struct {
value string
}
// Decrypt decrypts a ciphertext secret into its plaintext.
func (c CiphertextSecret) Decrypt(ctx context.Context, dec Decrypter) (PlaintextSecret, error) {
plaintext, err := dec.DecryptValue(ctx, c.value)
if err != nil {
return "", err
}
return PlaintextSecret(plaintext), nil
}
// A PlaintextSecret is a secret configuration value represented as plaintext.
type PlaintextSecret string
// Encrypt encrypts a plaintext value into its ciphertext.
func (p PlaintextSecret) Encrypt(ctx context.Context, enc Encrypter) (CiphertextSecret, error) {
ciphertext, err := enc.EncryptValue(ctx, string(p))
if err != nil {
return CiphertextSecret{}, err
}
return CiphertextSecret{ciphertext}, nil
}
// Copyright 2016-2022, 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 config
import (
"context"
"strconv"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
type Type int
const (
TypeUnknown = iota
TypeString
TypeInt
TypeFloat
TypeBool
)
// Value is a single config value.
type Value struct {
value string
secure bool
object bool
typ Type
}
func NewSecureValue(v string) Value {
return Value{value: v, secure: true}
}
func NewValue(v string) Value {
return Value{value: v, secure: false}
}
func NewTypedValue(v string, t Type) Value {
return Value{value: v, secure: false, typ: t}
}
func NewSecureObjectValue(v string) Value {
return Value{value: v, secure: true, object: true}
}
func NewObjectValue(v string) Value {
return Value{value: v, secure: false, object: true}
}
// Value fetches the value of this configuration entry, using decrypter to decrypt if necessary. If the value
// is a secret and decrypter is nil, or if decryption fails for any reason, a non-nil error is returned.
func (c Value) Value(decrypter Decrypter) (string, error) {
if decrypter == NopDecrypter {
return c.value, nil
}
obj, err := c.unmarshalObject()
if err != nil {
return "", err
}
plaintext, err := obj.Decrypt(context.TODO(), decrypter)
if err != nil {
return "", err
}
return plaintext.marshalText()
}
func (c Value) Decrypt(ctx context.Context, decrypter Decrypter) (Plaintext, error) {
obj, err := c.unmarshalObject()
if err != nil {
return Plaintext{}, err
}
return obj.Decrypt(ctx, decrypter)
}
func (c Value) Merge(base Value) (Value, error) {
obj, err := c.unmarshalObject()
if err != nil {
return Value{}, err
}
baseObj, err := base.unmarshalObject()
if err != nil {
return Value{}, err
}
return obj.Merge(baseObj).marshalValue()
}
func (c Value) Copy(decrypter Decrypter, encrypter Encrypter) (Value, error) {
obj, err := c.unmarshalObject()
if err != nil {
return Value{}, err
}
plaintext, err := obj.Decrypt(context.TODO(), decrypter)
if err != nil {
return Value{}, err
}
return plaintext.Encrypt(context.TODO(), encrypter)
}
func (c Value) SecureValues(decrypter Decrypter) ([]string, error) {
obj, err := c.unmarshalObject()
if err != nil {
return nil, err
}
return obj.SecureValues(decrypter)
}
func (c Value) Secure() bool {
return c.secure
}
func (c Value) Object() bool {
return c.object
}
// coerceObject returns a more suitable value for objects by converting untyped string configuration values into
// boolean or number values.
func (c Value) coerceObject() (object, error) {
// If it's a secure value, a typed value, or an object, return as-is.
if c.Secure() || c.Object() || c.typ != TypeUnknown {
return c.unmarshalObject()
}
// Otherwise, attempt to coerce the value into a boolean or a number.
coerced, ok := coerce(c.value)
if !ok {
return c.unmarshalObject()
}
switch coerced := coerced.(type) {
case bool:
return newObject(coerced), nil
case int64:
return newObject(coerced), nil
case uint64:
return newObject(coerced), nil
default:
contract.Failf("unreachable")
return object{}, nil
}
}
func (c Value) unmarshalObject() (object, error) {
if c.secure || c.object || c.typ == TypeUnknown {
var obj object
err := obj.UnmarshalString(c.value, c.secure, c.object)
return obj, err
}
switch c.typ {
case TypeString:
return newObject(c.value), nil
case TypeInt:
i, err := strconv.Atoi(c.value)
if err != nil {
return object{}, err
}
return newObject(int64(i)), nil
case TypeBool:
return newObject(c.value == "true"), nil
case TypeFloat:
f, err := strconv.ParseFloat(c.value, 64)
if err != nil {
return object{}, err
}
return newObject(f), nil
default:
contract.Failf("unreachable")
return object{}, nil
}
}
// ToObject returns the string value (if not an object), or the unmarshalled JSON object (if an object).
func (c Value) ToObject() (any, error) {
obj, err := c.unmarshalObject()
if err != nil {
return nil, err
}
return obj.marshalObjectValue(true), nil
}
func (c Value) MarshalJSON() ([]byte, error) {
obj, err := c.unmarshalObject()
if err != nil {
return nil, err
}
return obj.MarshalJSON()
}
func (c *Value) UnmarshalJSON(b []byte) (err error) {
var obj object
if err = obj.UnmarshalJSON(b); err != nil {
return err
}
c.value, c.secure, c.object, err = obj.MarshalString()
return err
}
func (c Value) MarshalYAML() (interface{}, error) {
obj, err := c.unmarshalObject()
if err != nil {
return "", err
}
return obj.MarshalYAML()
}
func (c *Value) UnmarshalYAML(unmarshal func(interface{}) error) (err error) {
var obj object
if err = obj.UnmarshalYAML(unmarshal); err != nil {
return err
}
c.value, c.secure, c.object, err = obj.MarshalString()
return err
}
// Copyright 2019-2024, 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 resource
type CustomTimeouts struct {
Create float64 `json:"create,omitempty" yaml:"create,omitempty"`
Update float64 `json:"update,omitempty" yaml:"update,omitempty"`
Delete float64 `json:"delete,omitempty" yaml:"delete,omitempty"`
}
func (c *CustomTimeouts) IsNotEmpty() bool {
return c.Delete != 0 || c.Update != 0 || c.Create != 0
}
// 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 resource
import (
"github.com/pulumi/pulumi/sdk/v3/go/common/util/mapper"
)
// NewErrors creates a new error list pertaining to a resource. Note that it just turns around and defers to
// the same mapping infrastructure used for serialization and deserialization, but it presents a nicer interface.
func NewErrors(errs []error) error {
return mapper.NewMappingError(errs)
}
// NewPropertyError creates a new error pertaining to a resource's property. Note that it just turns around and defers
// to the same mapping infrastructure used for serialization and deserialization, but it presents a nicer interface.
func NewPropertyError(typ string, property string, err error) error {
return mapper.NewFieldError(typ, property, err)
}
// Copyright 2016-2021, 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 resource
import (
"fmt"
"reflect"
"sort"
"strings"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/archive"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/asset"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/sig"
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/mapper"
)
// PropertyKey is the name of a property.
type PropertyKey tokens.Name
// PropertyMap is a simple map keyed by property name with "JSON-like" values.
type PropertyMap map[PropertyKey]PropertyValue
// NewPropertyMap turns a struct into a property map, using any JSON tags inside to determine naming.
func NewPropertyMap(s interface{}) PropertyMap {
return NewPropertyMapRepl(s, nil, nil)
}
// NewPropertyMapRepl turns a struct into a property map, using any JSON tags inside to determine naming. If non-nil
// replk or replv function(s) are provided, key and/or value transformations are performed during the mapping.
func NewPropertyMapRepl(s interface{},
replk func(string) (PropertyKey, bool), replv func(interface{}) (PropertyValue, bool),
) PropertyMap {
m, err := mapper.Unmap(s)
contract.Assertf(err == nil, "Struct of properties failed to map correctly: %v", err)
return NewPropertyMapFromMapRepl(m, replk, replv)
}
// NewPropertyMapFromMap creates a resource map from a regular weakly typed JSON-like map.
func NewPropertyMapFromMap(m map[string]interface{}) PropertyMap {
return NewPropertyMapFromMapRepl(m, nil, nil)
}
// NewPropertyMapFromMapRepl optionally replaces keys/values in an existing map while creating a new resource map.
func NewPropertyMapFromMapRepl(m map[string]interface{},
replk func(string) (PropertyKey, bool), replv func(interface{}) (PropertyValue, bool),
) PropertyMap {
result := make(PropertyMap)
for k, v := range m {
key := PropertyKey(k)
if replk != nil {
if rk, repl := replk(k); repl {
key = rk
}
}
result[key] = NewPropertyValueRepl(v, replk, replv)
}
return result
}
// PropertyValue is the value of a property, limited to a select few types (see below).
type PropertyValue struct {
V interface{}
}
// Computed represents the absence of a property value, because it will be computed at some point in the future. It
// contains a property value which represents the underlying expected type of the eventual property value.
type Computed struct {
Element PropertyValue // the eventual value (type) of the computed property.
}
// Output is a property value that will eventually be computed by the resource provider. If an output property is
// encountered, it means the resource has not yet been created, and so the output value is unavailable. Note that an
// output property is a special case of computed, but carries additional semantic meaning.
type Output struct {
Element PropertyValue // the value of this output if it is resolved.
Known bool `json:"-"` // true if this output's value is known.
Secret bool `json:"-"` // true if this output's value is secret.
Dependencies []URN `json:"-"` // the dependencies associated with this output.
}
// Secret indicates that the underlying value should be persisted securely.
//
// In order to facilitate the ability to distinguish secrets with identical plaintext in downstream code that may
// want to cache a secret's ciphertext, secret PropertyValues hold the address of the Secret. If a secret must be
// copied, its value--not its address--should be copied.
type Secret struct {
Element PropertyValue
}
// ResourceReference is a property value that represents a reference to a Resource. The reference captures the
// resource's URN, ID, and the version of its containing package. Note that there are several cases to consider with
// respect to the ID:
//
// - The reference may not contain an ID if the referenced resource is a component resource. In this case, the ID will
// be null.
// - The ID may be unknown (in which case it will be the unknown property value)
// - Otherwise, the ID must be a string.
//
//nolint:revive
type ResourceReference struct {
URN URN
ID PropertyValue
PackageVersion string
}
func (ref ResourceReference) IDString() (value string, hasID bool) {
switch {
case ref.ID.IsComputed():
return "", true
case ref.ID.IsString():
return ref.ID.StringValue(), true
default:
return "", false
}
}
func (ref ResourceReference) Equal(other ResourceReference) bool {
if ref.URN != other.URN {
return false
}
vid, oid := ref.ID, other.ID
if vid.IsComputed() && oid.IsComputed() {
return true
}
return vid.DeepEquals(oid)
}
type ReqError struct {
K PropertyKey
}
func IsReqError(err error) bool {
_, isreq := err.(*ReqError)
return isreq
}
func (err *ReqError) Error() string {
return fmt.Sprintf("required property '%v' is missing", err.K)
}
// HasValue returns true if the slot associated with the given property key contains a real value. It returns false
// if a value is null or an output property that is awaiting a value to be assigned. That is to say, HasValue indicates
// a semantically meaningful value is present (even if it's a computed one whose concrete value isn't yet evaluated).
func (props PropertyMap) HasValue(k PropertyKey) bool {
v, has := props[k]
return has && v.HasValue()
}
// ContainsUnknowns returns true if the property map contains at least one unknown value.
func (props PropertyMap) ContainsUnknowns() bool {
for _, v := range props {
if v.ContainsUnknowns() {
return true
}
}
return false
}
// ContainsSecrets returns true if the property map contains at least one secret value.
func (props PropertyMap) ContainsSecrets() bool {
for _, v := range props {
if v.ContainsSecrets() {
return true
}
}
return false
}
// Mappable returns a mapper-compatible object map, suitable for deserialization into structures.
func (props PropertyMap) Mappable() map[string]interface{} {
return props.MapRepl(nil, nil)
}
// MapRepl returns a mapper-compatible object map, suitable for deserialization into structures. A key and/or value
// replace function, replk/replv, may be passed that will replace elements using custom logic if appropriate.
func (props PropertyMap) MapRepl(replk func(string) (string, bool),
replv func(PropertyValue) (interface{}, bool),
) map[string]interface{} {
obj := make(map[string]interface{})
for _, k := range props.StableKeys() {
key := string(k)
if replk != nil {
if rk, repk := replk(key); repk {
key = rk
}
}
obj[key] = props[k].MapRepl(replk, replv)
}
return obj
}
// Copy makes a shallow copy of the map.
func (props PropertyMap) Copy() PropertyMap {
new := make(PropertyMap)
for k, v := range props {
new[k] = v
}
return new
}
// StableKeys returns all of the map's keys in a stable order.
func (props PropertyMap) StableKeys() []PropertyKey {
sorted := slice.Prealloc[PropertyKey](len(props))
for k := range props {
sorted = append(sorted, k)
}
sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
return sorted
}
// PropertyValueType enumerates the actual types that may be stored in a PropertyValue.
//
//nolint:lll
type PropertyValueType interface {
bool | float64 | string | *asset.Asset | *archive.Archive | Computed | Output | *Secret | ResourceReference | []PropertyValue | PropertyMap
}
// NewProperty creates a new PropertyValue.
func NewProperty[T PropertyValueType](v T) PropertyValue {
return PropertyValue{v}
}
func NewNullProperty() PropertyValue { return PropertyValue{nil} }
func NewBoolProperty(v bool) PropertyValue { return PropertyValue{v} }
func NewNumberProperty(v float64) PropertyValue { return PropertyValue{v} }
func NewStringProperty(v string) PropertyValue { return PropertyValue{v} }
func NewArrayProperty(v []PropertyValue) PropertyValue { return PropertyValue{v} }
func NewAssetProperty(v *asset.Asset) PropertyValue { return PropertyValue{v} }
func NewArchiveProperty(v *archive.Archive) PropertyValue { return PropertyValue{v} }
func NewObjectProperty(v PropertyMap) PropertyValue { return PropertyValue{v} }
func NewComputedProperty(v Computed) PropertyValue { return PropertyValue{v} }
func NewOutputProperty(v Output) PropertyValue { return PropertyValue{v} }
func NewSecretProperty(v *Secret) PropertyValue { return PropertyValue{v} }
func NewResourceReferenceProperty(v ResourceReference) PropertyValue { return PropertyValue{v} }
func MakeComputed(v PropertyValue) PropertyValue {
return NewProperty(Computed{Element: v})
}
func MakeOutput(v PropertyValue) PropertyValue {
return NewProperty(Output{Element: v})
}
func MakeSecret(v PropertyValue) PropertyValue {
return NewProperty(&Secret{Element: v})
}
// MakeComponentResourceReference creates a reference to a component resource.
func MakeComponentResourceReference(urn URN, packageVersion string) PropertyValue {
return NewProperty(ResourceReference{
URN: urn,
PackageVersion: packageVersion,
})
}
// MakeCustomResourceReference creates a reference to a custom resource. If the resource's ID is the empty string, it
// will be treated as unknown.
func MakeCustomResourceReference(urn URN, id ID, packageVersion string) PropertyValue {
idProp := NewProperty(string(id))
if id == "" {
idProp = MakeComputed(NewProperty(""))
}
return NewProperty(ResourceReference{
ID: idProp,
URN: urn,
PackageVersion: packageVersion,
})
}
// NewPropertyValue turns a value into a property value, provided it is of a legal "JSON-like" kind.
func NewPropertyValue(v interface{}) PropertyValue {
return NewPropertyValueRepl(v, nil, nil)
}
// NewPropertyValueRepl turns a value into a property value, provided it is of a legal "JSON-like" kind. The
// replacement functions, replk and replv, may be supplied to transform keys and/or values as the mapping takes place.
func NewPropertyValueRepl(v interface{},
replk func(string) (PropertyKey, bool), replv func(interface{}) (PropertyValue, bool),
) PropertyValue {
// If a replacement routine is supplied, use that.
if replv != nil {
if rv, repl := replv(v); repl {
return rv
}
}
// If nil, easy peasy, just return a null.
if v == nil {
return NewNullProperty()
}
// Else, check for some known primitive types.
switch t := v.(type) {
case bool:
return NewProperty(t)
case int:
return NewProperty(float64(t))
case uint:
return NewProperty(float64(t))
case int32:
return NewProperty(float64(t))
case uint32:
return NewProperty(float64(t))
case int64:
return NewProperty(float64(t))
case uint64:
return NewProperty(float64(t))
case float32:
return NewProperty(float64(t))
case float64:
return NewProperty(t)
case string:
return NewProperty(t)
case *asset.Asset:
return NewProperty(t)
case *archive.Archive:
return NewProperty(t)
case Computed:
return NewProperty(t)
case Output:
return NewProperty(t)
case *Secret:
return NewProperty(t)
case ResourceReference:
return NewProperty(t)
case PropertyValue:
return t
}
// Next, see if it's an array, slice, pointer or struct, and handle each accordingly.
rv := reflect.ValueOf(v)
//nolint:exhaustive // We intentionally only handle some types here.
switch rk := rv.Type().Kind(); rk {
case reflect.Array, reflect.Slice:
// If an array or slice, just create an array out of it.
arr := []PropertyValue{}
for i := 0; i < rv.Len(); i++ {
elem := rv.Index(i)
arr = append(arr, NewPropertyValueRepl(elem.Interface(), replk, replv))
}
return NewProperty(arr)
case reflect.Ptr:
// If a pointer, recurse and return the underlying value.
if rv.IsNil() {
return NewNullProperty()
}
return NewPropertyValueRepl(rv.Elem().Interface(), replk, replv)
case reflect.Map:
// If a map, create a new property map, provided the keys and values are okay.
obj := PropertyMap{}
for iter := rv.MapRange(); iter.Next(); {
key := iter.Key()
if key.Kind() != reflect.String {
contract.Failf("Unrecognized PropertyMap key type %v", key.Type())
}
pk := PropertyKey(key.String())
if replk != nil {
if rk, repl := replk(string(pk)); repl {
pk = rk
}
}
val := iter.Value().Interface()
pv := NewPropertyValueRepl(val, replk, replv)
obj[pk] = pv
}
return NewProperty(obj)
case reflect.String:
return NewProperty(rv.String())
case reflect.Struct:
obj := NewPropertyMapRepl(v, replk, replv)
return NewProperty(obj)
default:
contract.Failf("Unrecognized value type: type=%v kind=%v", rv.Type(), rk)
return NewNullProperty()
}
}
// HasValue returns true if a value is semantically meaningful.
func (v PropertyValue) HasValue() bool {
if v.IsOutput() {
return v.OutputValue().Known
}
return !v.IsNull()
}
// ContainsUnknowns returns true if the property value contains at least one unknown (deeply).
func (v PropertyValue) ContainsUnknowns() bool {
if v.IsComputed() || (v.IsOutput() && !v.OutputValue().Known) {
return true
} else if v.IsArray() {
for _, e := range v.ArrayValue() {
if e.ContainsUnknowns() {
return true
}
}
} else if v.IsObject() {
return v.ObjectValue().ContainsUnknowns()
} else if v.IsSecret() {
return v.SecretValue().Element.ContainsUnknowns()
}
return false
}
// ContainsSecrets returns true if the property value contains at least one secret (deeply).
func (v PropertyValue) ContainsSecrets() bool {
if v.IsSecret() {
return true
} else if v.IsComputed() {
return v.Input().Element.ContainsSecrets()
} else if v.IsOutput() {
return v.OutputValue().Secret || v.OutputValue().Element.ContainsSecrets()
} else if v.IsArray() {
for _, e := range v.ArrayValue() {
if e.ContainsSecrets() {
return true
}
}
} else if v.IsObject() {
return v.ObjectValue().ContainsSecrets()
}
return false
}
// BoolValue fetches the underlying bool value (panicking if it isn't a bool).
func (v PropertyValue) BoolValue() bool { return v.V.(bool) }
// NumberValue fetches the underlying number value (panicking if it isn't a number).
func (v PropertyValue) NumberValue() float64 { return v.V.(float64) }
// StringValue fetches the underlying string value (panicking if it isn't a string).
func (v PropertyValue) StringValue() string { return v.V.(string) }
// ArrayValue fetches the underlying array value (panicking if it isn't a array).
func (v PropertyValue) ArrayValue() []PropertyValue { return v.V.([]PropertyValue) }
// AssetValue fetches the underlying asset value (panicking if it isn't an asset).
func (v PropertyValue) AssetValue() *asset.Asset { return v.V.(*asset.Asset) }
// ArchiveValue fetches the underlying archive value (panicking if it isn't an archive).
func (v PropertyValue) ArchiveValue() *archive.Archive { return v.V.(*archive.Archive) }
// ObjectValue fetches the underlying object value (panicking if it isn't a object).
func (v PropertyValue) ObjectValue() PropertyMap { return v.V.(PropertyMap) }
// Input fetches the underlying computed value (panicking if it isn't a computed).
func (v PropertyValue) Input() Computed { return v.V.(Computed) }
// OutputValue fetches the underlying output value (panicking if it isn't a output).
func (v PropertyValue) OutputValue() Output { return v.V.(Output) }
// SecretValue fetches the underlying secret value (panicking if it isn't a secret).
func (v PropertyValue) SecretValue() *Secret { return v.V.(*Secret) }
// ResourceReferenceValue fetches the underlying resource reference value (panicking if it isn't a resource reference).
func (v PropertyValue) ResourceReferenceValue() ResourceReference { return v.V.(ResourceReference) }
// IsNull returns true if the underlying value is a null.
func (v PropertyValue) IsNull() bool {
return v.V == nil
}
// IsBool returns true if the underlying value is a bool.
func (v PropertyValue) IsBool() bool {
_, is := v.V.(bool)
return is
}
// IsNumber returns true if the underlying value is a number.
func (v PropertyValue) IsNumber() bool {
_, is := v.V.(float64)
return is
}
// IsString returns true if the underlying value is a string.
func (v PropertyValue) IsString() bool {
_, is := v.V.(string)
return is
}
// IsArray returns true if the underlying value is an array.
func (v PropertyValue) IsArray() bool {
_, is := v.V.([]PropertyValue)
return is
}
// IsAsset returns true if the underlying value is an asset.
func (v PropertyValue) IsAsset() bool {
_, is := v.V.(*asset.Asset)
return is
}
// IsArchive returns true if the underlying value is an archive.
func (v PropertyValue) IsArchive() bool {
_, is := v.V.(*archive.Archive)
return is
}
// IsObject returns true if the underlying value is an object.
func (v PropertyValue) IsObject() bool {
_, is := v.V.(PropertyMap)
return is
}
// IsComputed returns true if the underlying value is a computed value.
func (v PropertyValue) IsComputed() bool {
_, is := v.V.(Computed)
return is
}
// IsOutput returns true if the underlying value is an output value.
func (v PropertyValue) IsOutput() bool {
_, is := v.V.(Output)
return is
}
// IsSecret returns true if the underlying value is a secret value.
func (v PropertyValue) IsSecret() bool {
_, is := v.V.(*Secret)
return is
}
// IsResourceReference returns true if the underlying value is a resource reference value.
func (v PropertyValue) IsResourceReference() bool {
_, is := v.V.(ResourceReference)
return is
}
// TypeString returns a type representation of the property value's holder type.
func (v PropertyValue) TypeString() string {
if v.IsNull() {
return "null"
} else if v.IsBool() {
return "bool"
} else if v.IsNumber() {
return "number"
} else if v.IsString() {
return "string"
} else if v.IsArray() {
return "[]"
} else if v.IsAsset() {
return "asset"
} else if v.IsArchive() {
return "archive"
} else if v.IsObject() {
return "object"
} else if v.IsComputed() {
return "output<" + v.Input().Element.TypeString() + ">"
} else if v.IsOutput() {
if !v.OutputValue().Known {
return MakeComputed(v.OutputValue().Element).TypeString()
} else if v.OutputValue().Secret {
return MakeSecret(v.OutputValue().Element).TypeString()
}
return v.OutputValue().Element.TypeString()
} else if v.IsSecret() {
return "secret<" + v.SecretValue().Element.TypeString() + ">"
} else if v.IsResourceReference() {
ref := v.ResourceReferenceValue()
return fmt.Sprintf("resourceReference(%q, %q, %q)", ref.URN, ref.ID, ref.PackageVersion)
}
contract.Failf("Unrecognized PropertyValue type")
return ""
}
// Mappable returns a mapper-compatible value, suitable for deserialization into structures.
func (v PropertyValue) Mappable() interface{} {
return v.MapRepl(nil, nil)
}
// MapRepl returns a mapper-compatible object map, suitable for deserialization into structures. A key and/or value
// replace function, replk/replv, may be passed that will replace elements using custom logic if appropriate.
func (v PropertyValue) MapRepl(replk func(string) (string, bool),
replv func(PropertyValue) (interface{}, bool),
) interface{} {
if replv != nil {
if rv, repv := replv(v); repv {
return rv
}
}
if v.IsNull() {
return nil
} else if v.IsBool() {
return v.BoolValue()
} else if v.IsNumber() {
return v.NumberValue()
} else if v.IsString() {
return v.StringValue()
} else if v.IsArray() {
arr := []interface{}{}
for _, e := range v.ArrayValue() {
arr = append(arr, e.MapRepl(replk, replv))
}
return arr
} else if v.IsAsset() {
return v.AssetValue()
} else if v.IsArchive() {
return v.ArchiveValue()
} else if v.IsComputed() {
return v.Input()
} else if v.IsOutput() {
return v.OutputValue()
} else if v.IsSecret() {
return v.SecretValue()
} else if v.IsResourceReference() {
return v.ResourceReferenceValue()
}
contract.Assertf(v.IsObject(), "v is not Object '%v' instead", v.TypeString())
return v.ObjectValue().MapRepl(replk, replv)
}
// String implements the fmt.Stringer interface to add slightly more information to the output.
func (v PropertyValue) String() string {
if v.IsComputed() {
// For computed properties, show the type followed by an empty object string.
return fmt.Sprintf("%v{}", v.TypeString())
} else if v.IsOutput() {
if !v.OutputValue().Known {
return MakeComputed(v.OutputValue().Element).String()
} else if v.OutputValue().Secret {
return MakeSecret(v.OutputValue().Element).String()
}
return v.OutputValue().Element.String()
}
// For all others, just display the underlying property value.
return fmt.Sprintf("{%v}", v.V)
}
// Property is a pair of key and value.
type Property struct {
Key PropertyKey
Value PropertyValue
}
// SigKey is sometimes used to encode type identity inside of a map. This is required when flattening into ordinary
// maps, like we do when performing serialization, to ensure recoverability of type identities later on.
const SigKey = sig.Key
// HasSig checks to see if the given property map contains the specific signature match.
func HasSig(obj PropertyMap, match string) bool {
if sig, hassig := obj[SigKey]; hassig {
return sig.IsString() && sig.StringValue() == match
}
return false
}
// SecretSig is the unique secret signature.
const SecretSig = sig.Secret
// ResourceReferenceSig is the unique resource reference signature.
const ResourceReferenceSig = sig.ResourceReference
// OutputValueSig is the unique output value signature.
const OutputValueSig = sig.OutputValue
// IsInternalPropertyKey returns true if the given property key is an internal key that should not be displayed to
// users.
func IsInternalPropertyKey(key PropertyKey) bool {
return strings.HasPrefix(string(key), "__")
}
// 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 resource
import (
"sort"
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
)
// ObjectDiff holds the results of diffing two object property maps.
type ObjectDiff struct {
Adds PropertyMap // properties in this map are created in the new.
Deletes PropertyMap // properties in this map are deleted from the new.
Sames PropertyMap // properties in this map are the same.
Updates map[PropertyKey]ValueDiff // properties in this map are changed in the new.
}
// Added returns true if the property 'k' has been added in the new property set.
func (diff *ObjectDiff) Added(k PropertyKey) bool {
_, has := diff.Adds[k]
return has
}
// Deleted returns true if the property 'k' has been deleted from the new property set.
func (diff *ObjectDiff) Deleted(k PropertyKey) bool {
_, has := diff.Deletes[k]
return has
}
// Updated returns true if the property 'k' has been changed between new and old property sets.
func (diff *ObjectDiff) Updated(k PropertyKey) bool {
_, has := diff.Updates[k]
return has
}
// Changed returns true if the property 'k' is known to be different between old and new.
func (diff *ObjectDiff) Changed(k PropertyKey) bool {
return diff.Added(k) || diff.Deleted(k) || diff.Updated(k)
}
// Same returns true if the property 'k' is *not* known to be different; note that this isn't the same as looking up in
// the Sames map, because it is possible the key is simply missing altogether (as is the case for nulls).
func (diff *ObjectDiff) Same(k PropertyKey) bool {
return !diff.Changed(k)
}
// AnyChanges returns true if there are any changes (adds, deletes, updates) in the diff. Otherwise returns false.
func (diff *ObjectDiff) AnyChanges() bool {
return diff != nil && len(diff.Adds)+len(diff.Deletes)+len(diff.Updates) > 0
}
// Keys returns a stable snapshot of all keys known to this object, across adds, deletes, sames, and updates.
func (diff *ObjectDiff) Keys() []PropertyKey {
bufferSize := len(diff.Adds) + len(diff.Deletes) + len(diff.Sames) + len(diff.Updates)
ks := slice.Prealloc[PropertyKey](bufferSize)
for k := range diff.Adds {
ks = append(ks, k)
}
for k := range diff.Deletes {
ks = append(ks, k)
}
for k := range diff.Sames {
ks = append(ks, k)
}
for k := range diff.Updates {
ks = append(ks, k)
}
sort.Slice(ks, func(i, j int) bool { return ks[i] < ks[j] })
return ks
}
// All keys where Changed(k) = true.
func (diff *ObjectDiff) ChangedKeys() []PropertyKey {
var ks []PropertyKey
if diff != nil {
for _, k := range diff.Keys() {
if diff.Changed(k) {
ks = append(ks, k)
}
}
}
return ks
}
// ValueDiff holds the results of diffing two property values.
type ValueDiff struct {
Old PropertyValue // the old value.
New PropertyValue // the new value.
Array *ArrayDiff // the array's detailed diffs (only for arrays).
Object *ObjectDiff // the object's detailed diffs (only for objects).
}
// ArrayDiff holds the results of diffing two arrays of property values.
type ArrayDiff struct {
Adds map[int]PropertyValue // elements added in the new.
Deletes map[int]PropertyValue // elements deleted in the new.
Sames map[int]PropertyValue // elements the same in both.
Updates map[int]ValueDiff // elements that have changed in the new.
}
// Len computes the length of this array, taking into account adds, deletes, sames, and updates.
func (diff *ArrayDiff) Len() int {
length := 0
for i := range diff.Adds {
if i+1 > length {
length = i + 1
}
}
for i := range diff.Deletes {
if i+1 > length {
length = i + 1
}
}
for i := range diff.Sames {
if i+1 > length {
length = i + 1
}
}
for i := range diff.Updates {
if i+1 > length {
length = i + 1
}
}
return length
}
// IgnoreKeyFunc is the callback type for Diff's ignore option.
type IgnoreKeyFunc func(key PropertyKey) bool
// Diff returns a diffset by comparing the property map to another; it returns nil if there are no diffs.
func (props PropertyMap) Diff(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *ObjectDiff {
adds := make(PropertyMap)
deletes := make(PropertyMap)
sames := make(PropertyMap)
updates := make(map[PropertyKey]ValueDiff)
ignore := func(key PropertyKey) bool {
for _, ikf := range ignoreKeys {
if ikf(key) {
return true
}
}
return false
}
// First find any updates or deletes.
for k, old := range props {
if ignore(k) {
continue
}
if new, has := other[k]; has {
// If a new exists, use it; for output properties, however, ignore differences.
if new.IsOutput() {
sames[k] = old
} else if diff := old.Diff(new, ignoreKeys...); diff != nil {
if !old.HasValue() {
adds[k] = new
} else if !new.HasValue() {
deletes[k] = old
} else {
updates[k] = *diff
}
} else {
sames[k] = old
}
} else if old.HasValue() {
// If there was no new property, it has been deleted.
deletes[k] = old
}
}
// Next find any additions not in the old map.
for k, new := range other {
if ignore(k) {
continue
}
if _, has := props[k]; !has && new.HasValue() {
adds[k] = new
}
}
// If no diffs were found, return nil; else return a diff structure.
if len(adds) == 0 && len(deletes) == 0 && len(updates) == 0 {
return nil
}
return &ObjectDiff{
Adds: adds,
Deletes: deletes,
Sames: sames,
Updates: updates,
}
}
// Diff returns a diff by comparing a single property value to another; it returns nil if there are no diffs.
func (v PropertyValue) Diff(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *ValueDiff {
if v.IsArray() && other.IsArray() {
old := v.ArrayValue()
new := other.ArrayValue()
// If any elements exist in the new array but not the old, track them as adds.
adds := make(map[int]PropertyValue)
for i := len(old); i < len(new); i++ {
adds[i] = new[i]
}
// If any elements exist in the old array but not the new, track them as adds.
deletes := make(map[int]PropertyValue)
for i := len(new); i < len(old); i++ {
deletes[i] = old[i]
}
// Now if elements exist in both, track them as sames or updates.
sames := make(map[int]PropertyValue)
updates := make(map[int]ValueDiff)
for i := 0; i < len(old) && i < len(new); i++ {
if diff := old[i].Diff(new[i]); diff != nil {
updates[i] = *diff
} else {
sames[i] = old[i]
}
}
if len(adds) == 0 && len(deletes) == 0 && len(updates) == 0 {
return nil
}
return &ValueDiff{
Old: v,
New: other,
Array: &ArrayDiff{
Adds: adds,
Deletes: deletes,
Sames: sames,
Updates: updates,
},
}
}
if v.IsObject() && other.IsObject() {
old := v.ObjectValue()
new := other.ObjectValue()
if diff := old.Diff(new, ignoreKeys...); diff != nil {
return &ValueDiff{
Old: v,
New: other,
Object: diff,
}
}
return nil
}
// If we got here, either the values are primitives, or they weren't the same type; do a simple diff.
if v.DeepEquals(other) {
return nil
}
return &ValueDiff{Old: v, New: other}
}
// DeepEquals returns true if this property map is deeply equal to the other property map; and false otherwise.
func (props PropertyMap) DeepEquals(other PropertyMap) bool {
// If any in props either doesn't exist, or is of a different value, return false.
for _, k := range props.StableKeys() {
v := props[k]
if p, has := other[k]; has {
if !v.DeepEquals(p) {
return false
}
} else if v.HasValue() {
return false
}
}
// If the other map has properties that this map doesn't have, return false.
for _, k := range other.StableKeys() {
if _, has := props[k]; !has && other[k].HasValue() {
return false
}
}
return true
}
// DeepEquals returns true if this property map is deeply equal to the other property map; and false otherwise.
func (v PropertyValue) DeepEquals(other PropertyValue) bool {
// Arrays are equal if they are both of the same size and elements are deeply equal.
if v.IsArray() {
if !other.IsArray() {
return false
}
va := v.ArrayValue()
oa := other.ArrayValue()
if len(va) != len(oa) {
return false
}
for i, elem := range va {
if !elem.DeepEquals(oa[i]) {
return false
}
}
return true
}
// Assets and archives enjoy value equality.
if v.IsAsset() {
if !other.IsAsset() {
return false
}
return v.AssetValue().Equals(other.AssetValue())
} else if v.IsArchive() {
if !other.IsArchive() {
return false
}
return v.ArchiveValue().Equals(other.ArchiveValue())
}
// Object values are equal if their contents are deeply equal.
if v.IsObject() {
if !other.IsObject() {
return false
}
vo := v.ObjectValue()
oa := other.ObjectValue()
return vo.DeepEquals(oa)
}
// Secret are equal if the value they wrap are equal.
if v.IsSecret() {
if !other.IsSecret() {
return false
}
vs := v.SecretValue()
os := other.SecretValue()
return vs.Element.DeepEquals(os.Element)
}
// Resource references are equal if they refer to the same resource. The package version is ignored.
if v.IsResourceReference() {
if !other.IsResourceReference() {
return false
}
vr := v.ResourceReferenceValue()
or := other.ResourceReferenceValue()
if vr.URN != or.URN {
return false
}
vid, oid := vr.ID, or.ID
if vid.IsComputed() && oid.IsComputed() {
return true
}
return vid.DeepEquals(oid)
}
// Outputs are equal if each of their fields is deeply equal.
if v.IsOutput() {
if !other.IsOutput() {
return false
}
vo := v.OutputValue()
oo := other.OutputValue()
if vo.Known != oo.Known {
return false
}
if vo.Secret != oo.Secret {
return false
}
// Note that the dependencies are assumed to be sorted.
if len(vo.Dependencies) != len(oo.Dependencies) {
return false
}
for i, dep := range vo.Dependencies {
if dep != oo.Dependencies[i] {
return false
}
}
return vo.Element.DeepEquals(oo.Element)
}
if v.IsComputed() {
if !other.IsComputed() {
return false
}
vc := v.Input().Element
oc := other.Input().Element
return vc.DeepEquals(oc)
}
// For all other cases, primitives are equal if their values are equal.
return v.V == other.V
}
// DiffIncludeUnknowns returns a diffset by comparing the property map to another; it returns nil if there are no diffs.
func (props PropertyMap) DiffIncludeUnknowns(other PropertyMap, ignoreKeys ...IgnoreKeyFunc) *ObjectDiff {
adds := make(PropertyMap)
deletes := make(PropertyMap)
sames := make(PropertyMap)
updates := make(map[PropertyKey]ValueDiff)
ignore := func(key PropertyKey) bool {
for _, ikf := range ignoreKeys {
if ikf(key) {
return true
}
}
return false
}
// First find any updates or deletes.
for k, old := range props {
if ignore(k) {
continue
}
if new, has := other[k]; has {
// If a new exists, use it; for output properties, however, ignore differences.
if new.IsOutput() {
sames[k] = new
} else if diff := old.DiffIncludeUnknowns(new, ignoreKeys...); diff != nil {
if !old.HasValue() {
adds[k] = new
} else if !new.HasValue() {
deletes[k] = old
} else {
updates[k] = *diff
}
} else {
sames[k] = new
}
} else {
if old.IsComputed() {
// The old property was <computed> it probably resolved to undefined so this isn't a diff,
// but it isn't really a same either... just don't add to the diff
} else if old.HasValue() {
// If there was no new property, it has been deleted.
deletes[k] = old
}
}
}
// Next find any additions not in the old map.
for k, new := range other {
if ignore(k) {
continue
}
if _, has := props[k]; !has && new.HasValue() {
adds[k] = new
}
}
// If no diffs were found, return nil; else return a diff structure.
if len(adds) == 0 && len(deletes) == 0 && len(updates) == 0 {
return nil
}
return &ObjectDiff{
Adds: adds,
Deletes: deletes,
Sames: sames,
Updates: updates,
}
}
// Diff returns a diff by comparing a single property value to another; it returns nil if there are no diffs.
func (v PropertyValue) DiffIncludeUnknowns(other PropertyValue, ignoreKeys ...IgnoreKeyFunc) *ValueDiff {
if v.IsArray() && other.IsArray() {
old := v.ArrayValue()
new := other.ArrayValue()
// If any elements exist in the new array but not the old, track them as adds.
adds := make(map[int]PropertyValue)
for i := len(old); i < len(new); i++ {
adds[i] = new[i]
}
// If any elements exist in the old array but not the new, track them as adds.
deletes := make(map[int]PropertyValue)
for i := len(new); i < len(old); i++ {
deletes[i] = old[i]
}
// Now if elements exist in both, track them as sames or updates.
sames := make(map[int]PropertyValue)
updates := make(map[int]ValueDiff)
for i := 0; i < len(old) && i < len(new); i++ {
if diff := old[i].DiffIncludeUnknowns(new[i]); diff != nil {
updates[i] = *diff
} else {
sames[i] = new[i]
}
}
if len(adds) == 0 && len(deletes) == 0 && len(updates) == 0 {
return nil
}
return &ValueDiff{
Old: v,
New: other,
Array: &ArrayDiff{
Adds: adds,
Deletes: deletes,
Sames: sames,
Updates: updates,
},
}
}
if v.IsObject() && other.IsObject() {
old := v.ObjectValue()
new := other.ObjectValue()
if diff := old.DiffIncludeUnknowns(new, ignoreKeys...); diff != nil {
return &ValueDiff{
Old: v,
New: other,
Object: diff,
}
}
return nil
}
// If we got here, either the values are primitives, or they weren't the same type; do a simple diff.
if v.DeepEqualsIncludeUnknowns(other) {
return nil
}
return &ValueDiff{Old: v, New: other}
}
func (props PropertyMap) DeepEqualsIncludeUnknowns(other PropertyMap) bool {
// If any in props either doesn't exist, or is of a different value, return false.
for _, k := range props.StableKeys() {
v := props[k]
if p, has := other[k]; has {
if !v.DeepEqualsIncludeUnknowns(p) {
return false
}
} else if v.HasValue() && !v.IsComputed() {
return false
}
}
// If the other map has properties that this map doesn't have, return false.
for _, k := range other.StableKeys() {
if _, has := props[k]; !has && other[k].HasValue() {
return false
}
}
return true
}
func (v PropertyValue) DeepEqualsIncludeUnknowns(other PropertyValue) bool {
// Anything is equal to a computed
if v.IsComputed() || other.IsComputed() {
return true
}
// Arrays are equal if they are both of the same size and elements are deeply equal.
if v.IsArray() {
if !other.IsArray() {
return false
}
va := v.ArrayValue()
oa := other.ArrayValue()
if len(va) != len(oa) {
return false
}
for i, elem := range va {
if !elem.DeepEqualsIncludeUnknowns(oa[i]) {
return false
}
}
return true
}
// Assets and archives enjoy value equality.
if v.IsAsset() {
if !other.IsAsset() {
return false
}
return v.AssetValue().Equals(other.AssetValue())
} else if v.IsArchive() {
if !other.IsArchive() {
return false
}
return v.ArchiveValue().Equals(other.ArchiveValue())
}
// Object values are equal if their contents are deeply equal.
if v.IsObject() {
if !other.IsObject() {
return false
}
vo := v.ObjectValue()
oa := other.ObjectValue()
return vo.DeepEqualsIncludeUnknowns(oa)
}
// Secret are equal if the value they wrap are equal.
if v.IsSecret() {
if !other.IsSecret() {
return false
}
vs := v.SecretValue()
os := other.SecretValue()
return vs.Element.DeepEqualsIncludeUnknowns(os.Element)
}
// Resource references are equal if they refer to the same resource. The package version is ignored.
if v.IsResourceReference() {
if !other.IsResourceReference() {
return false
}
vr := v.ResourceReferenceValue()
or := other.ResourceReferenceValue()
return vr.Equal(or)
}
// Outputs are equal if each of their fields is deeply equal.
if v.IsOutput() {
if !other.IsOutput() {
return false
}
vo := v.OutputValue()
oo := other.OutputValue()
if vo.Known != oo.Known {
return false
}
if vo.Secret != oo.Secret {
return false
}
// Note that the dependencies are assumed to be sorted.
if len(vo.Dependencies) != len(oo.Dependencies) {
return false
}
for i, dep := range vo.Dependencies {
if dep != oo.Dependencies[i] {
return false
}
}
return vo.Element.DeepEqualsIncludeUnknowns(oo.Element)
}
// For all other cases, primitives are equal if their values are equal.
return v.V == other.V
}
// Copyright 2019-2024, 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 resource
import (
"bytes"
"errors"
"fmt"
"strconv"
"strings"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
)
// PropertyPath represents a path to a nested property. The path may be composed of strings (which access properties
// in ObjectProperty values) and integers (which access elements of ArrayProperty values).
type PropertyPath []interface{}
// ParsePropertyPath parses a property path into a PropertyPath value.
//
// A property path string is essentially a Javascript property access expression in which all elements are literals.
// Valid property paths obey the following EBNF-ish grammar:
//
// propertyName := [a-zA-Z_$] { [a-zA-Z0-9_$] }
// quotedPropertyName := '"' ( '\' '"' | [^"] ) { ( '\' '"' | [^"] ) } '"'
// arrayIndex := { [0-9] }
//
// propertyIndex := '[' ( quotedPropertyName | arrayIndex ) ']'
// rootProperty := ( propertyName | propertyIndex )
// propertyAccessor := ( ( '.' propertyName ) | propertyIndex )
// path := rootProperty { propertyAccessor }
//
// Examples of valid paths:
// - root
// - root.nested
// - root["nested"]
// - root.double.nest
// - root["double"].nest
// - root["double"]["nest"]
// - root.array[0]
// - root.array[100]
// - root.array[0].nested
// - root.array[0][1].nested
// - root.nested.array[0].double[1]
// - root["key with \"escaped\" quotes"]
// - root["key with a ."]
// - ["root key with \"escaped\" quotes"].nested
// - ["root key with a ."][100]
// - root.array[*].field
// - root.array["*"].field
func parsePropertyPath(path string, strict bool) (PropertyPath, error) {
// We interpret the grammar above a little loosely in order to keep things simple. Specifically, we will accept
// something close to the following:
// pathElement := { '.' } [a-zA-Z_$][a-zA-Z0-9_$]
// pathIndex := '[' ( [0-9]+ | '"' ('\' '"' | [^"] )+ '"' ']'
// path := { pathElement | pathIndex }
var elements []interface{}
if len(path) > 0 && path[0] == '.' {
return nil, errors.New("expected property path to start with a name or index")
}
for len(path) > 0 {
switch path[0] {
case '.':
path = path[1:]
if len(path) == 0 {
return nil, errors.New("expected property path to end with a name or index")
}
if path[0] == '[' && strict {
return nil, errors.New("expected property name after '.'")
} else if path[0] == '[' {
// We tolerate a '.' followed by a '[', which is not strictly legal, but is common from old providers.
logging.V(10).Infof("property path '%s' contains a '.' followed by a '['; this is not strictly legal", path)
}
case '[':
// If the character following the '[' is a '"', parse a string key.
var pathElement interface{}
if len(path) > 1 && path[1] == '"' {
var propertyKey []byte
var i int
for i = 2; ; {
if i >= len(path) {
return nil, errors.New("missing closing quote in property name")
} else if path[i] == '"' {
i++
break
} else if path[i] == '\\' && i+1 < len(path) && path[i+1] == '"' {
propertyKey = append(propertyKey, '"')
i += 2
} else {
propertyKey = append(propertyKey, path[i])
i++
}
}
if i >= len(path) || path[i] != ']' {
return nil, errors.New("missing closing bracket in property access")
}
pathElement, path = string(propertyKey), path[i:]
} else {
// Look for a closing ']'
rbracket := strings.IndexRune(path, ']')
if rbracket == -1 {
return nil, errors.New("missing closing bracket in array index")
}
segment := path[1:rbracket]
if segment == "*" {
pathElement, path = "*", path[rbracket:]
} else {
index, err := strconv.ParseInt(segment, 10, 0)
if err != nil {
return nil, fmt.Errorf("invalid array index: %w", err)
}
pathElement, path = int(index), path[rbracket:]
}
}
elements, path = append(elements, pathElement), path[1:]
default:
for i := 0; ; i++ {
if i == len(path) || path[i] == '.' || path[i] == '[' {
elements, path = append(elements, path[:i]), path[i:]
break
}
}
}
}
return PropertyPath(elements), nil
}
func ParsePropertyPath(path string) (PropertyPath, error) {
return parsePropertyPath(path, false)
}
func ParsePropertyPathStrict(path string) (PropertyPath, error) {
return parsePropertyPath(path, true)
}
// Get attempts to get the value located by the PropertyPath inside the given PropertyValue. If any component of the
// path does not exist, this function will return (NullPropertyValue, false).
func (p PropertyPath) Get(v PropertyValue) (PropertyValue, bool) {
for _, key := range p {
switch {
case v.IsArray():
index, ok := key.(int)
if !ok || index < 0 || index >= len(v.ArrayValue()) {
return PropertyValue{}, false
}
v = v.ArrayValue()[index]
case v.IsObject():
k, ok := key.(string)
if !ok {
return PropertyValue{}, false
}
v, ok = v.ObjectValue()[PropertyKey(k)]
if !ok {
return PropertyValue{}, false
}
default:
return PropertyValue{}, false
}
}
return v, true
}
// Set attempts to set the location inside a PropertyValue indicated by the PropertyPath to the given value. If any
// component of the path besides the last component does not exist, this function will return false.
func (p PropertyPath) Set(dest, v PropertyValue) bool {
if len(p) == 0 {
return false
}
dest, ok := p[:len(p)-1].Get(dest)
if !ok {
return false
}
key := p[len(p)-1]
switch {
case dest.IsArray():
index, ok := key.(int)
if !ok || index < 0 || index >= len(dest.ArrayValue()) {
return false
}
dest.ArrayValue()[index] = v
case dest.IsObject():
k, ok := key.(string)
if !ok {
return false
}
dest.ObjectValue()[PropertyKey(k)] = v
default:
return false
}
return true
}
// Add sets the location inside a PropertyValue indicated by the PropertyPath to the given value. Any components
// referred to by the path that do not exist will be created. If there is a mismatch between the type of an existing
// component and a key that traverses that component, this function will return false. If the destination is a null
// property value, this function will create and return a new property value.
func (p PropertyPath) Add(dest, v PropertyValue) (PropertyValue, bool) {
if len(p) == 0 {
return PropertyValue{}, false
}
// set sets the destination referred to by the last element of the path to the given value.
rv := dest
set := func(v PropertyValue) {
dest, rv = v, v
}
for _, key := range p {
switch key := key.(type) {
case int:
// This key is an int, so we expect an array.
switch {
case dest.IsNull():
// If the destination array does not exist, create a new array with enough room to store the value at
// the requested index.
dest = NewProperty(make([]PropertyValue, key+1))
set(dest)
case dest.IsArray():
// If the destination array does exist, ensure that it is large enough to accommodate the requested
// index.
if arr := dest.ArrayValue(); key >= len(arr) {
dest = NewProperty(append(make([]PropertyValue, key+1-len(arr)), arr...))
set(dest)
}
default:
return PropertyValue{}, false
}
destV := dest.ArrayValue()
set = func(v PropertyValue) {
destV[key] = v
}
dest = destV[key]
case string:
// This key is a string, so we expect an object.
switch {
case dest.IsNull():
// If the destination does not exist, create a new object.
dest = NewProperty(PropertyMap{})
set(dest)
case dest.IsObject():
// OK
default:
return PropertyValue{}, false
}
destV := dest.ObjectValue()
set = func(v PropertyValue) {
destV[PropertyKey(key)] = v
}
dest = destV[PropertyKey(key)]
default:
return PropertyValue{}, false
}
}
set(v)
return rv, true
}
// Delete attempts to delete the value located by the PropertyPath inside the given PropertyValue. If any component
// of the path does not exist, this function will return false.
func (p PropertyPath) Delete(dest PropertyValue) bool {
if len(p) == 0 {
return false
}
dest, ok := p[:len(p)-1].Get(dest)
if !ok {
return false
}
key := p[len(p)-1]
switch {
case dest.IsArray():
index, ok := key.(int)
if !ok || index < 0 || index >= len(dest.ArrayValue()) {
return false
}
dest.ArrayValue()[index] = PropertyValue{}
case dest.IsObject():
k, ok := key.(string)
if !ok {
return false
}
delete(dest.ObjectValue(), PropertyKey(k))
default:
return false
}
return true
}
// Contains returns true if the receiver property path contains the other property path.
// For example, the path `foo["bar"][1]` contains the path `foo.bar[1].baz`. The key `"*"`
// is a wildcard which matches any string or int index at that same nesting level. So for example,
// the path `foo.*.baz` contains `foo.bar.baz.bam`, and the path `*` contains any path.
func (p PropertyPath) Contains(other PropertyPath) bool {
if len(other) < len(p) {
return false
}
for i := range p {
pp := p[i]
otherp := other[i]
switch pp := pp.(type) {
case int:
if otherpi, ok := otherp.(int); !ok || otherpi != pp {
return false
}
case string:
if pp == "*" {
continue
}
if otherps, ok := otherp.(string); !ok || otherps != pp {
return false
}
default:
// Invalid path, return false
return false
}
}
return true
}
// unwrapSecrets recursively unwraps any secrets from the given PropertyValue returning true if any secrets were
// unwrapped.
func unwrapSecrets(v PropertyValue) (PropertyValue, bool) {
if v.IsSecret() {
inner, _ := unwrapSecrets(v.SecretValue().Element)
return inner, true
}
return v, false
}
func (p PropertyPath) reset(old, new PropertyValue, oldIsSecret, newIsSecret bool) bool {
if len(p) == 0 {
return false
}
// Unwrap any secrets from old & new, we can just go through them for this traversal.
old, isSecret := unwrapSecrets(old)
oldIsSecret = oldIsSecret || isSecret
new, isSecret = unwrapSecrets(new)
newIsSecret = newIsSecret || isSecret
// If this is the last component we want to do the reset, else we want to search for the next component.
key := p[0]
switch key := key.(type) {
case int:
// An index < 0 is always a path error, even for empty arrays or objects
if key < 0 {
return false
}
// This is a leaf path element, so we want to reset the value at this index in new to the value at this index from old
if len(p) == 1 {
if !old.IsArray() && !new.IsArray() {
// Neither old nor new are arrays, so we can't reset this index
return true
} else if !old.IsArray() || !new.IsArray() {
// One of old or new is an array but the other isn't, so this is a path error
return false
}
// If neither array contains this index then this is a _same_ and so ok, e.g. given old:[1, 2] and
// new:[1] and a path of [3] we can return true because new at [3] is the same as old at [3], it
// doesn't exist.
if key >= len(old.ArrayValue()) && key >= len(new.ArrayValue()) {
return true
}
// If one array has this index but the other doesn't this is a path failure because we can't
// remove a location from an array.
if key >= len(old.ArrayValue()) || key >= len(new.ArrayValue()) {
return false
}
// Otherwise both arrays contain this index and we can reset the value of it in new to what is in
// old.
v := old.ArrayValue()[key]
// If this was a secret value in old, but new isn't currently a secret context then we need to mark this
// reset value as secret.
if oldIsSecret && !newIsSecret {
v = MakeSecret(v)
}
new.ArrayValue()[key] = v
return true
}
if !old.IsArray() || !new.IsArray() {
// At least one of old or new is not an array, so we can't keep searching along this path but
// we only return an error if both are not arrays.
return !old.IsArray() && !new.IsArray()
}
// If this index is out of bounds in either array then this is a path failure because we can't
// continue the search of this path down each PropertyValue.
if key >= len(old.ArrayValue()) || key >= len(new.ArrayValue()) {
return false
}
old = old.ArrayValue()[key]
new = new.ArrayValue()[key]
return p[1:].reset(old, new, oldIsSecret, newIsSecret)
case string:
if key == "*" {
if len(p) == 1 {
if new.IsObject() {
if old.IsObject() {
for k := range old.ObjectValue() {
v := old.ObjectValue()[k]
// If this was a secret value in old, but new isn't currently a secret context then we need
// to mark this reset value as secret.
if oldIsSecret && !newIsSecret {
v = MakeSecret(v)
}
new.ObjectValue()[k] = v
}
for k := range new.ObjectValue() {
if _, has := old.ObjectValue()[k]; !has {
delete(new.ObjectValue(), k)
}
}
}
return true
} else if new.IsArray() {
if old.IsArray() {
oldArray := old.ArrayValue()
newArray := new.ArrayValue()
for i := range oldArray {
if i >= len(newArray) {
break
}
v := oldArray[i]
// If this was a secret value in old, but new isn't currently a secret context then we need
// to mark this reset value as secret.
if oldIsSecret && !newIsSecret {
v = MakeSecret(v)
}
newArray[i] = v
}
// If arrays are of different length then this is a path failure because we can't
// synchronise the two values.
if len(oldArray) != len(newArray) {
return false
}
}
return true
}
return false
}
if old.IsObject() && new.IsObject() {
oldObject := old.ObjectValue()
newObject := new.ObjectValue()
for k := range oldObject {
var hasOld, hasNew bool
oldValue, hasOld := oldObject[k]
newValue, hasNew := newObject[k]
if !hasOld || !hasNew {
return false
}
if !p[1:].reset(oldValue, newValue, oldIsSecret, newIsSecret) {
return false
}
}
return true
} else if old.IsArray() && new.IsArray() {
oldArray := old.ArrayValue()
newArray := new.ArrayValue()
for i := range oldArray {
if i >= len(newArray) {
break
}
if !p[1:].reset(oldArray[i], newArray[i], oldIsSecret, newIsSecret) {
return false
}
}
// If arrays are of different length then this is a path failure because we can't
// continue the search of this path down each PropertyValue.
if len(oldArray) != len(newArray) {
return false
}
return true
}
return false
}
pkey := PropertyKey(key)
if len(p) == 1 {
// This is the leaf path entry, so we want to reset this property in new to it's value in old.
// Firstly if old doesn't have this key (either because it isn't an object or because it
// doesn't have the property) then we want to delete this from new.
var v PropertyValue
var has bool
if old.IsObject() {
v, has = old.ObjectValue()[pkey]
}
if has {
// If this path exists in old but new isn't an object than return a path error
if !new.IsObject() {
return false
}
// Else simply overwrite the value in new with the value from old, if this was a secret value in
// old, but new isn't currently a secret context then we need to mark this reset value as secret.
if oldIsSecret && !newIsSecret {
v = MakeSecret(v)
}
new.ObjectValue()[pkey] = v
} else {
// If the path doesn't exist in old then we want to delete it from new, but if new isn't
// an object then we can just do nothing we don't consider this a path error. e.g. given
// old:{} and new:1 and a path of "a" we can return true because ["a"] in both is the
// same (it doesn't exist).
if new.IsObject() {
delete(new.ObjectValue(), pkey)
}
}
return true
}
if !old.IsObject() || !new.IsObject() {
// At least one of old or new is not an object, so we can't keep searching along this path but
// we only return an error if both are not objects.
return !old.IsObject() && !new.IsObject()
}
new, hasNew := new.ObjectValue()[pkey]
old, hasOld := old.ObjectValue()[pkey]
if hasOld && !hasNew {
// Old has this key but new doesn't, but we still searching for the leaf item to set so this
// is a path error.
return false
}
if !hasOld && !hasNew {
// Neither value contain this path, so we're done.
return true
}
return p[1:].reset(old, new, oldIsSecret, newIsSecret)
}
contract.Failf("Invalid property path component type: %T", key)
return true
}
// Reset attempts to reset the values located by the PropertyPath inside the given new PropertyMap to the
// values from the same location in the old PropertyMap. Reset behaves likes Set in that it will not create
// intermediate locations, it also won't create or delete array locations (because that would change the size
// of the array).
func (p PropertyPath) Reset(old, new PropertyMap) bool {
return p.reset(NewProperty(old), NewProperty(new), false, false)
}
func requiresQuote(c rune) bool {
return (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '_'
}
func (p PropertyPath) String() string {
var buf bytes.Buffer
for i, k := range p {
switch k := k.(type) {
case string:
var keyBuf bytes.Buffer
quoted := false
for _, c := range k {
if requiresQuote(c) {
quoted = true
if c == '"' {
keyBuf.WriteByte('\\')
}
}
keyBuf.WriteRune(c)
}
if !quoted {
if i == 0 {
fmt.Fprintf(&buf, "%s", keyBuf.String())
} else {
fmt.Fprintf(&buf, ".%s", keyBuf.String())
}
} else {
fmt.Fprintf(&buf, `["%s"]`, keyBuf.String())
}
case int:
fmt.Fprintf(&buf, "[%d]", k)
}
}
return buf.String()
}
// Copyright 2016-2024, 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 resource
import (
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/property"
)
// Translate a [property.Map] into a [PropertyMap].
//
// This is a lossless transition, such that this will be true:
//
// FromResourcePropertyMap(ToResourcePropertyMap(m)).Equals(m)
func ToResourcePropertyMap(v property.Map) PropertyMap {
vMap := v.AsMap()
rMap := make(PropertyMap, len(vMap))
for k, vElem := range vMap {
rMap[PropertyKey(k)] = ToResourcePropertyValue(vElem)
}
return rMap
}
// Translate a Value into a PropertyValue.
//
// This is a lossless transition, such that this will be true:
//
// FromResourcePropertyValue(ToResourcePropertyValue(v)).Equals(v)
func ToResourcePropertyValue(v property.Value) PropertyValue {
var r PropertyValue
switch {
case v.IsBool():
r = NewProperty(v.AsBool())
case v.IsNumber():
r = NewProperty(v.AsNumber())
case v.IsString():
r = NewProperty(v.AsString())
case v.IsArray():
vArr := v.AsArray().AsSlice()
arr := make([]PropertyValue, len(vArr))
for i, vElem := range vArr {
arr[i] = ToResourcePropertyValue(vElem)
}
r = NewProperty(arr)
case v.IsMap():
r = NewProperty(ToResourcePropertyMap(v.AsMap()))
case v.IsAsset():
r = NewProperty(v.AsAsset())
case v.IsArchive():
r = NewProperty(v.AsArchive())
case v.IsResourceReference():
ref := v.AsResourceReference()
r = NewProperty(ResourceReference{
URN: ref.URN,
ID: ToResourcePropertyValue(ref.ID),
PackageVersion: ref.PackageVersion,
})
case v.IsNull():
r = NewNullProperty()
}
switch {
case len(v.Dependencies()) > 0 || (v.Secret() && v.IsComputed()):
r = NewProperty(Output{
Element: r,
Known: !v.IsComputed(),
Secret: v.Secret(),
Dependencies: v.Dependencies(),
})
case v.Secret():
r = MakeSecret(r)
case v.IsComputed():
r = MakeComputed(NewProperty(""))
}
return r
}
// Translate a [PropertyValue] into a [property.Value].
//
// This is a normalizing transition, such that the last expression will be true:
//
// normalized := ToResourcePropertyMap(FromResourcePropertyMap(m))
// normalized.DeepEquals(ToResourcePropertyMap(FromResourcePropertyMap(m)))
func FromResourcePropertyMap(v PropertyMap) property.Map {
rMap := make(map[string]property.Value, len(v))
for k, v := range v {
rMap[string(k)] = FromResourcePropertyValue(v)
}
return property.NewMap(rMap)
}
// Translate a PropertyValue into a Value.
//
// This is a normalizing transition, such that the last expression will be true:
//
// normalized := ToResourcePropertyValue(FromResourcePropertyValue(v))
// normalized.DeepEquals(ToResourcePropertyValue(FromResourcePropertyValue(v)))
func FromResourcePropertyValue(v PropertyValue) property.Value {
switch {
// Value types
case v.IsBool():
return property.New(v.BoolValue())
case v.IsNumber():
return property.New(v.NumberValue())
case v.IsString():
return property.New(v.StringValue())
case v.IsArray():
vArr := v.ArrayValue()
arr := make([]property.Value, len(vArr))
for i, v := range vArr {
arr[i] = FromResourcePropertyValue(v)
}
return property.New(arr)
case v.IsObject():
return property.New(FromResourcePropertyMap(v.ObjectValue()))
case v.IsAsset():
return property.New(v.AssetValue())
case v.IsArchive():
return property.New(v.ArchiveValue())
case v.IsResourceReference():
r := v.ResourceReferenceValue()
return property.New(property.ResourceReference{
URN: r.URN,
ID: FromResourcePropertyValue(r.ID),
PackageVersion: r.PackageVersion,
})
case v.IsNull():
return property.Value{}
// Flavor types
case v.IsComputed():
return property.New(property.Computed).WithSecret(
v.Input().Element.IsSecret() ||
(v.Input().Element.IsOutput() && v.Input().Element.OutputValue().Secret))
case v.IsSecret():
return FromResourcePropertyValue(v.SecretValue().Element).WithSecret(true)
case v.IsOutput():
o := v.OutputValue()
var elem property.Value
if !o.Known {
elem = property.New(property.Computed)
} else {
elem = FromResourcePropertyValue(o.Element)
}
// If the value is already secret, we leave it secret, otherwise we take
// the value from Output.
if o.Secret {
elem = elem.WithSecret(true)
}
return elem.WithDependencies(o.Dependencies)
default:
contract.Failf("Unknown property value type %T", v.V)
return property.Value{}
}
}
// 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 resource
import (
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
)
// Goal is a desired state for a resource object. Normally it represents a subset of the resource's state expressed by
// a program, however if Output is true, it represents a more complete, post-deployment view of the state.
type Goal struct {
Type tokens.Type // the type of resource.
Name string // the name for the resource's URN.
Custom bool // true if this resource is custom, managed by a plugin.
Properties PropertyMap // the resource's property state.
Parent URN // an optional parent URN for this resource.
Protect *bool // true to protect this resource from deletion.
Dependencies []URN // dependencies of this resource object.
Provider string // the provider to use for this resource.
InitErrors []string // errors encountered as we attempted to initialize the resource.
PropertyDependencies map[PropertyKey][]URN // the set of dependencies that affect each property.
DeleteBeforeReplace *bool // true if this resource should be deleted prior to replacement.
IgnoreChanges []string // a list of property paths to ignore when diffing.
AdditionalSecretOutputs []PropertyKey // outputs that should always be treated as secrets.
Aliases []Alias // additional structured Aliases that should be assigned.
ID ID // the expected ID of the resource, if any.
CustomTimeouts CustomTimeouts // an optional config object for resource options
ReplaceOnChanges []string // a list of property paths that if changed should force a replacement.
// if set to True, the providers Delete method will not be called for this resource.
RetainOnDelete *bool
// if set, the providers Delete method will not be called for this resource
// if specified resource is being deleted as well.
DeletedWith URN
SourcePosition string // If set, the source location of the resource registration
StackTrace []StackFrame // If set, the stack trace at time of registration
ResourceHooks map[HookType][]string // The resource hooks attached to the resource, by type.
}
// NewGoal is used to construct Goal values. The dataflow for Goal is rather sensitive, so all fields are required.
// Call [NewGoal.Make] to create the *Goal value.
type NewGoal struct {
// the type of resource.
Type tokens.Type // required
// the name for the resource's URN.
Name string // required
// true if this resource is custom, managed by a plugin.
Custom bool // required
// the resource's property state.
Properties PropertyMap // required
// an optional parent URN for this resource.
Parent URN // required
// true to protect this resource from deletion.
Protect *bool // required
// dependencies of this resource object.
Dependencies []URN // required
// the provider to use for this resource.
Provider string // required
// errors encountered as we attempted to initialize the resource.
InitErrors []string // required
// the set of dependencies that affect each property.
PropertyDependencies map[PropertyKey][]URN // required
// true if this resource should be deleted prior to replacement.
DeleteBeforeReplace *bool // required
// a list of property paths to ignore when diffing.
IgnoreChanges []string // required
// outputs that should always be treated as secrets.
AdditionalSecretOutputs []PropertyKey // required
// additional structured Aliases that should be assigned.
Aliases []Alias // required
// the expected ID of the resource, if any.
ID ID // required
// an optional config object for resource options
CustomTimeouts *CustomTimeouts // required
// a list of property paths that if changed should force a replacement.
ReplaceOnChanges []string // required
// if set to True, the providers Delete method will not be called for this resource.
// required
RetainOnDelete *bool // required
// if set, the providers Delete method will not be called for this resource
// if specified resource is being deleted as well.
DeletedWith URN // required
// If set, the source location of the resource registration
SourcePosition string // required
// If set, the stack trace at time of registration
StackTrace []StackFrame // required
// The resource hooks attached to the resource, by type.
ResourceHooks map[HookType][]string // required
}
// Make consumes the NewGoal to create a *Goal.
func (g NewGoal) Make() *Goal {
var customTimeouts CustomTimeouts
if g.CustomTimeouts != nil {
customTimeouts = *g.CustomTimeouts
}
return &Goal{
Type: g.Type,
Name: g.Name,
Custom: g.Custom,
Properties: g.Properties,
Parent: g.Parent,
Protect: g.Protect,
Dependencies: g.Dependencies,
Provider: g.Provider,
InitErrors: g.InitErrors,
PropertyDependencies: g.PropertyDependencies,
DeleteBeforeReplace: g.DeleteBeforeReplace,
IgnoreChanges: g.IgnoreChanges,
AdditionalSecretOutputs: g.AdditionalSecretOutputs,
Aliases: g.Aliases,
ID: g.ID,
CustomTimeouts: customTimeouts,
ReplaceOnChanges: g.ReplaceOnChanges,
RetainOnDelete: g.RetainOnDelete,
DeletedWith: g.DeletedWith,
SourcePosition: g.SourcePosition,
StackTrace: g.StackTrace,
ResourceHooks: g.ResourceHooks,
}
}
// 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 resource
import (
"crypto"
cryptorand "crypto/rand"
"encoding/hex"
"fmt"
"lukechampine.com/frand"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
// ID is a unique resource identifier; it is managed by the provider and is mostly opaque.
type ID string
// String converts a resource ID into a string.
func (id ID) String() string {
return string(id)
}
// StringPtr converts an optional ID into an optional string.
func (id *ID) StringPtr() *string {
if id == nil {
return nil
}
ids := (*id).String()
return &ids
}
// IDStrings turns an array of resource IDs into an array of strings.
func IDStrings(ids []ID) []string {
ss := make([]string, len(ids))
for i, id := range ids {
ss[i] = id.String()
}
return ss
}
// MaybeID turns an optional string into an optional resource ID.
func MaybeID(s *string) *ID {
var ret *ID
if s != nil {
id := ID(*s)
ret = &id
}
return ret
}
// NewUniqueHex generates a new "random" hex string for use by resource providers. It will take the optional prefix
// and append randlen random characters (defaulting to 8 if not > 0). The result must not exceed maxlen total
// characterss (if > 0). Note that capping to maxlen necessarily increases the risk of collisions.
func NewUniqueHex(prefix string, randlen, maxlen int) (string, error) {
if randlen <= 0 {
randlen = 8
}
if maxlen > 0 && len(prefix)+randlen > maxlen {
return "", fmt.Errorf(
"name '%s' plus %d random chars is longer than maximum length %d", prefix, randlen, maxlen)
}
bs := make([]byte, (randlen+1)/2)
n, err := cryptorand.Read(bs)
contract.AssertNoErrorf(err, "error generating random bytes")
contract.Assertf(n == len(bs), "generated fewer bytes (%d) than requested (%d)", n, len(bs))
return prefix + hex.EncodeToString(bs)[:randlen], nil
}
// NewUniqueHexID generates a new "random" hex string for use by resource providers. It will take the optional prefix
// and append randlen random characters (defaulting to 8 if not > 0). The result must not exceed maxlen total
// characterss (if > 0). Note that capping to maxlen necessarily increases the risk of collisions.
func NewUniqueHexID(prefix string, randlen, maxlen int) (ID, error) {
u, err := NewUniqueHex(prefix, randlen, maxlen)
return ID(u), err
}
// NewUniqueName generates a new "random" string primarily intended for use by resource providers for
// autonames. It will take the optional prefix and append randlen random characters (defaulting to 8 if not >
// 0). The result must not exceed maxlen total characters (if > 0). The characters that make up the random
// suffix can be set via charset, and will default to [a-f0-9]. Note that capping to maxlen necessarily
// increases the risk of collisions. The randomness for this method is a function of randomSeed if given, else
// it falls back to a non-deterministic source of randomness.
func NewUniqueName(randomSeed []byte, prefix string, randlen, maxlen int, charset []rune) (string, error) {
if randlen <= 0 {
randlen = 8
}
if maxlen > 0 && len(prefix)+randlen > maxlen {
return "", fmt.Errorf(
"name '%s' plus %d random chars is longer than maximum length %d", prefix, randlen, maxlen)
}
if charset == nil {
charset = []rune("0123456789abcdef")
}
var random *frand.RNG
if len(randomSeed) == 0 {
random = frand.New()
} else {
// frand.NewCustom needs a 32 byte seed. Take the SHA256 hash of whatever bytes we've been given as a
// seed and pass the 32 byte result of that to frand.
hash := crypto.SHA256.New()
hash.Write(randomSeed)
seed := hash.Sum(nil)
bufsize := 1024 // Same bufsize as used by frand.New.
rounds := 12 // Same rounds as used by frand.New.
random = frand.NewCustom(seed, bufsize, rounds)
}
randomSuffix := make([]rune, randlen)
for i := range randomSuffix {
randomSuffix[i] = charset[random.Intn(len(charset))]
}
return prefix + string(randomSuffix), nil
}
// 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 resource
// OperationType is the type of operations issued by the engine.
type OperationType string
const (
// OperationTypeCreating is the state of resources that are being created.
OperationTypeCreating OperationType = "creating"
// OperationTypeUpdating is the state of resources that are being updated.
OperationTypeUpdating OperationType = "updating"
// OperationTypeDeleting is the state of resources that are being deleted.
OperationTypeDeleting OperationType = "deleting"
// OperationTypeReading is the state of resources that are being read.
OperationTypeReading OperationType = "reading"
// OperationTypeImporting is the state of resources that are being imported.
OperationTypeImporting OperationType = "importing"
)
// Operation represents an operation that the engine has initiated but has not yet completed. It is
// essentially just a tuple of a resource and a string identifying the operation.
type Operation struct {
Resource *State
Type OperationType
}
// NewOperation constructs a new Operation from a state and an operation name.
func NewOperation(state *State, op OperationType) Operation {
return Operation{state, op}
}
func (op Operation) Copy() Operation {
return Operation{
Resource: op.Resource.Copy(),
Type: op.Type,
}
}
// Copyright 2016-2024, 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 resource
import (
"sync"
"time"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
// State is a structure containing state associated with a resource. This resource may have been serialized and
// deserialized, or snapshotted from a live graph of resource objects. The value's state is not, however, associated
// with any runtime objects in memory that may be actively involved in ongoing computations.
//
// Only test code should create State values directly. All other code should use [NewState].
//
//nolint:lll
type State struct {
// Currently the engine implements RegisterResourceOutputs by directly mutating the state to change the `Outputs`. This
// triggers a race between the snapshot serialization code and the engine. Ideally we'd do a more principled fix, but
// just locking in these two places is sufficient to stop the race detector from firing on integration tests.
Lock sync.Mutex
Type tokens.Type // the resource's type.
URN URN // the resource's object urn, a human-friendly, unique name for the resource.
Custom bool // true if the resource is custom, managed by a plugin.
Delete bool // true if this resource is pending deletion due to a replacement.
ID ID // the resource's unique ID, assigned by the resource provider (or blank if none/uncreated).
Inputs PropertyMap // the resource's input properties (as specified by the program).
Outputs PropertyMap // the resource's complete output state (as returned by the resource provider).
Parent URN // an optional parent URN that this resource belongs to.
Protect bool // true to "protect" this resource (protected resources cannot be deleted).
Taint bool // true to force replacement of this resource during the next update.
External bool // true if this resource is "external" to Pulumi and we don't control the lifecycle.
Dependencies []URN // the resource's dependencies.
InitErrors []string // the set of errors encountered in the process of initializing resource.
Provider string // the provider to use for this resource.
PropertyDependencies map[PropertyKey][]URN // the set of dependencies that affect each property.
PendingReplacement bool // true if this resource was deleted and is awaiting replacement.
AdditionalSecretOutputs []PropertyKey // an additional set of outputs that should be treated as secrets.
Aliases []URN // an optional set of URNs for which this resource is an alias.
CustomTimeouts CustomTimeouts // A config block that will be used to configure timeouts for CRUD operations.
ImportID ID // the resource's import id, if this was an imported resource.
RetainOnDelete bool // if set to True, the providers Delete method will not be called for this resource.
DeletedWith URN // If set, the providers Delete method will not be called for this resource if specified resource is being deleted as well.
Created *time.Time // If set, the time when the state was initially added to the state file. (i.e. Create, Import)
Modified *time.Time // If set, the time when the state was last modified in the state file.
SourcePosition string // If set, the source location of the resource registration
StackTrace []StackFrame // If set, the stack trace at time of registration
IgnoreChanges []string // If set, the list of properties to ignore changes for.
ReplaceOnChanges []string // If set, the list of properties that if changed trigger a replace.
RefreshBeforeUpdate bool // true if this resource should always be refreshed prior to updates.
ViewOf URN // If set, the URN of the resource this resource is a view of.
ResourceHooks map[HookType][]string // The resource hooks attached to the resource, by type.
}
// Copy creates a deep copy of the resource state, except without copying the lock.
func (s *State) Copy() *State {
return &State{
Type: s.Type,
URN: s.URN,
Custom: s.Custom,
Delete: s.Delete,
ID: s.ID,
Inputs: s.Inputs,
Outputs: s.Outputs,
Parent: s.Parent,
Protect: s.Protect,
Taint: s.Taint,
External: s.External,
Dependencies: s.Dependencies,
InitErrors: s.InitErrors,
Provider: s.Provider,
PropertyDependencies: s.PropertyDependencies,
PendingReplacement: s.PendingReplacement,
AdditionalSecretOutputs: s.AdditionalSecretOutputs,
Aliases: s.Aliases,
CustomTimeouts: s.CustomTimeouts,
ImportID: s.ImportID,
RetainOnDelete: s.RetainOnDelete,
DeletedWith: s.DeletedWith,
Created: s.Created,
Modified: s.Modified,
SourcePosition: s.SourcePosition,
StackTrace: s.StackTrace,
IgnoreChanges: s.IgnoreChanges,
ReplaceOnChanges: s.ReplaceOnChanges,
RefreshBeforeUpdate: s.RefreshBeforeUpdate,
ViewOf: s.ViewOf,
ResourceHooks: s.ResourceHooks,
}
}
func (s *State) GetAliasURNs() []URN {
return s.Aliases
}
func (s *State) GetAliases() []Alias {
aliases := make([]Alias, len(s.Aliases))
for i, alias := range s.Aliases {
aliases[i] = Alias{URN: alias}
}
return aliases
}
// NewState is used to construct State values. The dataflow for State is rather sensitive, so all fields are required.
// Call [NewState.Make] to create the *State value.
//
//nolint:lll
type NewState struct {
// the resource's type.
Type tokens.Type // required
// the resource's object urn, a human-friendly, unique name for the resource.
URN URN // required
// true if the resource is custom, managed by a plugin.
Custom bool // required
// true if this resource is pending deletion due to a replacement.
Delete bool // required
// the resource's unique ID, assigned by the resource provider (or blank if none/uncreated).
ID ID // required
// the resource's input properties (as specified by the program).
Inputs PropertyMap // required
// the resource's complete output state (as returned by the resource provider).
Outputs PropertyMap // required
// an optional parent URN that this resource belongs to.
Parent URN // required
// true to "protect" this resource (protected resources cannot be deleted).
Protect bool // required
// true to force replacement of this resource during the next update.
Taint bool // required
// true if this resource is "external" to Pulumi and we don't control the lifecycle.
External bool // required
// the resource's dependencies.
Dependencies []URN // required
// the set of errors encountered in the process of initializing resource.
InitErrors []string // required
// the provider to use for this resource.
Provider string // required
// the set of dependencies that affect each property.
PropertyDependencies map[PropertyKey][]URN // required
// true if this resource was deleted and is awaiting replacement.
PendingReplacement bool // required
// an additional set of outputs that should be treated as secrets.
AdditionalSecretOutputs []PropertyKey // required
// an optional set of URNs for which this resource is an alias.
Aliases []URN // required
// A config block that will be used to configure timeouts for CRUD operations.
CustomTimeouts *CustomTimeouts // required
// the resource's import id, if this was an imported resource.
ImportID ID // required
// if set to True, the providers Delete method will not be called for this resource.
RetainOnDelete bool // required
// If set, the providers Delete method will not be called for this resource if specified resource is being deleted as well.
DeletedWith URN // required
// If set, the time when the state was initially added to the state file. (i.e. Create, Import)
Created *time.Time // required
// If set, the time when the state was last modified in the state file.
Modified *time.Time // required
// If set, the source location of the resource registration
SourcePosition string // required
// If set, the stack trace at time of registration
StackTrace []StackFrame // required
// If set, the list of properties to ignore changes for.
IgnoreChanges []string // required
// If set, the list of properties that if changed trigger a replace.
ReplaceOnChanges []string // required
// true if this resource should always be refreshed prior to updates.
RefreshBeforeUpdate bool // required
// If set, the URN of the resource this resource is a view of.
ViewOf URN // required
// The resource hooks attached to the resource, by type.
ResourceHooks map[HookType][]string // required
}
// Make consumes the NewState to create a *State.
func (s NewState) Make() *State {
contract.Assertf(s.Type != "", "type was empty")
contract.Assertf(s.Custom || s.ID == "", "is custom or had empty ID")
var customTimeouts CustomTimeouts
if s.CustomTimeouts != nil {
customTimeouts = *s.CustomTimeouts
}
return &State{
Type: s.Type,
URN: s.URN,
Custom: s.Custom,
Delete: s.Delete,
ID: s.ID,
Inputs: s.Inputs,
Outputs: s.Outputs,
Parent: s.Parent,
Protect: s.Protect,
Taint: s.Taint,
External: s.External,
Dependencies: s.Dependencies,
InitErrors: s.InitErrors,
Provider: s.Provider,
PropertyDependencies: s.PropertyDependencies,
PendingReplacement: s.PendingReplacement,
AdditionalSecretOutputs: s.AdditionalSecretOutputs,
Aliases: s.Aliases,
CustomTimeouts: customTimeouts,
ImportID: s.ImportID,
RetainOnDelete: s.RetainOnDelete,
DeletedWith: s.DeletedWith,
Created: s.Created,
Modified: s.Modified,
SourcePosition: s.SourcePosition,
StackTrace: s.StackTrace,
IgnoreChanges: s.IgnoreChanges,
ReplaceOnChanges: s.ReplaceOnChanges,
RefreshBeforeUpdate: s.RefreshBeforeUpdate,
ViewOf: s.ViewOf,
ResourceHooks: s.ResourceHooks,
}
}
// StateDependency objects are used when enumerating all the dependencies of a
// resource. They encapsulate the various types of dependency relationships that
// Pulumi resources may have with one another.
type StateDependency struct {
// The type of dependency.
Type StateDependencyType
// If the dependency is a property dependency, the property key that owns the
// dependency.
Key PropertyKey
// The URN of the resource that is being depended on.
URN URN
}
// The type of dependencies that a resource may have.
type StateDependencyType string
const (
// ResourceParent is the type of parent-child dependency relationships. The
// resource being depended on is the parent of the dependent resource.
ResourceParent StateDependencyType = "parent"
// ResourceDependency is the type of dependency relationships where there is
// no specific property owning the dependency.
ResourceDependency StateDependencyType = "dependency"
// ResourcePropertyDependency is the type of dependency relationships where a
// specific property makes reference to another resource.
ResourcePropertyDependency StateDependencyType = "property-dependency"
// ResourceDeletedWith is the type of dependency relationships where a
// resource will be "deleted with" another. The resource being depended on is
// one whose deletion subsumes the deletion of the dependent resource.
ResourceDeletedWith StateDependencyType = "deleted-with"
)
// GetAllDependencies returns a resource's provider and all of its dependencies.
// For use cases that rely on processing all possible links between sets of
// resources, this method (coupled with e.g. an exhaustive switch over the types
// of dependencies returned) should be preferred over direct access to e.g.
// Dependencies, PropertyDependencies, and so on.
func (s *State) GetAllDependencies() (string, []StateDependency) {
var allDeps []StateDependency
if s.Parent != "" {
allDeps = append(allDeps, StateDependency{Type: ResourceParent, URN: s.Parent})
}
for _, dep := range s.Dependencies {
if dep != "" {
allDeps = append(allDeps, StateDependency{Type: ResourceDependency, URN: dep})
}
}
for key, deps := range s.PropertyDependencies {
for _, dep := range deps {
if dep != "" {
allDeps = append(allDeps, StateDependency{Type: ResourcePropertyDependency, Key: key, URN: dep})
}
}
}
if s.DeletedWith != "" {
allDeps = append(allDeps, StateDependency{Type: ResourceDeletedWith, URN: s.DeletedWith})
}
return s.Provider, allDeps
}
// StackFrames are used to record the stack at the time a resource is registered.
type StackFrame struct {
// The source position associated with the stack frame.
SourcePosition string
}
// 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 resource
import (
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
)
// RootStackType is the type name that will be used for the root component in the Pulumi resource tree.
const RootStackType tokens.Type = tokens.RootStackType
// DefaultRootStackURN constructs a default root stack URN for the given stack and project.
func DefaultRootStackURN(stack tokens.QName, proj tokens.PackageName) URN {
return NewURN(stack, proj, "", RootStackType, string(proj)+"-"+string(stack))
}
// Copyright 2016-2023, 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 resource
// The contents of this file have been moved. The logic behind URN now lives in
// "github.com/pulumi/pulumi/sdk/v3/go/common/resource/urn". This file exists to fulfill
// backwards-compatibility requirements. No new declarations should be added here.
import (
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/urn"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
)
// URN is a friendly, but unique, URN for a resource, most often auto-assigned by Pulumi. These are
// used as unique IDs for objects, and help us to perform graph diffing and resolution of resource
// objects.
//
// In theory, we could support manually assigned URIs in the future. For the time being, however,
// we have opted to simplify developers' lives by mostly automating the generation of them
// algorithmically. The one caveat where it isn't truly automatic is that a developer -- or
// resource provider -- must provide a semi-unique name part.
//
// Each resource URN is of the form:
//
// urn:pulumi:<Stack>::<Project>::<Qualified$Type$Name>::<Name>
//
// wherein each element is the following:
//
// <Stack> The stack being deployed into
// <Project> The project being evaluated
// <Qualified$Type$Name> The object type's qualified type token (including the parent type)
// <Name> The human-friendly name identifier assigned by the developer or provider
//
// In the future, we may add elements to the URN; it is more important that it is unique than it is
// human-typable.
type URN = urn.URN
const (
URNPrefix = urn.Prefix // the standard URN prefix
URNNamespaceID = urn.NamespaceID // the URN namespace
URNNameDelimiter = urn.NameDelimiter // the delimiter between URN name elements
URNTypeDelimiter = urn.TypeDelimiter // the delimiter between URN type elements
)
// ParseURN attempts to parse a string into a URN returning an error if it's not valid.
func ParseURN(s string) (URN, error) { return urn.Parse(s) }
// ParseOptionalURN is the same as ParseURN except it will allow the empty string.
func ParseOptionalURN(s string) (URN, error) { return urn.ParseOptional(s) }
// NewURN creates a unique resource URN for the given resource object.
func NewURN(stack tokens.QName, proj tokens.PackageName, parentType, baseType tokens.Type, name string) URN {
return urn.New(stack, proj, parentType, baseType, name)
}
// Copyright 2016-2023, 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 urn
import (
"errors"
"fmt"
"runtime"
"strings"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
// URN is a friendly, but unique, URN for a resource, most often auto-assigned by Pulumi. These are
// used as unique IDs for objects, and help us to perform graph diffing and resolution of resource
// objects.
//
// In theory, we could support manually assigned URIs in the future. For the time being, however,
// we have opted to simplify developers' lives by mostly automating the generation of them
// algorithmically. The one caveat where it isn't truly automatic is that a developer -- or
// resource provider -- must provide a semi-unique name part.
//
// Each resource URN is of the form:
//
// urn:pulumi:<Stack>::<Project>::<Qualified$Type$Name>::<Name>
//
// wherein each element is the following:
//
// <Stack> The stack being deployed into
// <Project> The project being evaluated
// <Qualified$Type$Name> The object type's qualified type token (including the parent type)
// <Name> The human-friendly name identifier assigned by the developer or provider
//
// In the future, we may add elements to the URN; it is more important that it is unique than it is
// human-typable.
type URN string
const (
Prefix = "urn:" + NamespaceID + ":" // the standard URN prefix
NamespaceID = "pulumi" // the URN namespace
NameDelimiter = "::" // the delimiter between URN name elements
TypeDelimiter = "$" // the delimiter between URN type elements
)
// Parse attempts to parse a string into a URN returning an error if it's not valid.
func Parse(s string) (URN, error) {
if s == "" {
return "", errors.New("missing required URN")
}
urn := URN(s)
if !urn.IsValid() {
return "", fmt.Errorf("invalid URN %q", s)
}
return urn, nil
}
// ParseOptional is the same as Parse except it will allow the empty string.
func ParseOptional(s string) (URN, error) {
if s == "" {
return "", nil
}
return Parse(s)
}
// New creates a unique resource URN for the given resource object.
func New(stack tokens.QName, proj tokens.PackageName, parentType, baseType tokens.Type, name string) URN {
typ := string(baseType)
if parentType != "" && parentType != tokens.RootStackType {
typ = string(parentType) + TypeDelimiter + typ
}
return URN(
Prefix +
string(stack) +
NameDelimiter + string(proj) +
NameDelimiter + typ +
NameDelimiter + name,
)
}
// Quote returns the quoted form of the URN appropriate for use as a command line argument for the current OS.
func (urn URN) Quote() string {
quote := `'`
if runtime.GOOS == "windows" {
// Windows uses double-quotes instead of single-quotes.
quote = `"`
}
return quote + string(urn) + quote
}
// IsValid returns true if the URN is well-formed.
func (urn URN) IsValid() bool {
if !strings.HasPrefix(string(urn), Prefix) {
return false
}
return strings.Count(string(urn), NameDelimiter) >= 3
// TODO: We should validate the stack, project and type tokens here, but currently those fields might not
// actually be "valid" (e.g. spaces in project names, custom component types, etc).
}
// URNName returns the URN name part of a URN (i.e., strips off the prefix).
func (urn URN) URNName() string {
s := string(urn)
contract.Assertf(strings.HasPrefix(s, Prefix), "Urn is: '%s'", string(urn))
return s[len(Prefix):]
}
// Stack returns the resource stack part of a URN.
func (urn URN) Stack() tokens.QName {
return tokens.QName(getComponent(urn.URNName(), NameDelimiter, 0))
}
// Project returns the project name part of a URN.
func (urn URN) Project() tokens.PackageName {
return tokens.PackageName(getComponent(urn.URNName(), NameDelimiter, 1))
}
// QualifiedType returns the resource type part of a URN including the parent type
func (urn URN) QualifiedType() tokens.Type {
return tokens.Type(getComponent(urn.URNName(), NameDelimiter, 2))
}
// Gets the n'th delimited component of a string.
//
// This is used instead of the `strings.Split(string, delimiter)[index]` pattern which
// is inefficient.
func getComponent(input string, delimiter string, index int) string {
return getComponentN(input, delimiter, index, false)
}
// This gets the n'th delimited compnent of a string, and optionally the rest of the string
//
// If the *open* parameter is true, then this will return everything after the n-1th delimiter
func getComponentN(input string, delimiter string, index int, open bool) string {
if open && index == 0 {
return input
}
nameDelimiters := 0
partStart := 0
for i := 0; i < len(input); i++ {
if strings.HasPrefix(input[i:], delimiter) {
nameDelimiters++
if nameDelimiters == index {
i += len(delimiter)
partStart = i
if open {
return input[partStart:]
}
i--
} else if nameDelimiters > index {
return input[partStart:i]
} else {
i += len(delimiter) - 1
}
}
}
return input[partStart:]
}
// Type returns the resource type part of a URN
func (urn URN) Type() tokens.Type {
name := urn.URNName()
qualifiedType := getComponent(name, NameDelimiter, 2)
lastTypeDelimiter := strings.LastIndex(qualifiedType, TypeDelimiter)
return tokens.Type(qualifiedType[lastTypeDelimiter+1:])
}
// Name returns the resource name part of a URN.
func (urn URN) Name() string {
return getComponentN(urn.URNName(), NameDelimiter, 3, true)
}
// Returns a new URN with an updated name part
func (urn URN) Rename(newName string) URN {
return New(
urn.Stack(),
urn.Project(),
// parent type is empty because the qualified type already includes it
"",
urn.QualifiedType(),
newName,
)
}
// Returns a new URN with an updated stack part
func (urn URN) RenameStack(stack tokens.StackName) URN {
return New(
stack.Q(),
urn.Project(),
// parent type is empty because the qualified type already includes it
"",
urn.QualifiedType(),
urn.Name(),
)
}
// Returns a new URN with an updated project part
func (urn URN) RenameProject(project tokens.PackageName) URN {
return New(
urn.Stack(),
project,
// parent type is empty because the qualified type already includes it
"",
urn.QualifiedType(),
urn.Name(),
)
}
// Copyright 2016-2023, 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 slice
// Preallocates a slice of type T of a given length. If the input length is 0 then the returned slice will be
// nil. We prefer this over `make([]T, 0, length)` because nil is often treated semantically different from an
// empty slice.
func Prealloc[T any](capacity int) []T {
if capacity == 0 {
return nil
}
return make([]T, 0, capacity)
}
// Map applies the given function to each element of the given slice and returns a new slice with the results.
func Map[T, U any](s []T, f func(T) U) []U {
r := Prealloc[U](len(s))
for _, v := range s {
r = append(r, f(v))
}
return r
}
// MapError applies the given function to each element of the given slice and returns a new slice with the
// results. If any element returns an error that error is returned, as well as the slice of results so far.
func MapError[T, U any](s []T, f func(T) (U, error)) ([]U, error) {
r := Prealloc[U](len(s))
for _, v := range s {
u, err := f(v)
if err != nil {
return r, err
}
r = append(r, u)
}
return r, nil
}
// 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 tokens
import (
"regexp"
"strings"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
// Name is an identifier. It conforms to NameRegexpPattern.
type Name string
func (nm Name) String() string { return string(nm) }
// Q turns a Name into a qualified name; this is legal, since Name's is a proper subset of QName's grammar.
func (nm Name) Q() QName { return QName(nm) }
var (
NameRegexp = regexp.MustCompile(NameRegexpPattern)
nameFirstCharRegexp = regexp.MustCompile("^" + nameFirstCharRegexpPattern + "$")
nameRestCharRegexp = regexp.MustCompile("^" + nameRestCharRegexpPattern + "$")
)
var NameRegexpPattern = nameFirstCharRegexpPattern + nameRestCharRegexpPattern
const (
nameFirstCharRegexpPattern = "[A-Za-z0-9_.-]"
nameRestCharRegexpPattern = "[A-Za-z0-9_.-]*"
)
// IsName checks whether a string is a legal Name.
func IsName(s string) bool {
return s != "" && NameRegexp.FindString(s) == s
}
// QName is a qualified identifier. The "/" character optionally delimits different pieces of the name. Each element
// conforms to NameRegexpPattern. For example, "pulumi/pulumi/stack".
type QName string
func (nm QName) String() string { return string(nm) }
// QNameDelimiter is what delimits Namespace and Name parts.
const QNameDelimiter = "/"
var (
QNameRegexp = regexp.MustCompile(QNameRegexpPattern)
QNameRegexpPattern = "(" + NameRegexpPattern + "\\" + QNameDelimiter + ")*" + NameRegexpPattern
)
// IsQName checks whether a string is a legal QName.
func IsQName(s string) bool {
return s != "" && QNameRegexp.FindString(s) == s
}
// IntoQName converts an arbitrary string into a QName, converting the string to a valid QName if
// necessary. The conversion is deterministic, but also lossy.
func IntoQName(s string) QName {
output := []string{}
for _, s := range strings.Split(s, QNameDelimiter) {
if s == "" {
continue
}
segment := []byte(s)
if !nameFirstCharRegexp.Match([]byte{segment[0]}) {
segment[0] = '_'
}
for i := 1; i < len(s); i++ {
if !nameRestCharRegexp.Match([]byte{segment[i]}) {
segment[i] = '_'
}
}
output = append(output, string(segment))
}
result := strings.Join(output, QNameDelimiter)
if result == "" {
result = "_"
}
return QName(result)
}
// Name extracts the Name portion of a QName (dropping any namespace).
func (nm QName) Name() Name {
ix := strings.LastIndex(string(nm), QNameDelimiter)
var nmn string
if ix == -1 {
nmn = string(nm)
} else {
nmn = string(nm[ix+1:])
}
contract.Assertf(IsName(nmn), "QName %q has invalid name %q", nm, nmn)
return Name(nmn)
}
// Namespace extracts the namespace portion of a QName (dropping the name); this may be empty.
func (nm QName) Namespace() QName {
ix := strings.LastIndex(string(nm), QNameDelimiter)
var qn string
if ix == -1 {
qn = ""
} else {
qn = string(nm[:ix])
}
contract.Assertf(IsQName(qn), "QName %q has invalid namespace %q", nm, qn)
return QName(qn)
}
// PackageName is a qualified name referring to an imported package.
type PackageName QName
func (nm PackageName) String() string { return string(nm) }
// ModuleName is a qualified name referring to an imported module from a package.
type ModuleName QName
func (nm ModuleName) String() string { return string(nm) }
// ModuleMemberName is a simple name representing the module member's identifier.
type ModuleMemberName Name
func (nm ModuleMemberName) String() string { return string(nm) }
// ClassMemberName is a simple name representing the class member's identifier.
type ClassMemberName Name
func (nm ClassMemberName) Name() Name { return Name(nm) }
func (nm ClassMemberName) String() string { return string(nm) }
// TypeName is a simple name representing the type's name, without any package/module qualifiers.
type TypeName Name
func (nm TypeName) String() string { return string(nm) }
// Copyright 2016-2023, 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 tokens
import "errors"
// ValidateProjectName validates that the given string is a valid project name.
// The string must meet the following criteria:
//
// - must be non-empty
// - must be at most 100 characters
// - must contain only alphanumeric characters,
// hyphens, underscores, and periods (see [IsName])
//
// Returns a descriptive error if the string is not a valid project name.
func ValidateProjectName(s string) error {
switch {
case s == "":
return errors.New("project names may not be empty")
case len(s) > 100:
return errors.New("project names are limited to 100 characters")
case !IsName(s):
return errors.New("project names may only contain alphanumerics, hyphens, underscores, and periods")
}
return nil
}
// Copyright 2016-2023, 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 tokens
import (
"errors"
"fmt"
"regexp"
"github.com/pulumi/pulumi/sdk/v3/go/common/env"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
// StackName is a valid stack name. It should always be initialised via ParseStackName, the use of it's zero
// value will panic.
type StackName struct {
str string
}
// IsEmpty returns true if the stack name is empty.
func (sn StackName) IsEmpty() bool {
return sn.str == ""
}
// String implements fmt.Stringer. This method panics if StackName was zero initialized.
func (sn StackName) String() string {
if !env.DisableValidation.Value() {
contract.Assertf(sn.str != "", "stack name must not be empty")
}
return sn.str
}
// Q is a convenience method that returns the stack name as a QName. This method panics if StackName was zero
// initialized.
func (sn StackName) Q() QName {
return QName(sn.String())
}
var stackNameRegex = regexp.MustCompile("^[A-Za-z0-9_.-]*")
// ParseStackName parses a stack name from a string.
func ParseStackName(s string) (StackName, error) {
// Temporary flag to allow stack names validation to be disabled for the time being. Be sure to update the
// DisableValidation help text when this is removed.
if env.DisableValidation.Value() {
return StackName{s}, nil
}
if s == "" {
return StackName{}, errors.New("a stack name may not be empty")
}
if len(s) > 100 {
return StackName{}, errors.New("a stack name cannot exceed 100 characters")
}
failure := -1
if match := stackNameRegex.FindStringIndex(s); match == nil {
// We have failed to find any match, so the first char must be invalid.
failure = 0
} else if match[1] != len(s) {
// Our match did not extend to the end, so the invalid char must be the
// first char not matched.
failure = match[1]
}
if failure != -1 {
return StackName{}, fmt.Errorf(
"a stack name may only contain alphanumeric, hyphens, underscores, or periods: "+
"invalid character %q at position %d", s[failure], failure)
}
return StackName{s}, nil
}
// MustParseStackName parses a stack name from a string.
func MustParseStackName(s string) StackName {
n, err := ParseStackName(s)
contract.AssertNoErrorf(err, "failed to parse stack name %q", s)
return n
}
// 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 tokens contains the core symbol and token types for referencing resources and related entities.
package tokens
import (
"fmt"
"strings"
"unicode"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
// Token is a qualified name that is capable of resolving to a symbol entirely on its own. Most uses of tokens are
// typed based on the context, so that a subset of the token syntax is permissible (see the various typedefs below).
// However, in its full generality, a token can have a package part, a module part, a module-member part, and a
// class-member part. Obviously tokens that are meant to address just a module won't have the module-member part, and
// tokens addressing module members won't have the class-member part, etc.
//
// Token's grammar is as follows:
//
// Token = <Identifier> |
// <QualifiedToken> |
// <DecoratedType>
// Identifier = <Name>
// QualifiedToken = <PackageName> [ ":" <ModuleName> [ ":" <ModuleMemberName> [ ":" <ClassMemberName> ] ] ]
// PackageName = ... similar to <QName>, except dashes permitted ...
// ModuleName = <QName>
// ModuleMemberName = <Name>
// ClassMemberName = <Name>
//
// A token may be a simple identifier in the case that it refers to a built-in symbol, like a primitive type, or a
// variable in scope, rather than a qualified token that is to be bound to a symbol through package/module resolution.
//
// Notice that both package and module names may be qualified names (meaning they can have "/"s in them; see QName's
// comments), and that module and class members must use unqualified, simple names (meaning they have no delimiters).
// The specialized token kinds differ only in what elements they require as part of the token string.
//
// Finally, a token may also be a decorated type. This is for built-in array, map, pointer, and function types:
//
// DecoratedType = "*" <Token> |
// "[]" <Token> |
// "map[" <Token> "]" <Token> |
// "(" [ <Token> [ "," <Token> ]* ] ")" <Token>?
//
// Notice that a recursive parsing process is required to extract elements from a <DecoratedType> token.
type Token string
const TokenDelimiter string = ":" // the character delimiting portions of a qualified token.
func (tok Token) Delimiters() int { return strings.Count(string(tok), TokenDelimiter) }
func (tok Token) HasModule() bool { return tok.Delimiters() > 0 }
func (tok Token) HasModuleMember() bool { return tok.Delimiters() > 1 }
func (tok Token) Simple() bool { return tok.Delimiters() == 0 }
func (tok Token) String() string { return string(tok) }
// delimiter returns the Nth index of a delimiter, as specified by the argument.
func (tok Token) delimiter(n int) int {
ix := -1
for n > 0 {
// Make sure we still have space.
if ix+1 >= len(tok) {
ix = -1
break
}
// If we do, keep looking for the next delimiter.
nix := strings.Index(string(tok[ix+1:]), TokenDelimiter)
if nix == -1 {
break
}
ix += 1 + nix
n--
}
return ix
}
// Name returns the Token as a Name (and assumes it is a legal one).
func (tok Token) Name() Name {
contract.Requiref(tok.Simple(), "tok", "Simple")
contract.Requiref(IsName(tok.String()), "tok", "IsName(%v)", tok)
return Name(tok.String())
}
// Package extracts the package from the token, assuming one exists.
func (tok Token) Package() Package {
if t := Type(tok); t.Primitive() {
return "" // decorated and primitive types are built-in (and hence have no package).
}
if tok.HasModule() {
return Package(tok[:tok.delimiter(1)])
}
return Package(tok)
}
// Module extracts the module portion from the token, assuming one exists.
func (tok Token) Module() Module {
if tok.HasModule() {
if tok.HasModuleMember() {
return Module(tok[:tok.delimiter(2)])
}
return Module(tok)
}
return Module("")
}
// ModuleMember extracts the module member portion from the token, assuming one exists.
func (tok Token) ModuleMember() ModuleMember {
if tok.HasModuleMember() {
return ModuleMember(tok)
}
return ModuleMember("")
}
// Package is a token representing just a package. It uses a much simpler grammar:
//
// Package = <PackageName>
//
// Note that a package name of "." means "current package", to simplify emission and lookups.
type Package Token
func NewPackageToken(nm PackageName) Package {
contract.Assertf(IsQName(string(nm)), "Package name '%v' is not a legal qualified name", nm)
return Package(nm)
}
func (tok Package) Name() PackageName {
return PackageName(tok)
}
func (tok Package) String() string { return string(tok) }
// Module is a token representing a module. It uses the following subset of the token grammar:
//
// Module = <Package> ":" <ModuleName>
//
// Note that a module name of "." means "current module", to simplify emission and lookups.
type Module Token
func NewModuleToken(pkg Package, nm ModuleName) Module {
contract.Assertf(IsQName(string(nm)), "Package '%v' module name '%v' is not a legal qualified name", pkg, nm)
return Module(string(pkg) + TokenDelimiter + string(nm))
}
func (tok Module) Package() Package {
t := Token(tok)
contract.Assertf(t.HasModule(), "Module token '%v' missing module delimiter", tok)
return Package(tok[:t.delimiter(1)])
}
func (tok Module) Name() ModuleName {
t := Token(tok)
contract.Assertf(t.HasModule(), "Module token '%v' missing module delimiter", tok)
return ModuleName(tok[t.delimiter(1)+1:])
}
func (tok Module) String() string { return string(tok) }
// ModuleMember is a token representing a module's member. It uses the following grammar. Note that this is not
// ambiguous because member names cannot contain slashes, and so the "last" slash in a name delimits the member:
//
// ModuleMember = <Module> "/" <ModuleMemberName>
type ModuleMember Token
func NewModuleMemberToken(mod Module, nm ModuleMemberName) ModuleMember {
contract.Assertf(IsName(string(nm)), "Module '%v' member name '%v' is not a legal name", mod, nm)
return ModuleMember(string(mod) + TokenDelimiter + string(nm))
}
// ParseModuleMember attempts to turn the string s into a module member, returning an error if it isn't a valid one.
func ParseModuleMember(s string) (ModuleMember, error) {
if !Token(s).HasModuleMember() {
return "", fmt.Errorf("String '%v' is not a valid module member", s)
}
return ModuleMember(s), nil
}
func (tok ModuleMember) Package() Package {
return tok.Module().Package()
}
func (tok ModuleMember) Module() Module {
t := Token(tok)
contract.Assertf(t.HasModuleMember(), "Module member token '%v' missing module member delimiter", tok)
return Module(tok[:t.delimiter(2)])
}
func (tok ModuleMember) Name() ModuleMemberName {
t := Token(tok)
contract.Assertf(t.HasModuleMember(), "Module member token '%v' missing module member delimiter", tok)
return ModuleMemberName(tok[t.delimiter(2)+1:])
}
func (tok ModuleMember) String() string { return string(tok) }
// Type is a token representing a type. It is either a primitive type name, reference to a module class, or decorated:
//
// Type = <Name> | <ModuleMember> | <DecoratedType>
type Type Token
func NewTypeToken(mod Module, nm TypeName) Type {
contract.Assertf(IsName(string(nm)), "Module '%v' type name '%v' is not a legal name", mod, nm)
return Type(string(mod) + TokenDelimiter + string(nm))
}
// ParseTypeToken interprets an arbitrary string as a Type, returning an error if the string is not a valid Type.
func ParseTypeToken(s string) (Type, error) {
tok := Token(s)
if !tok.HasModuleMember() {
return "", fmt.Errorf("Type '%s' is not a valid type token (must have format '*:*:*')", tok)
}
return Type(tok), nil
}
func (tok Type) Package() Package {
if tok.Primitive() {
return Package("")
}
return ModuleMember(tok).Package()
}
func (tok Type) Module() Module {
if tok.Primitive() {
return Module("")
}
return ModuleMember(tok).Module()
}
func (tok Type) Name() TypeName {
if tok.Primitive() {
return TypeName(tok)
}
return TypeName(ModuleMember(tok).Name())
}
// Primitive indicates whether this type is a primitive type name (i.e., not qualified with a module, etc).
func (tok Type) Primitive() bool {
return !Token(tok).HasModule()
}
func (tok Type) String() string { return string(tok) }
func camelCase(s string) string {
if len(s) == 0 {
return s
}
runes := []rune(s)
runes[0] = unicode.ToLower(runes[0])
return string(runes)
}
// DisplayName returns a simpler, user-readable version of this type name.
//
// {package}:{module path truncated to the last slash}:{type name}
//
// If not possible, it will return the string representation of the type.
func (tok Type) DisplayName() string {
typeString := string(tok)
components := strings.Split(typeString, ":")
if len(components) != 3 {
return typeString
}
pkg, module, name := components[0], components[1], components[2]
if len(name) == 0 {
return typeString
}
lastSlashInModule := strings.LastIndexByte(module, '/')
if lastSlashInModule == -1 {
return typeString
}
file := module[lastSlashInModule+1:]
if file != camelCase(name) {
return typeString
}
return fmt.Sprintf("%v:%v:%v", pkg, module[:lastSlashInModule], name)
}
// Copyright 2016-2019, 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 ciutil
import (
"fmt"
"os"
"strings"
)
// azurePipelinesCI represents the Azure Pipelines CI/CD system
// that belongs to the Azure DevOps product suite.
type azurePipelinesCI struct {
baseCI
}
// DetectVars detects the env vars from Azure Piplines.
// See:
// https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables
func (az azurePipelinesCI) DetectVars() Vars {
v := Vars{Name: AzurePipelines}
v.BuildID = os.Getenv("BUILD_BUILDID")
v.BuildType = os.Getenv("BUILD_REASON")
v.SHA = os.Getenv("BUILD_SOURCEVERSION")
v.CommitMessage = os.Getenv("BUILD_SOURCEVERSIONMESSAGE")
orgURI := os.Getenv("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI")
orgURI = strings.TrimSuffix(orgURI, "/")
projectName := os.Getenv("SYSTEM_TEAMPROJECT")
v.BuildURL = fmt.Sprintf("%v/%v/_build/results?buildId=%v", orgURI, projectName, v.BuildID)
// Azure Pipelines can be connected to external repos.
// If the repo provider is GitHub, then we need to use
// `SYSTEM_PULLREQUEST_PULLREQUESTNUMBER` instead of
// `SYSTEM_PULLREQUEST_PULLREQUESTID` and
// `SYSTEM_PULLREQUEST_SOURCECOMMITID` instead of
// `BUILD_SOURCEVERSION`.
// For other Git repos,
// `SYSTEM_PULLREQUEST_PULLREQUESTID` may be the only variable
// that is set if the build is running for a PR build.
//
// Note that the PR ID/number only applies to Git repos.
vcsProvider := os.Getenv("BUILD_REPOSITORY_PROVIDER")
switch vcsProvider {
case "GitHub":
// GitHub is a git repo hosted on GitHub.
v.PRNumber = os.Getenv("SYSTEM_PULLREQUEST_PULLREQUESTNUMBER")
v.SHA = os.Getenv("SYSTEM_PULLREQUEST_SOURCECOMMITID")
default:
v.PRNumber = os.Getenv("SYSTEM_PULLREQUEST_PULLREQUESTID")
}
// Build.SourceBranchName is the last part of the head.
// If the build is running because of a PR, we should use the
// PR source branch name, instead of Build.SourceBranchName.
// That's because Build.SourceBranchName will always be `merge` --
// the last part of `refs/pull/1/merge`.
if v.PRNumber != "" {
v.BranchName = os.Getenv("SYSTEM_PULLREQUEST_SOURCEBRANCH")
} else {
v.BranchName = os.Getenv("BUILD_SOURCEBRANCHNAME")
}
return v
}
// Copyright 2016-2019, 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 ciutil
import (
"fmt"
"os"
)
// bitbucketPipelinesCI represents the Bitbucket CI system.
type bitbucketPipelinesCI struct {
baseCI
}
// DetectVars detects the Bitbucket env vars.
// See https://confluence.atlassian.com/bitbucket/environment-variables-794502608.html.
func (bb bitbucketPipelinesCI) DetectVars() Vars {
v := Vars{Name: bb.Name}
buildID := os.Getenv("BITBUCKET_BUILD_NUMBER")
v.BuildID = buildID
repoURL := os.Getenv("BITBUCKET_GIT_HTTP_ORIGIN")
if repoURL != "" {
buildURL := fmt.Sprintf("%v/addon/pipelines/home#!/results/%v", repoURL, buildID)
v.BuildURL = buildURL
}
v.SHA = os.Getenv("BITBUCKET_COMMIT")
v.BranchName = os.Getenv("BITBUCKET_BRANCH")
v.PRNumber = os.Getenv("BITBUCKET_PR_ID")
return v
}
// Copyright 2016-2021, 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 ciutil
import (
"os"
)
// buildkiteCI represents a Buildkite CI/CD system.
type buildkiteCI struct {
baseCI
}
// DetectVars detects the env vars for a Buildkite Build.
func (bci buildkiteCI) DetectVars() Vars {
v := Vars{Name: Buildkite}
// https://buildkite.com/docs/pipelines/environment-variables#bk-env-vars-buildkite-branch
v.BranchName = os.Getenv("BUILDKITE_BRANCH")
// https://buildkite.com/docs/pipelines/environment-variables#bk-env-vars-buildkite-build-id
v.BuildID = os.Getenv("BUILDKITE_BUILD_ID")
// https://buildkite.com/docs/pipelines/environment-variables#bk-env-vars-buildkite-build-number
v.BuildNumber = os.Getenv("BUILDKITE_BUILD_NUMBER")
// https://buildkite.com/docs/pipelines/environment-variables#bk-env-vars-buildkite-build-url
v.BuildURL = os.Getenv("BUILDKITE_BUILD_URL")
// https://buildkite.com/docs/pipelines/environment-variables#bk-env-vars-buildkite-message
// This is usually the commit message but can be other messages.
v.CommitMessage = os.Getenv("BUILDKITE_MESSAGE")
// https://buildkite.com/docs/pipelines/environment-variables#bk-env-vars-buildkite-pull-request
// If Buildkite's PR env var it is a pull request of the supplied number, else the build type is
// whatever Buildkite says it is. Pull requests are webhooks just like a standard push so this allows
// us to differentiate the two.
prNumber := os.Getenv("BUILDKITE_PULL_REQUEST")
if prNumber != "false" {
v.PRNumber = prNumber
v.BuildType = "PullRequest"
} else {
// https://buildkite.com/docs/pipelines/environment-variables#bk-env-vars-buildkite-source
v.BuildType = os.Getenv("BUILDKITE_SOURCE")
}
// https://buildkite.com/docs/pipelines/environment-variables#bk-env-vars-buildkite-commit
v.SHA = os.Getenv("BUILDKITE_COMMIT")
return v
}
// Copyright 2016-2019, 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 ciutil
import (
"os"
)
// circleCICI represents the "Circle CI" CI system.
type circleCICI struct {
baseCI
}
// DetectVars detects the Circle CI env vars.
// See: https://circleci.com/docs/2.0/env-vars/
func (c circleCICI) DetectVars() Vars {
v := Vars{Name: c.Name}
v.BuildID = os.Getenv("CIRCLE_BUILD_NUM")
v.BuildURL = os.Getenv("CIRCLE_BUILD_URL")
v.SHA = os.Getenv("CIRCLE_SHA1")
v.BranchName = os.Getenv("CIRCLE_BRANCH")
return v
}
// Copyright 2016-2019, 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 ciutil
import (
"os"
)
// codefreshCI represents the Codefresh CI system.
type codefreshCI struct {
baseCI
}
// DetectVars detects the env vars for a Codefresh CI system.
// See: https://codefresh.io/docs/docs/codefresh-yaml/variables/
func (c codefreshCI) DetectVars() Vars {
v := Vars{Name: c.Name}
v.BuildID = os.Getenv("CF_BUILD_ID")
v.BuildURL = os.Getenv("CF_BUILD_URL")
v.SHA = os.Getenv("CF_REVISION")
v.BranchName = os.Getenv("CF_BRANCH")
v.CommitMessage = os.Getenv("CF_COMMIT_MESSAGE")
v.PRNumber = os.Getenv("CF_PULL_REQUEST_NUMBER")
if v.PRNumber == "" {
v.BuildType = "PullRequest"
} else {
v.BuildType = "Push"
}
return v
}
// Copyright 2016-2025, 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 ciutil
import (
"os"
)
// detectors contains environment variable names and their values, if applicable, for detecting when we're running in
// CI. See https://github.com/watson/ci-info/blob/master/index.js.
//
// For any CI system for which we detect additional env vars, the type of `System` is specific to that CI system. The
// rest, even though we detect if it is that CI system, may not have an implementation that detects all useful env vars,
// and hence just uses the `baseCI` struct type.
var detectors = []system{
// GenericCI picks up a set of environment variables that users may define explicitly when using a CI system that we
// would not otherwise detect. It is deliberately placed first in this list so that it takes precedence over any other
// CI system that we may detect.
genericCICI{
baseCI: baseCI{
Name: SystemName(os.Getenv("PULUMI_CI_SYSTEM")),
EnvVarsToDetect: []string{"PULUMI_CI_SYSTEM"},
},
},
// Supported CI systems, in alphabetical order. We expect (rather, support) exactly one of these matching, so their
// order should not really matter.
baseCI{
Name: AppVeyor,
EnvVarsToDetect: []string{"APPVEYOR"},
},
baseCI{
Name: AWSCodeBuild,
EnvVarsToDetect: []string{"CODEBUILD_BUILD_ARN"},
},
baseCI{
Name: AtlassianBamboo,
EnvVarsToDetect: []string{"bamboo_planKey"},
},
bitbucketPipelinesCI{
baseCI: baseCI{
Name: AtlassianBitbucketPipelines,
EnvVarsToDetect: []string{"BITBUCKET_COMMIT"},
},
},
azurePipelinesCI{
baseCI: baseCI{
Name: AzurePipelines,
EnvVarsToDetect: []string{"TF_BUILD"},
},
},
buildkiteCI{
baseCI: baseCI{
Name: Buildkite,
EnvVarsToDetect: []string{"BUILDKITE"},
},
},
circleCICI{
baseCI: baseCI{
Name: CircleCI,
EnvVarsToDetect: []string{"CIRCLECI"},
},
},
codefreshCI{
baseCI: baseCI{
Name: Codefresh,
EnvVarsToDetect: []string{"CF_BUILD_URL"},
},
},
baseCI{
Name: Codeship,
EnvValuesToDetect: map[string]string{"CI_NAME": "codeship"},
},
baseCI{
Name: Drone,
EnvVarsToDetect: []string{"DRONE"},
},
githubActionsCI{
baseCI{
Name: GitHubActions,
EnvVarsToDetect: []string{"GITHUB_ACTIONS"},
},
},
gitlabCI{
baseCI: baseCI{
Name: GitLab,
EnvVarsToDetect: []string{"GITLAB_CI"},
},
},
baseCI{
Name: GoCD,
EnvVarsToDetect: []string{"GO_PIPELINE_LABEL"},
},
baseCI{
Name: Hudson,
EnvVarsToDetect: []string{"HUDSON_URL"},
},
jenkinsCI{
baseCI: baseCI{
Name: Jenkins,
EnvVarsToDetect: []string{"JENKINS_URL"},
},
},
baseCI{
Name: MagnumCI,
EnvVarsToDetect: []string{"MAGNUM"},
},
baseCI{
Name: Semaphore,
EnvVarsToDetect: []string{"SEMAPHORE"},
},
baseCI{
Name: Spacelift,
EnvVarsToDetect: []string{
"SPACELIFT_MAX_REQUESTS_BURST", "TF_VAR_spacelift_run_trigger", "SPACELIFT_STORE_HOOKS_ENV_VARS",
"TF_VAR_spacelift_commit_branch", "SPACELIFT_WORKER_TRACING_ENABLED",
},
},
baseCI{
Name: TaskCluster,
EnvVarsToDetect: []string{"TASK_ID", "RUN_ID"},
},
baseCI{
Name: TeamCity,
EnvVarsToDetect: []string{"TEAMCITY_VERSION"},
},
travisCI{
baseCI: baseCI{
Name: Travis,
EnvVarsToDetect: []string{"TRAVIS"},
},
},
}
// IsCI returns true if we are running in a known CI system.
func IsCI() bool {
return detectSystem() != nil
}
// detectSystem returns a CI system name when the current system looks like a CI system.
// Detection is based on environment variables that CI vendors, we know about, set.
func detectSystem() system {
// Provide a way to disable CI/CD detection, as it can interfere with the ability to test.
if os.Getenv("PULUMI_DISABLE_CI_DETECTION") != "" {
return nil
}
for _, system := range detectors {
if system.IsCI() {
return system
}
}
return nil
}
// Copyright 2016-2019, 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 ciutil
import (
"os"
)
// genericCICI represents a generic CI/CD system.
// It provides a way for users to workaround the fact that the CLI
// may not know about their CI system. This is sort of an
// escape hatch to give the CLI a hint about the CI environment.
type genericCICI struct {
baseCI
}
// DetectVars detects the env vars for a Generic CI system.
func (g genericCICI) DetectVars() Vars {
v := Vars{}
v.Name = SystemName(os.Getenv("PULUMI_CI_SYSTEM"))
v.BranchName = os.Getenv("PULUMI_CI_BRANCH_NAME")
v.BuildID = os.Getenv("PULUMI_CI_BUILD_ID")
v.BuildNumber = os.Getenv("PULUMI_CI_BUILD_NUMBER")
v.BuildType = os.Getenv("PULUMI_CI_BUILD_TYPE")
v.BuildURL = os.Getenv("PULUMI_CI_BUILD_URL")
v.CommitMessage = os.Getenv("PULUMI_COMMIT_MESSAGE")
v.PRNumber = os.Getenv("PULUMI_PR_NUMBER")
v.SHA = os.Getenv("PULUMI_CI_PULL_REQUEST_SHA")
return v
}
// Copyright 2016-2019, 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 ciutil
import (
"encoding/json"
"fmt"
"os"
"strconv"
)
// githubActionsCI represents the GitHub Actions CI system.
type githubActionsCI struct {
baseCI
}
type githubPRHead struct {
SHA string `json:"sha"`
Ref string `json:"ref"`
}
// githubPR represents the `pull_request` payload posted by GitHub to trigger
// workflows for PRs. Note that this is only a partial representation as we
// don't need anything other than the PR number.
// See https://developer.github.com/webhooks/event-payloads/#pull_request.
type githubPR struct {
Head githubPRHead `json:"head"`
}
// githubActionsPullRequestEvent represents the webhook payload for a pull_request event.
// https://help.github.com/en/actions/reference/events-that-trigger-workflows#pull-request-event-pull_request
type githubActionsPullRequestEvent struct {
Action string `json:"action"`
Number int64 `json:"number"`
PullRequest githubPR `json:"pull_request"`
}
// DetectVars detects the GitHub Actions env vars.
// See https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables.
func (t githubActionsCI) DetectVars() Vars {
v := Vars{Name: GitHubActions}
v.BuildID = os.Getenv("GITHUB_RUN_ID")
v.BuildNumber = os.Getenv("GITHUB_RUN_NUMBER")
v.BuildType = os.Getenv("GITHUB_EVENT_NAME")
v.BranchName = os.Getenv("GITHUB_REF")
repoSlug := os.Getenv("GITHUB_REPOSITORY")
if repoSlug != "" && v.BuildID != "" {
v.BuildURL = fmt.Sprintf("https://github.com/%s/actions/runs/%s", repoSlug, v.BuildID)
}
v.SHA = os.Getenv("GITHUB_SHA")
if v.BuildType == "pull_request" {
event := t.GetPREvent()
if event != nil {
prNumber := strconv.FormatInt(event.Number, 10)
v.PRNumber = prNumber
v.SHA = event.PullRequest.Head.SHA
v.BranchName = event.PullRequest.Head.Ref
}
}
return v
}
// GetPREvent returns the GitHub webhook payload found in the GitHub Actions environment.
// GitHub stores the JSON payload of the webhook that triggered the workflow in a path.
// The path is set as the value of the env var GITHUB_EVENT_PATH. Returns nil if an error
// is encountered or the GITHUB_EVENT_PATH is not set.
func (t githubActionsCI) GetPREvent() *githubActionsPullRequestEvent {
eventPath := os.Getenv("GITHUB_EVENT_PATH")
if eventPath == "" {
return nil
}
b, err := os.ReadFile(eventPath)
if err != nil {
return nil
}
var prEvent githubActionsPullRequestEvent
if err := json.Unmarshal(b, &prEvent); err != nil {
return nil
}
return &prEvent
}
// Copyright 2016-2019, 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 ciutil
import (
"os"
)
// gitlabCI represents the GitLab CI system.
type gitlabCI struct {
baseCI
}
// DetectVars detects the Travis env vars.
// See https://docs.gitlab.com/ee/ci/variables/.
func (gl gitlabCI) DetectVars() Vars {
v := Vars{Name: gl.Name}
v.BuildID = os.Getenv("CI_PIPELINE_ID")
v.BuildNumber = os.Getenv("CI_PIPELINE_IID")
v.BuildType = os.Getenv("CI_PIPELINE_SOURCE")
v.BuildURL = os.Getenv("CI_JOB_URL")
v.SHA = os.Getenv("CI_COMMIT_SHA")
v.BranchName = os.Getenv("CI_COMMIT_REF_NAME")
v.CommitMessage = os.Getenv("CI_COMMIT_MESSAGE")
v.PRNumber = os.Getenv("CI_MERGE_REQUEST_IID")
return v
}
// Copyright 2016-2019, 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 ciutil
import (
"os"
)
// jenkinsCI represents the Travis CI system.
type jenkinsCI struct {
baseCI
}
func (j jenkinsCI) DetectVars() Vars {
v := Vars{Name: Jenkins}
v.BuildID = os.Getenv("BUILD_NUMBER")
if v.BuildID == "" {
// BUILD_ID env var is defunct since version 1.597 and
// should return the same value as BUILD_NUMBER.
// See: https://issues.jenkins-ci.org/browse/JENKINS-26520.
v.BuildID = os.Getenv("BUILD_ID")
}
v.BuildURL = os.Getenv("BUILD_URL")
// Even though Jenkins supports SVN and CVS-based source control repos,
// we will just look at the GIT_* variables.
v.SHA = os.Getenv("GIT_COMMIT")
v.BranchName = os.Getenv("GIT_BRANCH")
return v
}
// 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 ciutil
import (
"os"
)
// CI system constants.
const (
AppVeyor SystemName = "AppVeyor"
AWSCodeBuild SystemName = "AWS CodeBuild"
AtlassianBamboo SystemName = "Atlassian Bamboo"
AtlassianBitbucketPipelines SystemName = "Atlassian Bitbucket Pipelines"
AzurePipelines SystemName = "Azure Pipelines"
Buildkite SystemName = "Buildkite"
CircleCI SystemName = "CircleCI"
Codefresh SystemName = "Codefresh"
Codeship SystemName = "Codeship"
Drone SystemName = "Drone"
// GenericCI is used when a CI system in which the CLI is being run,
// is not recognized by it. Users can set the relevant env vars
// as a fallback so that the CLI would still pick-up the metadata related
// to their CI build.
GenericCI SystemName = "Generic CI"
GitHubActions SystemName = "GitHub Actions"
GitLab SystemName = "GitLab CI/CD"
GoCD SystemName = "GoCD"
Hudson SystemName = "Hudson"
Jenkins SystemName = "Jenkins"
MagnumCI SystemName = "Magnum CI"
Semaphore SystemName = "Semaphore"
Spacelift SystemName = "Spacelift"
TaskCluster SystemName = "TaskCluster"
TeamCity SystemName = "TeamCity"
Travis SystemName = "Travis CI"
)
// SystemName is a recognized CI system.
type SystemName string
// system represents a CI/CD system.
type system interface {
// DetectVars when called on a specific instance of a CISystem
// detects the env vars of the corresponding CI/CD system and
// returns `Vars` with those values.
DetectVars() Vars
// IsCI returns true if any of the CI systems's associated environment variables are set.
IsCI() bool
}
// Vars contains a set of metadata variables about a CI system.
type Vars struct {
// Name is a required friendly name of the CI system.
Name SystemName
// BuildID is an optional unique identifier for the current build/job.
// In some CI systems the build ID is a system-wide unique internal ID
// and the `BuildNumber` is the repo/project-specific unique ID.
BuildID string
// BuildNumber is the unique identifier of a build within a project/repository.
// This is only set for CI systems that expose both the internal ID, as well as
// a project/repo-specific ID.
BuildNumber string
// BuildType is an optional friendly type name of the build/job type.
BuildType string
// BuildURL is an optional URL for this build/job's webpage.
BuildURL string
// SHA is the SHA hash of the code repo at which this build/job is running.
SHA string
// BranchName is the name of the feature branch currently being built.
BranchName string
// CommitMessage is the full message of the Git commit being built.
CommitMessage string
// PRNumber is the pull-request ID/number in the source control system.
PRNumber string
}
// baseCI implements the `System` interface with default
// implementations.
//
// When creating a new CI System implementation, implement the
// DetectVars and any other function you wish to override.
type baseCI struct {
Name SystemName
// EnvVarsToDetect is an array of env vars to check if any of these env vars is set,
// which would indicate that the Pulumi CLI is running in that CI system's environment.
EnvVarsToDetect []string
// EnvValuesToDetect is a map of env vars and their expected values to check for,
// in order to see if the Pulumi CLI is running inside a certain CI system's environment.
EnvValuesToDetect map[string]string
}
// DetectVars in the base implementation returns a Vars
// struct with just the Name property of the CI system.
func (d baseCI) DetectVars() Vars {
return Vars{Name: d.Name}
}
// IsCI returns true if a specific env var of a CI system is set.
func (d baseCI) IsCI() bool {
for _, e := range d.EnvVarsToDetect {
if os.Getenv(e) != "" {
return true
}
}
for k, v := range d.EnvValuesToDetect {
if os.Getenv(k) == v {
return true
}
}
return false
}
// Copyright 2016-2019, 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 ciutil
import (
"os"
)
// travisCI represents the Travis CI system.
type travisCI struct {
baseCI
}
// DetectVars detects the Travis env vars.
// See https://docs.travis-ci.com/user/environment-variables/.
func (t travisCI) DetectVars() Vars {
v := Vars{Name: Travis}
v.BuildID = os.Getenv("TRAVIS_JOB_ID")
v.BuildNumber = os.Getenv("TRAVIS_JOB_NUMBER")
v.BuildType = os.Getenv("TRAVIS_EVENT_TYPE")
v.BuildURL = os.Getenv("TRAVIS_BUILD_WEB_URL")
v.SHA = os.Getenv("TRAVIS_PULL_REQUEST_SHA")
v.BranchName = os.Getenv("TRAVIS_BRANCH")
v.CommitMessage = os.Getenv("TRAVIS_COMMIT_MESSAGE")
// Travis sets the value of TRAVIS_PULL_REQUEST to false if the build
// is not a PR build.
// See: https://docs.travis-ci.com/user/environment-variables/#convenience-variables
if prNumber := os.Getenv("TRAVIS_PULL_REQUEST"); prNumber != "false" {
v.PRNumber = prNumber
}
return v
}
// 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 ciutil
import (
"os"
)
// DetectVars detects and returns the CI variables for the current environment.
// Not all fields of the `Vars` struct are applicable to every CI system,
// and may be left blank.
func DetectVars() Vars {
if os.Getenv("PULUMI_DISABLE_CI_DETECTION") != "" {
return Vars{Name: ""}
}
var v Vars
system := detectSystem()
if system == nil {
return v
}
// Detect the vars for the respective CI system and
v = system.DetectVars()
return v
}
// 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 cmdutil
import (
"errors"
"fmt"
"github.com/hashicorp/go-multierror"
"github.com/spf13/cobra"
)
// ArgsFunc wraps a standard cobra argument validator with standard Pulumi error handling.
func ArgsFunc(argsValidator cobra.PositionalArgs) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
err := argsValidator(cmd, args)
if err != nil {
return errors.Join(cmd.Help(), err)
}
return nil
}
}
// NoArgs is the same as cobra.NoArgs, except it is wrapped with ArgsFunc to provide standard
// Pulumi error handling.
var NoArgs = ArgsFunc(cobra.NoArgs)
// MaximumNArgs is the same as cobra.MaximumNArgs, except it is wrapped with ArgsFunc to provide standard
// Pulumi error handling.
func MaximumNArgs(n int) cobra.PositionalArgs {
return ArgsFunc(cobra.MaximumNArgs(n))
}
// MinimumNArgs is the same as cobra.MinimumNArgs, except it is wrapped with ArgsFunc to provide standard
// Pulumi error handling.
func MinimumNArgs(n int) cobra.PositionalArgs {
return ArgsFunc(cobra.MinimumNArgs(n))
}
// ExactArgs is the same as cobra.ExactArgs, except it is wrapped with ArgsFunc to provide standard
// Pulumi error handling.
func ExactArgs(n int) cobra.PositionalArgs {
return ArgsFunc(cobra.ExactArgs(n))
}
// SpecificArgs requires a set of specific arguments. We use the names to improve diagnostics.
func SpecificArgs(argNames []string) cobra.PositionalArgs {
return ArgsFunc(func(cmd *cobra.Command, args []string) error {
if len(args) > len(argNames) {
return fmt.Errorf("too many arguments: got %d, expected %d", len(args), len(argNames))
} else if len(args) < len(argNames) {
var result error
for i := len(args); i < len(argNames); i++ {
result = multierror.Append(result, fmt.Errorf("missing required argument: %s", argNames[i]))
}
return result
}
return nil
})
}
// RangeArgs is the same as cobra.RangeArgs, except it is wrapped with ArgsFunc to provide standard
// Pulumi error handling.
func RangeArgs(minimum int, maximum int) cobra.PositionalArgs {
return ArgsFunc(cobra.RangeArgs(minimum, maximum))
}
// 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.
//go:build !windows && !js
// +build !windows,!js
package cmdutil
import (
"os"
"os/exec"
"syscall"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
// KillChildren calls os.Process.Kill() on every child process of `pid`'s, stoping after the first error (if any). It
// also only kills direct child process, not any children they may have.
func KillChildren(pid int) error {
// A subprocess that was launched after calling `RegisterProcessGroup` below will
// belong to a process group whose ID is the same as the PID. Passing the negation
// of our PID (same as the PGID) sends a SIGKILL to all processes in our group.
//
// Relevant documentation: https://linux.die.net/man/2/kill
// "If pid is less than -1, then sig is sent to every process in the
// process group whose ID is -pid. "
return syscall.Kill(-pid, syscall.SIGKILL)
}
// killProcessGroup sends SIGKILL to the process group for the given process.
//
// This is a helper function for TerminateProcessGroup;
// a Windows version with the same signature exists in child_windows.go.
func killProcessGroup(proc *os.Process) error {
return KillChildren(proc.Pid)
}
// RegisterProcessGroup informs the OS that it needs to call `setpgid` on this
// child process. When it comes time to kill this process, we'll kill all processes
// in the same process group.
func RegisterProcessGroup(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}
func InterruptChildren(pid int) {
err := syscall.Kill(-pid, syscall.SIGINT)
contract.IgnoreError(err)
}
// 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 cmdutil
import (
"fmt"
"io"
"os"
"regexp"
"runtime"
"strings"
"github.com/rivo/uniseg"
"golang.org/x/term"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/ciutil"
)
// Emoji controls whether emojis will by default be printed in the output.
// While some Linux systems can display Emoji's in the terminal by default, we restrict this to just macOS, like Yarn.
var Emoji = (runtime.GOOS == "darwin")
// EmojiOr returns the emoji string e if emojis are enabled, or the string or if emojis are disabled.
func EmojiOr(e, or string) string {
if Emoji && Interactive() {
return e
}
return or
}
// DisableInteractive may be set to true in order to disable prompts. This is useful when running in a non-attended
// scenario, such as in continuous integration, or when using the Pulumi CLI/SDK in a programmatic way.
var DisableInteractive bool
// Interactive returns true if we should be running in interactive mode. That is, we have an interactive terminal
// session, interactivity hasn't been explicitly disabled, and we're not running in a known CI system.
func Interactive() bool {
return !DisableInteractive && InteractiveTerminal() && !ciutil.IsCI()
}
// InteractiveTerminal returns true if the current terminal session is interactive.
func InteractiveTerminal() bool {
// If there's a 'TERM' variable and the terminal is 'dumb', then disable interactive mode.
if v := strings.ToLower(os.Getenv("TERM")); v == "dumb" {
return false
}
// if we're piping in stdin, we're clearly not interactive, as there's no way for a user to
// provide input. If we're piping stdout, we also can't be interactive as there's no way for
// users to see prompts to interact with them.
//nolint:gosec // os.Stdin.Fd() == 0 && os.Stdout.Fd() == 1: uintptr -> int conversion is always safe
return term.IsTerminal(int(os.Stdin.Fd())) &&
term.IsTerminal(int(os.Stdout.Fd()))
}
// ReadConsole reads the console with the given prompt text.
func ReadConsole(prompt string) (string, error) {
//nolint:gosec // os.Stdin.Fd() == 0: uintptr -> int conversion is always safe
if !term.IsTerminal(int(os.Stdin.Fd())) {
return readConsolePlain(os.Stdout, os.Stdin, prompt)
}
return readConsoleFancy(os.Stdout, os.Stdin, prompt, false /* secret */)
}
// ReadConsoleWithDefault reads the console with the given prompt text with support for a default value.
func ReadConsoleWithDefault(prompt string, defaultValue string) (string, error) {
promptMessage := fmt.Sprintf("%s [%s]", prompt, defaultValue)
value, err := ReadConsole(promptMessage)
if err != nil {
return "", err
}
if value == "" {
value = defaultValue
}
return value, nil
}
// readConsolePlain prints the given prompt (if any),
// and reads the user's response from stdin.
//
// It does so without altering the terminal's state in any way,
// and will work even if stdin is not a terminal.
func readConsolePlain(stdout io.Writer, stdin io.Reader, prompt string) (string, error) {
if prompt != "" {
fmt.Print(prompt + ": ")
}
var raw strings.Builder
for {
var b [1]byte
if _, err := os.Stdin.Read(b[:]); err != nil {
return "", err
}
if b[0] == '\n' {
break
}
raw.WriteByte(b[0])
}
return RemoveTrailingNewline(raw.String()), nil
}
// IsTruthy returns true if the given string represents a CLI input interpreted as "true".
func IsTruthy(s string) bool {
return s == "1" || strings.EqualFold(s, "true")
}
// RemoveTrailingNewline removes a trailing newline from a string. On windows, we'll remove either \r\n or \n, on other
// platforms, we just remove \n.
func RemoveTrailingNewline(s string) string {
s = strings.TrimSuffix(s, "\n")
if runtime.GOOS == "windows" {
s = strings.TrimSuffix(s, "\r")
}
return s
}
// EndKeypadTransmitMode switches the terminal out of the keypad transmit 'application' mode back to 'normal' mode.
func EndKeypadTransmitMode() {
if runtime.GOOS != "windows" && Interactive() {
// Print an escape sequence to switch the keypad mode, same as 'tput rmkx'.
// Work around https://github.com/pulumi/pulumi/issues/3480.
// A better fix might be fixing upstream https://github.com/AlecAivazis/survey/issues/228.
fmt.Print("\033[?1l")
}
}
type Table struct {
Headers []string
Rows []TableRow // Rows of the table.
Prefix string // Optional prefix to print before each row
}
// TableRow is a row in a table we want to print. It can be a series of a columns, followed
// by an additional line of information.
type TableRow struct {
Columns []string // Columns of the row
AdditionalInfo string // an optional line of information to print after the row
}
// FprintTable prints a grid of rows and columns. Width of columns is automatically determined by
// the max length of the items in each column. A default gap of two spaces is printed between each
// column.
func FprintTable(w io.Writer, table Table) error {
_, err := fmt.Fprint(w, table)
return err
}
// PrintTable prints the table to stdout.
// See [FprintTable] for details.
func PrintTable(table Table) {
_ = FprintTable(os.Stdout, table)
// Ignore error for stdout.
}
// PrintTableWithGap prints a grid of rows and columns. Width of columns is automatically determined
// by the max length of the items in each column. A gap can be specified between the columns.
func PrintTableWithGap(table Table, columnGap string) {
fmt.Print(table.ToStringWithGap(columnGap))
}
func (table Table) String() string {
return table.ToStringWithGap(" ")
}
// 7-bit C1 ANSI sequences
var ansiEscape = regexp.MustCompile(`\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])`)
// MeasureText returns the number of glyphs in a string.
// Importantly this also ignores ANSI escape sequences, so can be used to calculate layout of colorized strings.
func MeasureText(text string) int {
// Strip ansi escape sequences
clean := ansiEscape.ReplaceAllString(text, "")
// Need to count graphemes not runes or bytes
return uniseg.StringWidth(clean)
}
// normalizedRows returns the rows of a table in normalized form.
//
// A row is considered normalized if and only if it has no new lines in any of its fields.
func (table Table) normalizedRows() []TableRow {
rows := slice.Prealloc[TableRow](len(table.Rows))
for _, row := range table.Rows {
info := row.AdditionalInfo
buckets := make([][]string, len(row.Columns))
maxLines := 0
for i, column := range row.Columns {
buckets[i] = strings.Split(column, "\n")
maxLines = max(maxLines, len(buckets[i]))
}
row := []TableRow{}
for i := 0; i < maxLines; i++ {
part := TableRow{}
for _, b := range buckets {
if i < len(b) {
part.Columns = append(part.Columns, b[i])
} else {
part.Columns = append(part.Columns, "")
}
}
row = append(row, part)
}
row[len(row)-1].AdditionalInfo = info
rows = append(rows, row...)
}
return rows
}
func (table Table) ToStringWithGap(columnGap string) string {
return table.Render(&TableRenderOptions{ColumnGap: columnGap})
}
type TableRenderOptions struct {
ColumnGap string
HeaderStyle []colors.Color
ColumnStyle []colors.Color
Color colors.Colorization
}
func (table Table) Render(opts *TableRenderOptions) string {
if opts == nil {
opts = &TableRenderOptions{}
}
if opts.ColumnGap == "" {
opts.ColumnGap = " "
}
if opts.Color == "" {
opts.Color = colors.Never
}
columnCount := len(table.Headers)
// Figure out the preferred column width for each column. It will be set to the max length of
// any item in that column.
preferredColumnWidths := make([]int, columnCount)
allRows := []TableRow{{
Columns: table.Headers,
}}
allRows = append(allRows, table.normalizedRows()...)
for rowIndex, row := range allRows {
columns := row.Columns
if len(columns) != len(preferredColumnWidths) {
panic(fmt.Sprintf(
"Error printing table. Column count of row %v didn't match header column count. %v != %v",
rowIndex, len(columns), len(preferredColumnWidths)))
}
for columnIndex, val := range columns {
preferredColumnWidths[columnIndex] = max(preferredColumnWidths[columnIndex], MeasureText(val))
}
}
var result strings.Builder
for rowIndex, row := range allRows {
result.WriteString(table.Prefix)
for columnIndex, val := range row.Columns {
style := opts.HeaderStyle
if rowIndex != 0 {
style = opts.ColumnStyle
}
if len(style) != 0 {
result.WriteString(opts.Color.Colorize(style[columnIndex]))
}
result.WriteString(val)
if len(style) != 0 {
result.WriteString(opts.Color.Colorize(colors.Reset))
}
if columnIndex < columnCount-1 {
// Work out how much whitespace we need to add to this string to bring it up to the
// preferredColumnWidth for this column.
maxWidth := preferredColumnWidths[columnIndex]
padding := maxWidth - MeasureText(val)
result.WriteString(strings.Repeat(" ", padding))
// Now, ensure we have the requested gap between columns as well.
result.WriteString(opts.ColumnGap)
}
// do not want whitespace appended to the last column. It would cause wrapping on lines
// that were not actually long if some other line was very long.
}
result.WriteByte('\n')
if row.AdditionalInfo != "" {
result.WriteString(row.AdditionalInfo)
}
}
return result.String()
}
// 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.
//go:build !js
// +build !js
package cmdutil
import (
"io"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
func readConsoleFancy(stdout io.Writer, stdin io.Reader, prompt string, secret bool) (string, error) {
final, err := tea.NewProgram(
newReadConsoleModel(prompt, secret),
tea.WithInput(stdin),
tea.WithOutput(stdout),
).Run()
if err != nil {
return "", err
}
model, ok := final.(readConsoleModel)
contract.Assertf(ok, "expected readConsoleModel, got %T", final)
if model.Canceled {
return "", io.EOF
}
return model.Value, nil
}
// readConsoleModel drives a bubbletea widget that reads from the console.
type readConsoleModel struct {
input textinput.Model
secret bool
// Canceled is set to true when the model finishes
// if the user canceled the operation by pressing Ctrl-C or Esc.
Canceled bool
// Value is the user's response to the prompt.
Value string
}
var _ tea.Model = readConsoleModel{}
func newReadConsoleModel(prompt string, secret bool) readConsoleModel {
input := textinput.New()
input.Cursor.Style = lipgloss.NewStyle().
Foreground(lipgloss.Color("205")) // 205 = hot pink cursor
if secret {
input.EchoMode = textinput.EchoPassword
}
if prompt != "" {
input.Prompt = prompt + ": "
}
input.Focus() // required to receive input
return readConsoleModel{
input: input,
secret: secret,
}
}
// Init initializes the model.
// We don't have any initialization to do, so we just return nil.
func (readConsoleModel) Init() tea.Cmd { return nil }
// Update handles a single tick of the bubbletea loop.
func (m readConsoleModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
// If the user pressed enter, Ctrl-C, or Esc,
// it's time to stop the bubbletea loop.
//
// Only Enter is considered a success.
//nolint:exhaustive // We only want special handling for these keys.
switch msg.Type {
case tea.KeyEnter, tea.KeyCtrlC, tea.KeyEsc:
m.Value = m.input.Value()
m.Canceled = msg.Type != tea.KeyEnter
m.input.Blur() // hide the cursor
if m.secret {
// If we're in secret mode, don't include
// the '*' characters in the final output
// so as not to leak the length of the input.
m.input.EchoMode = textinput.EchoNone
}
var cmds []tea.Cmd
if !m.Canceled {
// If the user accepts the input,
// we'll primnt the prompt to the terminal
// before exiting this loop.
cmds = append(cmds, tea.Println(m.input.View()))
}
cmds = append(cmds, tea.Quit)
return m, tea.Sequence(cmds...)
}
}
var cmd tea.Cmd
m.input, cmd = m.input.Update(msg)
return m, cmd
}
// View renders the prompt.
func (m readConsoleModel) View() string {
return m.input.View()
}
// 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 cmdutil
import (
"os"
"golang.org/x/term"
)
// ReadConsoleNoEcho reads from the console without echoing. This is useful for reading passwords.
func ReadConsoleNoEcho(prompt string) (string, error) {
// If standard input is not a terminal, we must not use ReadPassword as it will fail with an ioctl
// error when it tries to disable local echo.
//
// In this case, just read normally
//nolint:gosec // os.Stdin.Fd() == 0: uintptr -> int conversion is always safe
if !term.IsTerminal(int(os.Stdin.Fd())) {
return readConsolePlain(os.Stdout, os.Stdin, prompt)
}
return readConsoleFancy(os.Stdout, os.Stdin, prompt, true /* secret */)
}
// 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 cmdutil
import (
"fmt"
"os"
"sync"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
var (
snkMutex sync.Mutex
snk diag.Sink
)
// By default we'll attempt to figure out if we should have colors or not. This can be overridden
// for any command by passing --color=... at the command line.
var globalColorization *colors.Colorization
// GetGlobalColorization gets the global setting for how things should be colored.
// This is helpful for the parts of our stack that do not take a DisplayOptions struct.
func GetGlobalColorization() colors.Colorization {
if globalColorization != nil {
// User has set an explicit colorization preference. We'll respect whatever they asked for,
// no matter what.
return *globalColorization
}
// Colorization is set to 'auto' (either explicit set to that by the user, or not set at all).
// Figure out the best thing to do here.
// If the external environment has requested no colors, then turn off all colors when in 'auto' mode.
if _, ok := os.LookupEnv("NO_COLOR"); ok {
return colors.Never
}
// Disable colors if we're not in an interactive session (i.e. we're redirecting stdout). This
// will just inject color tags into the stream which are not desirable here.
if !InteractiveTerminal() {
return colors.Never
}
// Things otherwise look good. Turn on colors.
return colors.Always
}
// SetGlobalColorization sets the global setting for how things should be colored.
// This is helpful for the parts of our stack that do not take a DisplayOptions struct.
func SetGlobalColorization(value string) error {
switch value {
case "auto":
globalColorization = nil
case "always":
c := colors.Always
globalColorization = &c
case "never":
c := colors.Never
globalColorization = &c
case "raw":
c := colors.Raw
globalColorization = &c
default:
return fmt.Errorf("unsupported color option: '%s'. Supported values are: auto, always, never, raw", value)
}
return nil
}
// Diag lazily allocates a sink to be used if we can't create a compiler.
func Diag() diag.Sink {
snkMutex.Lock()
defer snkMutex.Unlock()
if snk == nil {
snk = diag.DefaultSink(os.Stderr, os.Stderr, diag.FormatOptions{
Color: GetGlobalColorization(),
})
}
return snk
}
// InitDiag forces initialization of the diagnostics sink with the given options.
func InitDiag(opts diag.FormatOptions) {
contract.Assertf(snk == nil, "Cannot initialize diagnostics sink more than once")
snk = diag.DefaultSink(os.Stderr, os.Stderr, opts)
}
// 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 cmdutil
import (
"fmt"
"os"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/result"
)
// DetailedError extracts a detailed error message, including stack trace, if there is one.
func DetailedError(err error) string {
msg := errorMessage(err)
hasstack := false
for {
if stackerr, ok := err.(interface {
StackTrace() errors.StackTrace
}); ok {
msg += "\n"
if hasstack {
msg += "CAUSED BY...\n"
}
hasstack = true
// Append the stack trace.
for _, f := range stackerr.StackTrace() {
msg += fmt.Sprintf("%+v\n", f)
}
// Keep going up the causer chain, if any.
cause := errors.Cause(err)
if cause == err || cause == nil {
break
}
err = cause
} else {
break
}
}
return msg
}
// RunFunc wraps an error-returning run func with standard Pulumi error handling. All
// Pulumi commands should wrap themselves in this to ensure consistent and appropriate
// error behavior. In particular, we want to avoid any calls to os.Exit in the middle of
// a callstack which might prohibit reaping of child processes, resources, etc. And we
// wish to avoid the default Cobra unhandled error behavior, because it is formatted
// incorrectly and needlessly prints usage.
//
// If run returns a BailError, we will not print an error message.
//
// Deprecated: Instead of using [RunFunc], you should call [DisplayErrorMessage] and then
// manually exit with `os.Exit(-1)`
func RunFunc(run func(cmd *cobra.Command, args []string) error) func(*cobra.Command, []string) {
return func(cmd *cobra.Command, args []string) {
err := run(cmd, args)
if err != nil {
DisplayErrorMessage(err)
os.Exit(-1)
}
}
}
// DisplayErrorMessage displays an error message to the user.
//
// DisplayErrorMessage respects [result.IsBail] and [logging.LogToStderr].
func DisplayErrorMessage(err error) {
// If we were asked to bail, that means we already printed out a message. We just need
// to quit at this point (with an error code so no one thinks we succeeded). Bailing
// always indicates a failure, just one we don't need to print a message for.
if err == nil || result.IsBail(err) {
return
}
var msg string
if logging.LogToStderr {
msg = DetailedError(err)
} else {
msg = errorMessage(err)
logging.V(3).Info(DetailedError(err))
}
Diag().Errorf(diag.Message("", "%s"), msg)
}
// Exit exits with a given error.
func Exit(err error) {
ExitError(errorMessage(err))
}
// ExitError issues an error and exits with a standard error exit code.
func ExitError(msg string) {
Diag().Errorf(diag.Message("", "%s"), msg)
os.Exit(-1)
}
// errorMessage returns a message, possibly cleaning up the text if appropriate.
func errorMessage(err error) string {
contract.Requiref(err != nil, "err", "must not be nil")
underlying := flattenErrors(err)
switch len(underlying) {
case 0:
return err.Error()
case 1:
return underlying[0].Error()
default:
msg := fmt.Sprintf("%d errors occurred:", len(underlying))
for i, werr := range underlying {
msg += fmt.Sprintf("\n %d) %s", i+1, errorMessage(werr))
}
return msg
}
}
// Flattens an error into a slice of errors containing the supplied error and
// all errors it wraps. If the set of wrapped errors is a tree (as e.g. produced
// by errors.Join), the errors are flattened in a depth-first manner. This
// function supports both native wrapped errors and those produced by the
// multierror package.
func flattenErrors(err error) []error {
var errs []error
switch multi := err.(type) {
case *multierror.Error:
for _, e := range multi.Errors {
errs = append(errs, flattenErrors(e)...)
}
case interface{ Unwrap() []error }:
for _, e := range multi.Unwrap() {
errs = append(errs, flattenErrors(e)...)
}
default:
errs = append(errs, err)
}
return errs
}
// 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 cmdutil
import (
"fmt"
"os"
"runtime"
"runtime/pprof"
"runtime/trace"
"time"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
func InitProfiling(prefix string, memProfileRate int) error {
cpu, err := os.Create(fmt.Sprintf("%s.%v.cpu", prefix, os.Getpid()))
if err != nil {
return fmt.Errorf("could not start CPU profile: %w", err)
}
if err = pprof.StartCPUProfile(cpu); err != nil {
return fmt.Errorf("could not start CPU profile: %w", err)
}
exec, err := os.Create(fmt.Sprintf("%s.%v.trace", prefix, os.Getpid()))
if err != nil {
return fmt.Errorf("could not start execution trace: %w", err)
}
if err = trace.Start(exec); err != nil {
return fmt.Errorf("could not start execution trace: %w", err)
}
if memProfileRate > 0 {
runtime.MemProfileRate = memProfileRate
go memoryProfileWriteLoop(prefix)
}
return nil
}
func CloseProfiling(prefix string) error {
pprof.StopCPUProfile()
trace.Stop()
// get up-to-date statistics
return writeMemoryProfile(prefix)
}
func writeMemoryProfile(prefix string) error {
mem, err := os.Create(fmt.Sprintf("%s.%v.mem", prefix, os.Getpid()))
if err != nil {
return fmt.Errorf("could not create memory profile: %w", err)
}
defer contract.IgnoreClose(mem)
runtime.GC()
if err = pprof.Lookup("allocs").WriteTo(mem, 0); err != nil {
return fmt.Errorf("could not write memory profile: %w", err)
}
return nil
}
func memoryProfileWriteLoop(prefix string) {
// Every 5 seconds write a memory profile (in case we crash before we get a chance)
for i := 0; ; i++ {
time.Sleep(5 * time.Second)
mem, err := os.Create(fmt.Sprintf("%s.%v.mem.%d", prefix, os.Getpid(), i))
if err != nil {
contract.IgnoreClose(mem)
fmt.Fprintf(os.Stderr, "could not create memory profile: %s\n", err.Error())
return
}
runtime.GC() // get up-to-date statistics
if err = pprof.Lookup("allocs").WriteTo(mem, 0); err != nil {
fmt.Fprintf(os.Stderr, "could not create memory profile: %s\n", err.Error())
}
contract.IgnoreClose(mem)
os.Remove(fmt.Sprintf("%s.%v.mem.%d", prefix, os.Getpid(), i-1))
}
}
// Copyright 2025, 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.
//go:build !windows && !js
// +build !windows,!js
package cmdutil
import (
"context"
"fmt"
"net"
"net/http"
netpprof "net/http/pprof"
"os"
"os/signal"
"syscall"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
)
func InitPprofServer(ctx context.Context) {
sigusr := make(chan os.Signal, 1)
go func() {
defer logging.Flush()
<-sigusr
listener, err := net.Listen("tcp", "localhost:0")
if err != nil {
logging.Errorf("could not start listener for pprof server: %s", err)
return
}
mux := http.NewServeMux()
mux.Handle("/debug/pprof/", http.HandlerFunc(netpprof.Index))
mux.Handle("/debug/pprof/cmdline", http.HandlerFunc(netpprof.Cmdline))
mux.Handle("/debug/pprof/profile", http.HandlerFunc(netpprof.Profile))
mux.Handle("/debug/pprof/symbol", http.HandlerFunc(netpprof.Symbol))
mux.Handle("/debug/pprof/trace", http.HandlerFunc(netpprof.Trace))
serverErr := make(chan error, 1)
go func() {
serverErr <- http.Serve(listener, mux) //nolint:gosec // G114
}()
u := fmt.Sprintf("http://localhost:%d/debug/pprof/", listener.Addr().(*net.TCPAddr).Port)
// Don't use logging.V here, we always want to create & write a log file here.
logging.Infof("pprof server running on %s", u)
logging.Flush() // Immediately flush after logging the URL so we don't have to wait for the periodic flush.
select {
case <-ctx.Done():
case err := <-serverErr:
logging.Errorf("pprof server error: %s", err)
}
if err := listener.Close(); err != nil {
logging.Errorf("failed to close pprof listener: %s", err)
}
}()
signal.Notify(sigusr, syscall.SIGUSR1)
}
// 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 cmdutil
import (
"fmt"
"time"
"unicode/utf8"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
)
// NewSpinnerAndTicker returns a new Spinner and a ticker that will fire an event when the next call
// to Spinner.Tick() should be called. NewSpinnerAndTicket takes into account if stdout is
// connected to a tty or not and returns either a nice animated spinner that updates quickly, using
// the specified ttyFrames, or a simple spinner that just prints a dot on each tick and updates
// slowly.
func NewSpinnerAndTicker(prefix string, ttyFrames []string,
color colors.Colorization, timesPerSecond time.Duration,
suppressProgress bool,
) (Spinner, *time.Ticker) {
if ttyFrames == nil {
// If explicit tick frames weren't specified, default to unicode for Mac and ASCII for Windows/Linux.
if Emoji {
ttyFrames = DefaultEmojiSpinFrames
} else {
ttyFrames = DefaultASCIISpinFrames
}
}
if suppressProgress {
return &noopSpinner{}, time.NewTicker(time.Second * 20)
}
if Interactive() {
return &ttySpinner{
prefix: prefix,
frames: ttyFrames,
}, time.NewTicker(time.Second / timesPerSecond)
}
return &dotSpinner{
color: color,
prefix: prefix,
}, time.NewTicker(time.Second * 20)
}
// Spinner represents a very simple progress reporter.
type Spinner interface {
// Tick prints the next frame of the spinner. After Tick() has been called, there should be no writes to Stdout before
// calling Reset().
Tick()
// Reset is called to release ownership of stdout, so others may write to it.
Reset()
}
var (
// DefaultASCIISpinFrames is the default set of symbols to show while spinning in an ASCII TTY setting.
DefaultASCIISpinFrames = []string{
"|", "/", "-", "\\",
}
// DefaultEmojiSpinFrames is the default set of symbols to show while spinning in a Unicode-enabled TTY setting.
DefaultEmojiSpinFrames = []string{
"⠋", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋",
}
)
// ttySpinner is the spinner that can be used when standard out is a tty. When we are connected to a TTY we can erase
// characters we've written and provide a nice quick progress spinner.
type ttySpinner struct {
prefix string
frames []string
index int
lastWritten int
}
func (spin *ttySpinner) Tick() {
if spin.lastWritten > 0 {
for i := 0; i < spin.lastWritten; i++ {
fmt.Print("\b \b")
}
} else {
fmt.Print(spin.prefix)
}
frame := spin.frames[spin.index]
fmt.Print(frame)
spin.lastWritten = utf8.RuneCountInString(frame)
spin.index = (spin.index + 1) % len(spin.frames)
}
func (spin *ttySpinner) Reset() {
if spin.lastWritten > 0 {
for i := 0; i < len(spin.prefix)+spin.lastWritten; i++ {
fmt.Print("\b \b")
}
}
spin.index = 0
spin.lastWritten = 0
}
// dotSpinner is the spinner that can be used when standard out is not a tty. In this case, we just write a single
// dot on each tick.
type dotSpinner struct {
color colors.Colorization
prefix string
hasWritten bool
}
func (spin *dotSpinner) Tick() {
if !spin.hasWritten {
fmt.Print(spin.color.Colorize(colors.Yellow + spin.prefix + colors.Reset))
}
fmt.Print(spin.color.Colorize(colors.Yellow + "." + colors.Reset))
spin.hasWritten = true
}
func (spin *dotSpinner) Reset() {
if spin.hasWritten {
fmt.Println()
}
spin.hasWritten = false
}
type noopSpinner struct{}
func (spin *noopSpinner) Tick() {}
func (spin *noopSpinner) Reset() {}
// Copyright 2016-2023, 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 cmdutil
import (
"context"
"errors"
"os"
"os/exec"
"time"
)
// TerminateProcessGroup terminates the process group
// of the given process by sending a termination signal to it.
//
// - On Linux and macOS, it sends a SIGINT
// - On Windows, it sends a CTRL_BREAK_EVENT
//
// If the root process does not exit gracefully within the given duration,
// all processes in the group are forcibly terminated.
//
// Returns true if the process exited gracefully, false otherwise.
//
// Returns an error if the process could not be terminated,
// or if the process exited with a non-zero exit code.
func TerminateProcessGroup(proc *os.Process, cooldown time.Duration) (ok bool, err error) {
// The choice to use SIGINT and CTRL_BREAK_EVENT
// merits some explanation.
//
// On *nix, typically,
// SIGTERM is used for programmatic graceful shutdown,
// and SIGINT is used when the user presses Ctrl+C.
// e.g. Kubernetes sends SIGTERM to signal shutdown.
// So in short, SIGTERM is for computers, SIGINT is for humans.
//
// On Windows,
// there's CTRL_C_EVENT which is obviously analogous to SIGINT
// because they both handle Ctrl+C,
// and CTRL_BREAK_EVENT which is special to Windows,
// but we can decide it's analogous to SIGTERM.
//
// However, when writing a signal handler on Windows,
// different languages map these signals differently.
// Go maps both, CTRL_BREAK_EVENT and CTRL_C_EVENT to SIGINT,
// Node and Python map CTRL_BREAK_EVENT to SIGBREAK
// (which exists only on Windows), and CTRL_C_EVENT to SIGINT.
//
// In short:
//
// | OS | Signal sent | Language | Handled as |
// |------|------------------|----------|------------|
// | *nix | SIGTERM | Go | SIGTERM |
// | | | Node | SIGTERM |
// | | | Python | SIGTERM |
// | |------------------|----------|------------|
// | | SIGINT | Go | SIGINT |
// | | | Node | SIGINT |
// | | | Python | SIGINT |
// |------|------------------|----------|------------|
// | Win | CTRL_BREAK_EVENT | Go | SIGINT |
// | | | Node | SIGBREAK |
// | | | Python | SIGBREAK |
// | |------------------|----------|------------|
// | | CTRL_C_EVENT | Go | SIGINT |
// | | | Node | SIGINT |
// | | | Python | SIGINT |
//
// So the SIGINT+CTRL_C_EVENT combo would be the obvious choice here
// since it's consistent across languages and platforms;
// plugins would define a single SIGINT handler
// and it would work in all cases.
//
// Unfortunately, Winodws does not support sending CTRL_C_EVENT
// to a specific child process.
// It's "current process and all child processes" or nothing.
/// Per the docs [1], the CTRL_C_EVENT
// "cannot be limited to a specific process group."
//
// [1]: https://learn.microsoft.com/en-us/windows/console/generateconsolectrlevent
//
// So we have to use CTRL_BREAK_EVENT for Windows instead.
// At that point, using SIGINT for *nix makes sense because
// users will want to handle SIGINT anyway
// so that they can press Ctrl+C in the terminal.
if err := shutdownProcessGroup(proc.Pid); err != nil {
// Couldn't shut down the process gracefully.
// Let's just kill it.
return false, killProcessGroup(proc)
}
var waitErr error
ctx, cancel := context.WithTimeout(context.Background(), cooldown)
go func() {
defer cancel()
state, err := proc.Wait()
switch {
case err == nil && !state.Success():
// Non-zero exit code.
err = &exec.ExitError{ProcessState: state}
case isWaitAlreadyExited(err):
err = nil
}
waitErr = err
}()
// The context will be canceled when the timeout expires,
// or when the process exits, whichever happens first.
<-ctx.Done()
if err := ctx.Err(); errors.Is(err, context.DeadlineExceeded) {
// The process didn't exit within the given duration.
// Kill it.
return false, killProcessGroup(proc)
}
return true, waitErr
}
// Copyright 2016-2023, 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.
//go:build !windows && !js
// +build !windows,!js
package cmdutil
import (
"errors"
"syscall"
)
func Interrupt(pid int) error {
return syscall.Kill(pid, syscall.SIGINT)
}
// shutdownProcessGroup sends a SIGINT to the given process group.
// It returns immediately, and does not wait for the process to exit.
//
// A Windows version of this function is defined in term_windows.go.
func shutdownProcessGroup(pid int) error {
// Processes spawned after calling RegisterProcessGroup
// will be part of the same process group as the parent.
//
// -pid means send the signal to the entire process group.
//
// See: https://linux.die.net/man/2/kill
return syscall.Kill(-pid, syscall.SIGINT)
}
// isWaitAlreadyExited returns true
// if the error is due to the process already having exited.
//
// On Linux, this is indicated by ESRCH or ECHILD.
//
// A Windows version of this function is defined in term_windows.go.
func isWaitAlreadyExited(err error) bool {
return errors.Is(err, syscall.ESRCH) || // no such process
errors.Is(err, syscall.ECHILD) // no child processes
}
// Copyright 2016-2021, 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 cmdutil
import (
"fmt"
"io"
"log"
"net"
"net/url"
"os"
"runtime"
"strings"
"time"
opentracing "github.com/opentracing/opentracing-go"
"github.com/pulumi/appdash"
appdash_opentracing "github.com/pulumi/appdash/opentracing"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
jaeger "github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/transport/zipkin"
)
// TracingEndpoint is the Zipkin-compatible tracing endpoint where tracing data will be sent.
var TracingEndpoint string
// TracingToFile indicates if pulumi was called with a file:// scheme URL (--tracing=file:///...).
//
// Deprecated: Even in this case TracingEndpoint will now have the tcp:// scheme and will point to a
// proxy server that will append traces to the user-specified file. Plugins should respect
// TracingEndpoint and ignore TracingToFile.
var TracingToFile bool
var TracingRootSpan opentracing.Span
var traceCloser io.Closer
type localStore struct {
path string
store *appdash.MemoryStore
}
func (s *localStore) Close() error {
f, err := os.Create(s.path)
if err != nil {
return err
}
defer contract.IgnoreClose(f)
return s.store.Write(f)
}
func IsTracingEnabled() bool {
return TracingEndpoint != ""
}
// InitTracing initializes tracing
func InitTracing(name, rootSpanName, tracingEndpoint string) {
// If no tracing endpoint was provided, just return. The default global tracer is already a no-op tracer.
if tracingEndpoint == "" {
return
}
endpointURL, err := url.Parse(tracingEndpoint)
if err != nil {
log.Fatalf("invalid tracing endpoint: %v", err)
}
var tracer opentracing.Tracer
switch endpointURL.Scheme {
case "file":
// If the endpoint is a file:// URL, use a local tracer.
TracingToFile = true
path := endpointURL.Path
if path == "" {
path = endpointURL.Opaque
}
if path == "" {
log.Fatalf("invalid tracing endpoint: %v", err)
}
store := &localStore{
path: path,
store: appdash.NewMemoryStore(),
}
traceCloser = store
collector := appdash.NewLocalCollector(store.store)
tracer = appdash_opentracing.NewTracer(collector)
proxyEndpoint, err := startProxyAppDashServer(collector)
if err != nil {
log.Fatal(err)
}
// Instead of storing the original endpoint, store the
// proxy endpoint. The TracingEndpoint global var is
// consumed by code forking off sub-processes, and we
// want those sending data to the proxy endpoint, so
// it cleanly lands in the file managed by the parent
// process.
TracingEndpoint = proxyEndpoint
case "tcp":
// Store the tracing endpoint
TracingEndpoint = tracingEndpoint
// If the endpoint scheme is tcp, use an Appdash endpoint.
collector := appdash.NewRemoteCollector(endpointURL.Host)
traceCloser = collector
tracer = appdash_opentracing.NewTracer(collector)
default:
// Store the tracing endpoint
TracingEndpoint = tracingEndpoint
// Jaeger tracer can be initialized with a transport that will
// report tracing Spans to a Zipkin backend
transport, err := zipkin.NewHTTPTransport(
tracingEndpoint,
zipkin.HTTPBatchSize(1),
zipkin.HTTPLogger(jaeger.StdLogger),
)
if err != nil {
log.Fatalf("Cannot initialize HTTP transport: %v", err)
}
// create Jaeger tracer
t, closer := jaeger.NewTracer(
name,
jaeger.NewConstSampler(true), // sample all traces
jaeger.NewRemoteReporter(transport))
tracer, traceCloser = t, closer
}
// Set the ambient tracer
opentracing.SetGlobalTracer(tracer)
// If a root span was requested, start it now.
if rootSpanName != "" {
var options []opentracing.StartSpanOption
for _, tag := range rootSpanTags() {
options = append(options, tag)
}
TracingRootSpan = tracer.StartSpan(rootSpanName, options...)
go collectMemStats(rootSpanName)
}
}
// CloseTracing ensures that all pending spans have been flushed. It should be called before process exit.
func CloseTracing() {
if !IsTracingEnabled() {
return
}
if TracingRootSpan != nil {
TracingRootSpan.Finish()
}
contract.IgnoreClose(traceCloser)
}
// Starts an AppDash server listening on any available TCP port
// locally and sends the spans and annotations to the given collector.
// Returns a Pulumi-formatted tracing endpoint pointing to this
// server.
//
// See https://github.com/sourcegraph/appdash/blob/master/cmd/appdash/example_app.go
func startProxyAppDashServer(collector appdash.Collector) (string, error) {
l, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
if err != nil {
return "", err
}
collectorPort := l.Addr().(*net.TCPAddr).Port
cs := appdash.NewServer(l, collector)
cs.Debug = true
cs.Trace = true
go cs.Start()
// The default sends to stderr, which is unfortunate for
// end-users. Discard for now.
cs.Log = log.New(io.Discard, "appdash", 0)
return fmt.Sprintf("tcp://127.0.0.1:%d", collectorPort), nil
}
// Computes initial tags to write to the `TracingRootSpan`, which can
// be useful for aggregating trace data in benchmarks.
func rootSpanTags() []opentracing.Tag {
tags := []opentracing.Tag{
{
Key: "os.Args",
Value: os.Args,
},
{
Key: "runtime.GOOS",
Value: runtime.GOOS,
},
{
Key: "runtime.GOARCH",
Value: runtime.GOARCH,
},
{
Key: "runtime.GOMAXPROCS",
Value: runtime.GOMAXPROCS(0),
},
{
Key: "runtime.NumCPU",
Value: runtime.NumCPU(),
},
}
// Promote all env vars `pulumi_tracing_tag_foo=bar` into tags `foo: bar`.
envPrefix := "pulumi_tracing_tag_"
for _, e := range os.Environ() {
pair := strings.SplitN(e, "=", 2)
envVarName := strings.ToLower(pair[0])
envVarValue := pair[1]
if strings.HasPrefix(envVarName, envPrefix) {
tags = append(tags, opentracing.Tag{
Key: strings.TrimPrefix(envVarName, envPrefix),
Value: envVarValue,
})
}
}
return tags
}
// Samples memory stats in the background at 1s intervals, and creates
// spans for the data. This is currently opt-in via
// `PULUMI_TRACING_MEMSTATS_POLL_INTERVAL=1s` or similar. Consider
// collecting this by default later whenever tracing is enabled as we
// calibrate that the overhead is low enough.
func collectMemStats(spanPrefix string) {
memStats := runtime.MemStats{}
maxStats := runtime.MemStats{}
poll := func() {
if TracingRootSpan == nil {
return
}
runtime.ReadMemStats(&memStats)
// report cumulative metrics as is
TracingRootSpan.SetTag("runtime.NumCgoCall", runtime.NumCgoCall())
TracingRootSpan.SetTag("MemStats.TotalAlloc", memStats.TotalAlloc)
TracingRootSpan.SetTag("MemStats.Mallocs", memStats.Mallocs)
TracingRootSpan.SetTag("MemStats.Frees", memStats.Frees)
TracingRootSpan.SetTag("MemStats.PauseTotalNs", memStats.PauseTotalNs)
TracingRootSpan.SetTag("MemStats.NumGC", memStats.NumGC)
// for other metrics report the max
if memStats.Sys > maxStats.Sys {
maxStats.Sys = memStats.Sys
TracingRootSpan.SetTag("MemStats.Sys.Max", maxStats.Sys)
}
if memStats.HeapAlloc > maxStats.HeapAlloc {
maxStats.HeapAlloc = memStats.HeapAlloc
TracingRootSpan.SetTag("MemStats.HeapAlloc.Max", maxStats.HeapAlloc)
}
if memStats.HeapSys > maxStats.HeapSys {
maxStats.HeapSys = memStats.HeapSys
TracingRootSpan.SetTag("MemStats.HeapSys.Max", maxStats.HeapSys)
}
if memStats.HeapIdle > maxStats.HeapIdle {
maxStats.HeapIdle = memStats.HeapIdle
TracingRootSpan.SetTag("MemStats.HeapIdle.Max", maxStats.HeapIdle)
}
if memStats.HeapInuse > maxStats.HeapInuse {
maxStats.HeapInuse = memStats.HeapInuse
TracingRootSpan.SetTag("MemStats.HeapInuse.Max", maxStats.HeapInuse)
}
if memStats.HeapReleased > maxStats.HeapReleased {
maxStats.HeapReleased = memStats.HeapReleased
TracingRootSpan.SetTag("MemStats.HeapReleased.Max", maxStats.HeapReleased)
}
if memStats.HeapObjects > maxStats.HeapObjects {
maxStats.HeapObjects = memStats.HeapObjects
TracingRootSpan.SetTag("MemStats.HeapObjects.Max", maxStats.HeapObjects)
}
if memStats.StackInuse > maxStats.StackInuse {
maxStats.StackInuse = memStats.StackInuse
TracingRootSpan.SetTag("MemStats.StackInuse.Max", maxStats.StackInuse)
}
if memStats.StackSys > maxStats.StackSys {
maxStats.StackSys = memStats.StackSys
TracingRootSpan.SetTag("MemStats.StackSys.Max", maxStats.StackSys)
}
}
interval := os.Getenv("PULUMI_TRACING_MEMSTATS_POLL_INTERVAL")
if interval != "" {
intervalDuration, err := time.ParseDuration(interval)
if err == nil {
for {
poll()
time.Sleep(intervalDuration)
}
}
}
}
// 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 contract
import (
"fmt"
)
const assertMsg = "An assertion has failed"
// Assert checks a condition and Fails if it is false.
//
// Deprecated: Use Assertf.
func Assert(cond bool) {
if !cond {
failfast(assertMsg)
}
}
// Assertf checks a condition and Failfs if it is false, formatting and logging the given message.
func Assertf(cond bool, msg string, args ...interface{}) {
if !cond {
failfast(fmt.Sprintf("%v: %v", assertMsg, fmt.Sprintf(msg, args...)))
}
}
// AssertNoError will Fail if the error is non-nil.
//
// Deprecated: Use AssertNoErrorf.
func AssertNoError(err error) {
if err != nil {
failfast(err.Error())
}
}
// AssertNoErrorf will Fail if the error is non-nil, adding the additional log message.
func AssertNoErrorf(err error, msg string, args ...interface{}) {
if err != nil {
failfast(fmt.Sprintf("error %v: %v. source error: %v", assertMsg, fmt.Sprintf(msg, args...), err))
}
}
// 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 contract
import (
"fmt"
)
const failMsg = "A failure has occurred"
// Fail unconditionally abandons the process.
//
// Deprecated: Use Failf.
func Fail() {
failfast(failMsg)
}
// Failf unconditionally abandons the process, formatting and logging the given message.
func Failf(msg string, args ...interface{}) {
failfast(fmt.Sprintf("%v: %v", failMsg, fmt.Sprintf(msg, args...)))
}
// 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 contract
import (
"fmt"
)
// failfast logs and panics the process in a way that is friendly to debugging.
func failfast(msg string) {
panic(fmt.Sprintf("fatal: %v", msg))
}
// 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 contract
import (
"io"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
)
// Ignore explicitly ignores a value. This is similar to `_ = x`, but tells linters ignoring is intentional.
func Ignore(v interface{}) {
// Log something at a VERY verbose level just in case it helps to track down issues (e.g., an error that was
// ignored that represents something even more egregious than the eventual failure mode). If this truly matters, it
// probably implies the ignore was not appropriate, but as a safeguard, logging seems useful.
logging.V(11).Infof("Explicitly ignoring and discarding result: %v", v)
}
// IgnoreClose closes and ignores the returned error. This makes defer closes easier.
func IgnoreClose(cr io.Closer) {
err := cr.Close()
IgnoreError(err)
}
// IgnoreError explicitly ignores an error. This is similar to `_ = x`, but tells linters ignoring is intentional.
// This routine is specifically for ignoring errors which is potentially more risky, and so logs at a higher level.
func IgnoreError(err error) {
// Log something at a verbose level just in case it helps to track down issues (e.g., an error that was
// ignored that represents something even more egregious than the eventual failure mode). If this truly matters, it
// probably implies the ignore was not appropriate, but as a safeguard, logging seems useful.
if err != nil {
logging.V(3).Infof("Explicitly ignoring and discarding error: %v", err)
}
}
// 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 contract
import (
"fmt"
)
const requireMsg = "A precondition has failed for %v"
// Require checks a precondition condition pertaining to a function parameter, and Fails if it is false.
//
// Deprecated: Use Requiref.
func Require(cond bool, param string) {
if !cond {
failfast(fmt.Sprintf(requireMsg, param))
}
}
// Requiref checks a precondition condition pertaining to a function parameter, and Failfs if it is false.
func Requiref(cond bool, param string, msg string, args ...interface{}) {
if !cond {
failfast(fmt.Sprintf("%v: %v", fmt.Sprintf(requireMsg, param), fmt.Sprintf(msg, args...)))
}
}
// Copyright 2016-2023, 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.
// A small library for creating typed, consistent and documented environmental variable
// accesses.
//
// Declaring a variable is as simple as declaring a module level constant.
//
// var Var = env.Bool("VAR", "A boolean variable")
//
// Typed values can be retrieved by calling `Var.Value()`.
package env
import (
"fmt"
"os"
"sort"
"strconv"
"strings"
)
// Store holds a collection of key, value? pairs.
//
// Store acts like the environment that values are drawn from.
type Store interface {
// Retrieve a raw value from the Store. If the value is not present, "", false should
// be returned.
Raw(key string) (string, bool)
}
// A strongly typed environment.
type Env interface {
GetString(val StringValue) string
GetBool(val BoolValue) bool
GetInt(val IntValue) int
}
// Create a new strongly typed Env from an untyped Store.
func NewEnv(store Store) Env {
return env{store}
}
type env struct{ s Store }
func (e env) GetString(val StringValue) string {
return StringValue{val.withStore(e.s)}.Value()
}
func (e env) GetBool(val BoolValue) bool {
return BoolValue{val.withStore(e.s)}.Value()
}
func (e env) GetInt(val IntValue) int {
return IntValue{val.withStore(e.s)}.Value()
}
type envStore struct{}
func (envStore) Raw(key string) (string, bool) {
return os.LookupEnv(key)
}
type MapStore map[string]string
func (m MapStore) Raw(key string) (string, bool) {
v, ok := m[key]
return v, ok
}
// The global store of values that Value.Value() uses.
//
// Setting this value is not thread safe, and should be restricted to testing.
var Global Store = envStore{}
// An environmental variable.
type Var struct {
name string
Value Value
Description string
options options
}
// The default prefix for a environmental variable.
const Prefix = "PULUMI_"
// The name of an environmental variable.
//
// Name accounts for prefix if appropriate.
func (v Var) Name() string {
return v.options.name(v.name)
}
// The alternative name of an environmental variable.
func (v Var) Alternative() string {
if v.options.alternative == "" {
return ""
}
return v.options.name(v.options.alternative)
}
// The list of variables that a must be truthy for `v` to be set.
func (v Var) Requires() []BoolValue { return v.options.prerequs }
var envVars []Var
// Variables is a list of variables declared.
func Variables() []Var {
vars := envVars
sort.SliceStable(vars, func(i, j int) bool {
return vars[i].Name() < vars[j].Name()
})
return vars
}
// An Option to configure a environmental variable.
type Option func(*options)
type options struct {
prerequs []BoolValue
noPrefix bool
secret bool
alternative string
}
func (o options) name(underlying string) string {
if o.noPrefix {
return underlying
}
return Prefix + underlying
}
// Needs indicates that a variable can only be set if `val` is truthy.
func Needs(val BoolValue) Option {
return func(o *options) {
o.prerequs = append(o.prerequs, val)
}
}
// NoPrefix indicates that a variable should not have the default prefix applied.
func NoPrefix(opts *options) {
opts.noPrefix = true
}
// Secret indicates that the value should not be displayed in plaintext.
func Secret(opts *options) {
opts.secret = true
}
// Alternative indicates that the variable has an alternative name. This is generally used for backwards compatibility.
func Alternative(name string) Option {
return func(opts *options) {
opts.alternative = name
}
}
// The value of a environmental variable.
//
// In general, `Value`s should only be used in collections. For specific values, used the
// typed version (StringValue, BoolValue).
//
// Every implementer of Value also includes a `Value() T` method that returns a typed
// representation of the value.
type Value interface {
fmt.Stringer
// Retrieve the underlying string value associated with the variable.
//
// If the variable was not set, ("", false) is returned.
Underlying() (string, bool)
// Retrieve the Var associated with this value.
Var() Var
Validate() ValidateError
// set the associated variable for the value. This is necessary since Value and Var
// are inherently cyclical.
setVar(Var)
// A type correct formatting for the value. This is used for display purposes and
// should not be quoted.
formattedValue() string
}
type ValidateError struct {
Warning error
Error error
}
// An implementation helper for Value. New Values should be a typed wrapper around *value.
type value struct {
variable Var
store Store
}
func (v value) withStore(store Store) *value {
v.store = store // This is non-mutating since `v` is taken by value.
return &v
}
func (v value) String() string {
_, present := v.Underlying()
if !present {
return "unset"
}
if m := v.missingPrerequs(); m != "" {
return fmt.Sprintf("need %s (%s)", m, v.Var().Value.formattedValue())
}
return v.Var().Value.formattedValue()
}
func (v *value) setVar(variable Var) {
v.variable = variable
}
func (v value) Var() Var {
return v.variable
}
func (v value) Underlying() (string, bool) {
s := v.store
if s == nil {
s = Global
}
raw, has := s.Raw(v.Var().Name())
if has {
return raw, true
}
alt := v.Var().Alternative()
if alt != "" {
return s.Raw(alt)
}
return "", false
}
func (v value) missingPrerequs() string {
for _, p := range v.variable.options.prerequs {
if !p.Value() {
return p.Var().Name()
}
}
return ""
}
// A string retrieved from the environment.
type StringValue struct{ *value }
func (StringValue) Type() string { return "string" }
func (s StringValue) formattedValue() string {
if s.variable.options.secret {
return "[secret]"
}
return fmt.Sprintf("%#v", s.Value())
}
func (StringValue) Validate() ValidateError { return ValidateError{} }
// The string value of the variable.
//
// If the variable is unset, "" is returned.
func (s StringValue) Value() string {
if s.missingPrerequs() != "" {
return ""
}
v, ok := s.Underlying()
if !ok {
return ""
}
return v
}
// A boolean retrieved from the environment.
type BoolValue struct{ *value }
func (BoolValue) Type() string { return "bool" }
func (b BoolValue) formattedValue() string {
return fmt.Sprintf("%#v", b.Value())
}
func (b BoolValue) Validate() ValidateError {
v, ok := b.Underlying()
if !ok || b.Value() || v == "0" || strings.EqualFold(v, "false") {
return ValidateError{}
}
return ValidateError{
Warning: fmt.Errorf("%#v is falsy, but doesn't look like a boolean", v),
}
}
// The boolean value of the variable.
//
// If the variable is unset, false is returned.
func (b BoolValue) Value() bool {
if b.missingPrerequs() != "" {
return false
}
v, ok := b.Underlying()
if !ok {
return false
}
return v == "1" || strings.EqualFold(v, "true")
}
// An integer retrieved from the environment.
type IntValue struct{ *value }
func (IntValue) Type() string { return "int" }
func (i IntValue) Validate() ValidateError {
v, ok := i.Underlying()
if !ok {
return ValidateError{}
}
_, err := strconv.ParseInt(v, 10, 64)
return ValidateError{
Error: err,
}
}
// The integer value of the variable.
//
// If the variable is unset or not parsable, 0 is returned.
func (i IntValue) Value() int {
if i.missingPrerequs() != "" {
return 0
}
v, ok := i.Underlying()
if !ok {
return 0
}
parsed, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return 0
}
return int(parsed)
}
func (i IntValue) formattedValue() string {
return fmt.Sprintf("%#v", i.Value())
}
func setVar(val Value, variable Var) Value {
variable.Value = val
val.setVar(variable)
envVars = append(envVars, variable)
return val
}
// Declare a new environmental value.
//
// `name` is the runtime name of the variable. Unless `NoPrefix` is passed, name is
// pre-appended with `Prefix`. For example, a variable named "FOO" would be set by
// declaring "PULUMI_FOO=val" in the enclosing environment.
//
// `description` is the string description of what the variable does.
func String(name, description string, opts ...Option) StringValue {
var options options
for _, opt := range opts {
opt(&options)
}
val := StringValue{&value{}}
variable := Var{
name: name,
Description: description,
options: options,
}
return setVar(val, variable).(StringValue)
}
// Declare a new environmental value of type bool.
//
// `name` is the runtime name of the variable. Unless `NoPrefix` is passed, name is
// pre-appended with `Prefix`. For example, a variable named "FOO" would be set by
// declaring "PULUMI_FOO=1" in the enclosing environment.
//
// `description` is the string description of what the variable does.
func Bool(name, description string, opts ...Option) BoolValue {
var options options
for _, opt := range opts {
opt(&options)
}
val := BoolValue{&value{}}
variable := Var{
name: name,
Description: description,
options: options,
}
return setVar(val, variable).(BoolValue)
}
// Declare a new environmental value of type integer.
//
// `name` is the runtime name of the variable. Unless `NoPrefix` is passed, name is
// pre-appended with `Prefix`. For example, a variable named "FOO" would be set by
// declaring "PULUMI_FOO=1" in the enclosing environment.
//
// `description` is the string description of what the variable does.
func Int(name, description string, opts ...Option) IntValue {
var options options
for _, opt := range opts {
opt(&options)
}
val := IntValue{&value{}}
variable := Var{
name: name,
Description: description,
options: options,
}
return setVar(val, variable).(IntValue)
}
// 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 httputil
import (
"context"
"net/http"
"strings"
"time"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/retry"
)
// RetryOpts defines options to configure the retry behavior.
// Leave nil for defaults.
type RetryOpts struct {
// These fields map directly to util.Acceptor.
Delay *time.Duration
Backoff *float64
MaxDelay *time.Duration
MaxRetryCount *int
// HandshakeTimeoutsOnly indicates whether we should only be retrying timeouts that occur during the TLS handshake.
// These timeouts are safe to retry even on POST requests, since we know the actual request hasn't been sent yet.
HandshakeTimeoutsOnly bool
}
// DoWithRetry calls client.Do, and in the case of an error, retries the operation again after a slight delay.
// Uses the default retry delays, starting at 100ms and ramping up to ~1.3s.
func DoWithRetry(req *http.Request, client *http.Client) (*http.Response, error) {
var opts RetryOpts
return doWithRetry(req, client, opts)
}
// DoWithRetryOpts calls client.Do, but retrying 500s (even for POSTs). Using the provided delays.
func DoWithRetryOpts(req *http.Request, client *http.Client, opts RetryOpts) (*http.Response, error) {
return doWithRetry(req, client, opts)
}
func doWithRetry(req *http.Request, client *http.Client, opts RetryOpts) (*http.Response, error) {
contract.Assertf(req.ContentLength == 0 || req.GetBody != nil,
"Retryable request must have no body or rewindable body")
inRange := func(test, lower, upper int) bool {
return lower <= test && test <= upper
}
// maxRetryCount is the number of times to try an http request before
// giving up an returning the last error.
maxRetryCount := 5
if opts.MaxRetryCount != nil {
maxRetryCount = *opts.MaxRetryCount
}
acceptor := retry.Acceptor{
// If the opts field is nil, retry.Until will provide defaults.
Delay: opts.Delay,
Backoff: opts.Backoff,
MaxDelay: opts.MaxDelay,
Accept: func(try int, _ time.Duration) (bool, interface{}, error) {
if try > 0 && req.GetBody != nil {
// Reset request body, if present, for retries.
rc, bodyErr := req.GetBody()
if bodyErr != nil {
return false, nil, bodyErr
}
req.Body = rc
}
res, resErr := client.Do(req)
if opts.HandshakeTimeoutsOnly {
if resErr != nil && strings.Contains(resErr.Error(), "net/http: TLS handshake timeout") {
// If we have a handshake timeout, we can retry the request.
return false, nil, nil
}
return true, res, resErr
}
if resErr == nil && !inRange(res.StatusCode, 500, 599) {
return true, res, nil
}
if try >= (maxRetryCount - 1) {
return true, res, resErr
}
// Close the response body, if present, since our caller can't.
if resErr == nil {
contract.IgnoreError(res.Body.Close())
}
return false, nil, nil
},
}
_, res, err := retry.Until(context.Background(), acceptor)
if err != nil {
return nil, err
}
return res.(*http.Response), nil
}
// GetWithRetry issues a GET request with the given client, and in the case of an error, retries the operation again
// after a slight delay.
func GetWithRetry(url string, client *http.Client) (*http.Response, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
return DoWithRetry(req, client)
}
// 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 logging
// Wrapper around the glog API that allows us to intercept all logging calls and manipulate them as
// necessary. This is primarily used so we can make a best effort approach to filtering out secrets
// from any logs we emit before they get written to log-files/stderr.
//
// Code in pulumi should use this package instead of directly importing glog itself. If any glog
// methods are needed that are not exported from this, they can be added, with the caveat that they
// should be updated to properly filter as well before forwarding things along.
import (
"encoding/json"
"errors"
"flag"
"fmt"
"strconv"
"strings"
"sync"
"github.com/golang/glog"
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
)
type Filter interface {
Filter(s string) string
}
var (
LogToStderr = false // true if logging is being redirected to stderr.
Verbose = 0 // >0 if verbose logging is enabled at a particular level.
LogFlow = false // true to flow logging settings to child processes.
)
var (
rwLock sync.RWMutex
filters []Filter
)
// VerboseLogger logs messages only if verbosity matches the level it was built with.
//
// It may be used as a boolean to check if it's enabled.
//
// if log := logging.V(lvl); log {
// log.Infoln(expensiveComputation())
// }
type VerboseLogger glog.Verbose
// Info is equivalent to the global Info function, guarded by the value of v.
// See the documentation of V for usage.
func (v VerboseLogger) Info(args ...interface{}) {
if v {
glog.Verbose(v).InfoDepth(1, FilterString(fmt.Sprint(args...)))
}
}
// Infoln is equivalent to the global Infoln function, guarded by the value of v.
// See the documentation of V for usage.
func (v VerboseLogger) Infoln(args ...interface{}) {
if v {
glog.Verbose(v).Infoln(FilterString(fmt.Sprint(args...)))
}
}
// Infof is equivalent to the global Infof function, guarded by the value of v.
// See the documentation of V for usage.
func (v VerboseLogger) Infof(format string, args ...interface{}) {
if v {
glog.Verbose(v).InfoDepthf(1, "%s", FilterString(fmt.Sprintf(format, args...)))
}
}
// V builds a logger that logs messages only if verbosity is at least at the provided level.
func V(level glog.Level) VerboseLogger {
return VerboseLogger(glog.V(level))
}
func Errorf(format string, args ...interface{}) {
glog.ErrorDepthf(1, "%s", FilterString(fmt.Sprintf(format, args...)))
}
func Infof(format string, args ...interface{}) {
glog.InfoDepthf(1, "%s", FilterString(fmt.Sprintf(format, args...)))
}
func Warningf(format string, args ...interface{}) {
glog.WarningDepthf(1, "%s", FilterString(fmt.Sprintf(format, args...)))
}
func Flush() {
glog.Flush()
}
func maybeSetFlag(name, value string) {
if f := flag.Lookup(name); f != nil {
err := f.Value.Set(value)
assertNoError(err)
}
}
// InitLogging ensures the logging library has been initialized with the given settings.
func InitLogging(logToStderr bool, verbose int, logFlow bool) {
// Remember the settings in case someone inquires.
LogToStderr = logToStderr
Verbose = verbose
LogFlow = logFlow
// glog uses golang's built in flags package to set configuration values, which is incompatible with how
// we use cobra. In order to accommodate this, we call flag.CommandLine.Parse() with an empty array and
// explicitly set the flags we care about here.
if !flag.Parsed() {
err := flag.CommandLine.Parse([]string{})
assertNoError(err)
}
if logToStderr {
maybeSetFlag("logtostderr", "true")
}
if verbose > 0 {
maybeSetFlag("v", strconv.Itoa(verbose))
}
}
func GetLogfilePath() (string, error) {
logFiles, err := glog.Names("INFO")
if err != nil {
return "", err
}
if len(logFiles) == 0 {
return "", errors.New("no log files found")
}
return logFiles[0], nil
}
func assertNoError(err error) {
if err != nil {
failfast(err.Error())
}
}
func failfast(msg string) {
panic(fmt.Sprintf("fatal: %v", msg))
}
type nopFilter struct{}
func (f *nopFilter) Filter(s string) string {
return s
}
type replacerFilter struct {
replacer *strings.Replacer
}
func (f *replacerFilter) Filter(s string) string {
return f.replacer.Replace(s)
}
func AddGlobalFilter(filter Filter) {
rwLock.Lock()
filters = append(filters, filter)
rwLock.Unlock()
}
func CreateFilter(secrets []string, replacement string) Filter {
items := slice.Prealloc[string](len(secrets))
for _, secret := range secrets {
// For short secrets, don't actually add them to the filter, this is a trade-off we make to prevent
// displaying `[secret]`. Travis does a similar thing, for example.
if len(secret) < 3 {
continue
}
items = append(items, secret, replacement)
// Catch secrets that are serialized to JSON.
bs, err := json.Marshal(secret)
if err != nil {
continue
}
if escaped := string(bs[1 : len(bs)-1]); escaped != secret {
items = append(items, escaped, replacement)
}
}
if len(items) > 0 {
return &replacerFilter{replacer: strings.NewReplacer(items...)}
}
return &nopFilter{}
}
func FilterString(msg string) string {
var localFilters []Filter
rwLock.RLock()
localFilters = filters
rwLock.RUnlock()
for _, filter := range localFilters {
msg = filter.Filter(msg)
}
return msg
}
// 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 mapper
import (
"fmt"
"reflect"
)
// MappingError represents a collection of decoding errors, defined below.
type MappingError interface {
error
Failures() []error // the full set of errors (each of one of the below types).
AddFailure(err error) // registers a new failure.
}
// mappingError is a concrete implementation of MappingError; it is private, and we prefer to use the above interface
// type, to avoid tricky non-nil nils in common usage patterns (see https://golang.org/doc/faq#nil_error).
type mappingError struct {
failures []error
}
var _ error = (*mappingError)(nil) // ensure this implements the error interface.
func NewMappingError(errs []error) MappingError {
return &mappingError{failures: errs}
}
func (e *mappingError) Failures() []error { return e.failures }
func (e *mappingError) AddFailure(err error) {
e.failures = append(e.failures, err)
}
func (e *mappingError) Error() string {
str := fmt.Sprintf("%d failures decoding:", len(e.failures))
for _, failure := range e.failures {
switch f := failure.(type) {
case FieldError:
str += fmt.Sprintf("\n\t%v: %v", f.Field(), f.Reason())
default:
str += fmt.Sprintf("\n\t%v", f)
}
}
return str
}
// FieldError represents a failure during decoding of a specific field.
type FieldError interface {
error
Field() string // returns the name of the field with a problem.
Reason() string // returns a full diagnostic string about the error.
}
// fieldError is used when a general purpose error occurs decoding a field.
type fieldError struct {
Type string
Fld string
Message string
}
var (
_ error = (*fieldError)(nil) // ensure this implements the error interface.
_ FieldError = (*fieldError)(nil) // ensure this implements the fieldError interface.
)
func NewFieldError(ty string, fld string, err error) FieldError {
return &fieldError{
Type: ty,
Fld: fld,
Message: fmt.Sprintf("An error occurred decoding '%v.%v': %v", ty, fld, err),
}
}
func NewTypeFieldError(ty reflect.Type, fld string, err error) FieldError {
return NewFieldError(ty.Name(), fld, err)
}
func (e *fieldError) Error() string { return e.Message }
func (e *fieldError) Field() string { return e.Fld }
func (e *fieldError) Reason() string { return e.Message }
// MissingError is used when a required field is missing on an object of a given type.
type MissingError struct {
Type reflect.Type
Fld string
Message string
}
var (
_ error = (*MissingError)(nil) // ensure this implements the error interface.
_ FieldError = (*MissingError)(nil) // ensure this implements the FieldError interface.
)
func NewMissingError(ty reflect.Type, fld string) *MissingError {
return &MissingError{
Type: ty,
Fld: fld,
Message: fmt.Sprintf("Missing required field '%v' on '%v'", fld, ty),
}
}
func (e *MissingError) Error() string { return e.Message }
func (e *MissingError) Field() string { return e.Fld }
func (e *MissingError) Reason() string { return e.Message }
// UnrecognizedError is used when a field is unrecognized on the given type.
type UnrecognizedError struct {
Type reflect.Type
Fld string
Message string
}
var (
_ error = (*UnrecognizedError)(nil) // ensure this implements the error interface.
_ FieldError = (*UnrecognizedError)(nil) // ensure this implements the FieldError interface.
)
func NewUnrecognizedError(ty reflect.Type, fld string) *UnrecognizedError {
return &UnrecognizedError{
Type: ty,
Fld: fld,
Message: fmt.Sprintf("Unrecognized field '%v' on '%v'", fld, ty),
}
}
func (e *UnrecognizedError) Error() string { return e.Message }
func (e *UnrecognizedError) Field() string { return e.Fld }
func (e *UnrecognizedError) Reason() string { return e.Message }
type WrongTypeError struct {
Type reflect.Type
Fld string
Expect reflect.Type
Actual reflect.Type
Message string
}
var (
_ error = (*WrongTypeError)(nil) // ensure this implements the error interface.
_ FieldError = (*WrongTypeError)(nil) // ensure this implements the FieldError interface.
)
func NewWrongTypeError(ty reflect.Type, fld string, expect reflect.Type, actual reflect.Type) *WrongTypeError {
return &WrongTypeError{
Type: ty,
Fld: fld,
Expect: expect,
Actual: actual,
Message: fmt.Sprintf(
"Field '%v' on '%v' must be a '%v'; got '%v' instead", fld, ty, expect, actual),
}
}
func (e *WrongTypeError) Error() string { return e.Message }
func (e *WrongTypeError) Field() string { return e.Fld }
func (e *WrongTypeError) Reason() string { return e.Message }
// 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 mapper
import (
"reflect"
"strings"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
// Mapper can map from weakly typed JSON-like property bags to strongly typed structs, and vice versa.
type Mapper interface {
// Decode decodes a JSON-like object into the target pointer to a structure.
Decode(obj map[string]interface{}, target interface{}) MappingError
// DecodeValue decodes a single JSON-like value (with a given type and name) into a target pointer to a structure.
DecodeValue(obj map[string]interface{}, ty reflect.Type, key string, target interface{}, optional bool) FieldError
// Encode encodes an object into a JSON-like in-memory object.
Encode(source interface{}) (map[string]interface{}, MappingError)
// EncodeValue encodes a value into its JSON-like in-memory value format.
EncodeValue(v interface{}) (interface{}, MappingError)
}
// New allocates a new mapper object with the given options.
func New(opts *Opts) Mapper {
var initOpts Opts
if opts != nil {
initOpts = *opts
}
if initOpts.CustomDecoders == nil {
initOpts.CustomDecoders = make(Decoders)
}
return &mapper{opts: initOpts}
}
// Opts controls the way mapping occurs; for default behavior, simply pass an empty struct.
type Opts struct {
Tags []string // the tag names to recognize (`json` and `pulumi` if unspecified).
OptionalTags []string // the tags to interpret to mean "optional" (`optional` if unspecified).
SkipTags []string // the tags to interpret to mean "skip" (`skip` if unspecified).
CustomDecoders Decoders // custom decoders.
IgnoreMissing bool // ignore missing required fields.
IgnoreUnrecognized bool // ignore unrecognized fields.
}
type mapper struct {
opts Opts
}
// Map decodes an entire map into a target object, using an anonymous decoder and tag-directed mappings.
func Map(obj map[string]interface{}, target interface{}) MappingError {
return New(nil).Decode(obj, target)
}
// MapI decodes an entire map into a target object, using an anonymous decoder and tag-directed mappings. This variant
// ignores any missing required fields in the payload in addition to any unrecognized fields.
func MapI(obj map[string]interface{}, target interface{}) MappingError {
return New(&Opts{
IgnoreMissing: true,
IgnoreUnrecognized: true,
}).Decode(obj, target)
}
// MapIM decodes an entire map into a target object, using an anonymous decoder and tag-directed mappings. This variant
// ignores any missing required fields in the payload.
func MapIM(obj map[string]interface{}, target interface{}) MappingError {
return New(&Opts{IgnoreMissing: true}).Decode(obj, target)
}
// MapIU decodes an entire map into a target object, using an anonymous decoder and tag-directed mappings. This variant
// ignores any unrecognized fields in the payload.
func MapIU(obj map[string]interface{}, target interface{}) MappingError {
return New(&Opts{IgnoreUnrecognized: true}).Decode(obj, target)
}
// Unmap translates an already mapped target object into a raw, unmapped form.
func Unmap(obj interface{}) (map[string]interface{}, error) {
return New(nil).Encode(obj)
}
// structFields digs into a type to fetch all fields, including its embedded structs, for a given type.
func structFields(t reflect.Type) []reflect.StructField {
contract.Assertf(t.Kind() == reflect.Struct,
"StructFields only valid on struct types; %v is not one (kind %v)", t, t.Kind())
var fldinfos []reflect.StructField
fldtypes := []reflect.Type{t}
for len(fldtypes) > 0 {
fldtype := fldtypes[0]
fldtypes = fldtypes[1:]
for i := 0; i < fldtype.NumField(); i++ {
if fldinfo := fldtype.Field(i); fldinfo.Anonymous {
// If an embedded struct, push it onto the queue to visit.
if fldinfo.Type.Kind() == reflect.Struct {
fldtypes = append(fldtypes, fldinfo.Type)
}
} else {
// Otherwise, we will go ahead and consider this field in our decoding.
fldinfos = append(fldinfos, fldinfo)
}
}
}
return fldinfos
}
// defaultTags fetches the mapper's tag names from the options, or supplies defaults if not present.
func (md *mapper) defaultTags() (tags []string, optionalTags []string, skipTags []string) {
if md.opts.Tags == nil {
tags = []string{"json", "pulumi"}
} else {
tags = md.opts.Tags
}
if md.opts.OptionalTags == nil {
optionalTags = []string{"omitempty", "optional"}
} else {
optionalTags = md.opts.OptionalTags
}
if md.opts.SkipTags == nil {
skipTags = []string{"skip"}
} else {
skipTags = md.opts.SkipTags
}
return tags, optionalTags, skipTags
}
// structFieldTags includes a field's information plus any parsed tags.
type structFieldTags struct {
Info reflect.StructField // the struct field info.
Optional bool // true if this can be missing.
Skip bool // true to skip a field.
Key string // the JSON key name.
}
// structFieldsTags digs into a type to fetch all fields, including embedded structs, plus any associated tags.
func (md *mapper) structFieldsTags(t reflect.Type) []structFieldTags {
// Fetch the tag names to use.
tags, optionalTags, skipTags := md.defaultTags()
// Now walk the field infos and parse the tags to create the requisite structures.
var fldtags []structFieldTags
for _, fldinfo := range structFields(t) {
for _, tagname := range tags {
if tag := fldinfo.Tag.Get(tagname); tag != "" {
var key string // the JSON key name.
var optional bool // true if this can be missing.
var skip bool // true if we should skip auto-marshaling.
// Decode the tag.
tagparts := strings.Split(tag, ",")
contract.Assertf(len(tagparts) > 0,
"Expected >0 tagparts on field %v.%v; got %v", t.Name(), fldinfo.Name, len(tagparts))
key = tagparts[0]
if key == "-" {
skip = true // a name of "-" means skip
}
for _, part := range tagparts[1:] {
var match bool
for _, optionalTag := range optionalTags {
if part == optionalTag {
optional = true
match = true
break
}
}
if !match {
for _, skipTag := range skipTags {
if part == skipTag {
skip = true
match = true
break
}
}
}
contract.Assertf(match, "Unrecognized tagpart on field %v.%v: %v", t.Name(), fldinfo.Name, part)
}
fldtags = append(fldtags, structFieldTags{
Key: key,
Optional: optional,
Skip: skip,
Info: fldinfo,
})
}
}
}
return fldtags
}
// 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 mapper
import (
"encoding"
"fmt"
"reflect"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
// Decoder is a func that knows how to decode into particular type.
type Decoder func(m Mapper, obj map[string]interface{}) (interface{}, error)
// Decoders is a map from type to a decoder func that understands how to decode that type.
type Decoders map[reflect.Type]Decoder
// Decode decodes an entire map into a target object, using tag-directed mappings.
func (md *mapper) Decode(obj map[string]interface{}, target interface{}) MappingError {
// Fetch the destination types and validate that we can store into the target (i.e., a valid lval).
vdst := reflect.ValueOf(target)
contract.Assertf(vdst.Kind() == reflect.Ptr && !vdst.IsNil() && vdst.Elem().CanSet(),
"Target %v must be a non-nil, settable pointer", vdst.Type())
vdstType := vdst.Type().Elem()
contract.Assertf(vdstType.Kind() == reflect.Struct && !vdst.IsNil(),
"Target %v must be a struct type with `pulumi:\"x\"` tags to direct decoding", vdstType)
// Keep track of any errors that result.
var errs []error
// For each field in the struct that has a `pulumi:"name"`, look it up in the map by that `name`, issuing an error
// if it is missing or of the wrong type. For each field that is marked optional, e.g. `pulumi:"name,optional"`,
// do the same, but permit it to be missing without issuing an error.
flds := make(map[string]bool)
for _, fldtag := range md.structFieldsTags(vdstType) {
// Use the tag to direct unmarshaling.
key := fldtag.Key
if !fldtag.Skip {
fld := vdst.Elem().FieldByName(fldtag.Info.Name)
if err := md.DecodeValue(obj, vdstType, key, fld.Addr().Interface(), fldtag.Optional); err != nil {
errs = append(errs, err)
}
}
// Remember this key so we can be sure not to reject it later when checking for unrecognized fields.
flds[key] = true
}
// Afterwards, if there are any unrecognized fields, issue an error.
if !md.opts.IgnoreUnrecognized {
for k := range obj {
if !flds[k] {
err := NewUnrecognizedError(vdstType, k)
errs = append(errs, err)
}
}
}
// If there are no errors, return nil; else manufacture a decode error object.
if len(errs) == 0 {
return nil
}
return NewMappingError(errs)
}
// DecodeValue decodes primitive type fields. For fields of complex types, we use custom deserialization.
func (md *mapper) DecodeValue(obj map[string]interface{}, ty reflect.Type, key string,
target interface{}, optional bool,
) FieldError {
vdst := reflect.ValueOf(target)
contract.Assertf(vdst.Kind() == reflect.Ptr && !vdst.IsNil() && vdst.Elem().CanSet(),
"Target %v must be a non-nil, settable pointer", vdst.Type())
if v, has := obj[key]; has {
// The field exists; okay, try to map it to the right type.
vsrc := reflect.ValueOf(v)
// If the source is a ptr, dereference it as necessary to get the underlying
// value.
for vsrc.IsValid() && vsrc.Type().Kind() == reflect.Ptr && !vsrc.IsNil() {
vsrc = vsrc.Elem()
}
// Ensure the source is valid; this is false if the value reflects the zero value.
if vsrc.IsValid() {
vdstType := vdst.Type().Elem()
// So long as the target element is a pointer, we have a pointer to pointer; dig through until we bottom out
// on the non-pointer type that matches the source. This assumes the source isn't itself a pointer!
contract.Assertf(vsrc.Type().Kind() != reflect.Ptr, "source is a null pointer")
for vdstType.Kind() == reflect.Ptr {
vdst = vdst.Elem()
vdstType = vdstType.Elem()
if !vdst.Elem().CanSet() {
// If the pointer is nil, initialize it so we can set it below.
contract.Assertf(vdst.IsNil(), "destination pointer must be nil")
vdst.Set(reflect.New(vdstType))
}
}
// Adjust the value if necessary; this handles recursive struct marshaling, interface unboxing, and more.
var err FieldError
if vsrc, err = md.adjustValueForAssignment(vsrc, vdstType, ty, key); err != nil {
return err
}
// Finally, provided everything is kosher, go ahead and store the value; otherwise, issue an error.
if vsrc.Type().AssignableTo(vdstType) {
vdst.Elem().Set(vsrc)
return nil
}
return NewWrongTypeError(ty, key, vdstType, vsrc.Type())
}
}
if !optional && !md.opts.IgnoreMissing {
// The field doesn't exist and yet it is required; issue an error.
return NewMissingError(ty, key)
}
return nil
}
var (
emptyObject = map[string]interface{}{}
textUnmarshalerType = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem()
)
// adjustValueForAssignment converts if possible to produce the target type.
func (md *mapper) adjustValueForAssignment(val reflect.Value,
to reflect.Type, ty reflect.Type, key string,
) (reflect.Value, FieldError) {
for !val.Type().AssignableTo(to) {
// The source cannot be assigned directly to the destination. Go through all known conversions.
if val.Type().ConvertibleTo(to) {
// A simple conversion exists to make this right.
val = val.Convert(to)
} else if to.Kind() == reflect.Ptr && val.Type().AssignableTo(to.Elem()) {
// Here the destination type (to) is a pointer to a type that accepts val.
var adjusted reflect.Value // var adjusted *toElem
if val.CanAddr() && val.Addr().Type().AssignableTo(to) {
// If taking the address of val makes this right, do it.
adjusted = val.Addr() // adjusted = &val
} else {
// Otherwise create a fresh pointer of the desired type and point it to val.
adjusted = reflect.New(to.Elem()) // adjusted = new(toElem)
adjusted.Elem().Set(val) // *adjusted = val
}
// In either case, the loop condition should be sastisfied at this point.
contract.Assertf(adjusted.Type().AssignableTo(to), "type %v is not assignable to %v", adjusted.Type(), to)
return adjusted, nil
} else if val.Kind() == reflect.Interface {
// It could be that the source is an interface{} with the right element type (or the right element type
// through a series of successive conversions); go ahead and give it a try.
val = val.Elem()
} else if val.Type().Kind() == reflect.Slice && to.Kind() == reflect.Slice {
// If a slice, everything's ok so long as the elements are compatible.
arr := reflect.New(to).Elem()
for i := 0; i < val.Len(); i++ {
elem := val.Index(i)
if !elem.Type().AssignableTo(to.Elem()) {
ekey := fmt.Sprintf("%v[%v]", key, i)
var err FieldError
if elem, err = md.adjustValueForAssignment(elem, to.Elem(), ty, ekey); err != nil {
return val, err
}
if !elem.Type().AssignableTo(to.Elem()) {
return val, NewWrongTypeError(ty, ekey, to.Elem(), elem.Type())
}
}
arr = reflect.Append(arr, elem)
}
val = arr
} else if val.Type().Kind() == reflect.Map && to.Kind() == reflect.Map {
// Similarly, if a map, everything's ok so long as elements and keys are compatible.
m := reflect.MakeMap(to)
for _, k := range val.MapKeys() {
entry := val.MapIndex(k)
if !k.Type().AssignableTo(to.Key()) {
kkey := fmt.Sprintf("%v[%v] key", key, k.Interface())
var err FieldError
if k, err = md.adjustValueForAssignment(k, to.Key(), ty, kkey); err != nil {
return val, err
}
if !k.Type().AssignableTo(to.Key()) {
return val, NewWrongTypeError(ty, kkey, to.Key(), k.Type())
}
}
if !entry.Type().AssignableTo(to.Elem()) {
ekey := fmt.Sprintf("%v[%v] value", key, k.Interface())
var err FieldError
if entry, err = md.adjustValueForAssignment(entry, to.Elem(), ty, ekey); err != nil {
return val, err
}
if !entry.Type().AssignableTo(to.Elem()) {
return val, NewWrongTypeError(ty, ekey, to.Elem(), entry.Type())
}
}
m.SetMapIndex(k, entry)
}
val = m
} else if val.Type() == reflect.TypeOf(emptyObject) {
// The value is an object and needs to be decoded into a value.
obj := val.Interface().(map[string]interface{})
if decode, has := md.opts.CustomDecoders[to]; has {
// A custom decoder exists; use it to unmarshal the type.
target, err := decode(md, obj)
if err != nil {
return val, NewTypeFieldError(ty, key, err)
}
val = reflect.ValueOf(target)
} else if to.Kind() == reflect.Struct || (to.Kind() == reflect.Ptr && to.Elem().Kind() == reflect.Struct) {
// If the target is a struct, we can use the built-in decoding logic.
var target interface{}
if to.Kind() == reflect.Ptr {
target = reflect.New(to.Elem()).Interface()
} else {
target = reflect.New(to).Interface()
}
if err := md.Decode(obj, target); err != nil {
return val, NewTypeFieldError(ty, key, err)
}
val = reflect.ValueOf(target).Elem()
} else {
return val, NewTypeFieldError(ty, key,
fmt.Errorf(
"Cannot decode Object{} to type %v; it isn't a struct, and no custom decoder exists", to))
}
} else if val.Type().Kind() == reflect.String {
// If the source is a string, see if the target implements encoding.TextUnmarshaler.
target := reflect.New(to)
if target.Type().Implements(textUnmarshalerType) {
um := target.Interface().(encoding.TextUnmarshaler)
if err := um.UnmarshalText([]byte(val.String())); err != nil {
return val, NewTypeFieldError(ty, key, err)
}
val = target.Elem()
} else {
break
}
} else {
break
}
}
return val, nil
}
// 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 mapper
import (
"reflect"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
// Encode encodes a strongly typed struct into a weakly typed JSON-like property bag.
func (md *mapper) Encode(source interface{}) (map[string]interface{}, MappingError) {
if source == nil {
return nil, nil
}
return md.encode(reflect.ValueOf(source))
}
func (md *mapper) encode(vsrc reflect.Value) (map[string]interface{}, MappingError) {
contract.Requiref(vsrc.IsValid(), "vsrc", "value must be valid")
// Fetch the type; if it's a pointer, do a quick nil check, otherwise operate on its underlying type.
vsrcType := vsrc.Type()
if vsrcType.Kind() == reflect.Ptr {
if vsrc.IsNil() {
return nil, nil
}
vsrc = vsrc.Elem()
vsrcType = vsrc.Type()
}
contract.Assertf(vsrcType.Kind() == reflect.Struct,
"Source %v must be a struct type with `pulumi:\"x\"` tags to direct encoding (kind %v)",
vsrcType, vsrcType.Kind())
// Fetch the source type, allocate a fresh object, and start encoding into it.
var errs []error
obj := make(map[string]interface{})
for _, fldtag := range md.structFieldsTags(vsrc.Type()) {
if !fldtag.Skip {
key := fldtag.Key
fld := vsrc.FieldByName(fldtag.Info.Name)
v, err := md.encodeValue(fld)
if err != nil {
errs = append(errs, err.Failures()...)
} else if v == nil {
if !fldtag.Optional && !md.opts.IgnoreMissing {
// The field doesn't exist and yet it is required; issue an error.
errs = append(errs, NewMissingError(vsrcType, key))
}
} else {
obj[key] = v
}
}
}
// If there are no errors, return nil; else manufacture a decode error object.
var err MappingError
if len(errs) > 0 {
err = NewMappingError(errs)
}
return obj, err
}
// EncodeValue decodes primitive type fields. For fields of complex types, we use custom deserialization.
func (md *mapper) EncodeValue(v interface{}) (interface{}, MappingError) {
if v == nil {
return nil, nil
}
return md.encodeValue(reflect.ValueOf(v))
}
func (md *mapper) encodeValue(vsrc reflect.Value) (interface{}, MappingError) {
contract.Requiref(vsrc.IsValid(), "vsrc", "value must be valid")
// Otherwise, try to map to the closest JSON-like destination type we can.
switch k := vsrc.Kind(); k {
// Primitive types:
case reflect.Bool:
return vsrc.Bool(), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return float64(vsrc.Int()), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return float64(vsrc.Uint()), nil
case reflect.Float32, reflect.Float64:
return vsrc.Float(), nil
case reflect.String:
return vsrc.String(), nil
// Pointers:
case reflect.Ptr:
if vsrc.IsNil() {
return nil, nil
}
return md.encodeValue(vsrc.Elem())
// Slices and maps:
case reflect.Slice:
if vsrc.IsNil() {
return nil, nil
}
slice := make([]interface{}, vsrc.Len())
var errs []error
for i := 0; i < vsrc.Len(); i++ {
ev := vsrc.Index(i)
if elem, err := md.encodeValue(ev); err != nil {
errs = append(errs, err.Failures()...)
} else {
slice[i] = elem
}
}
if errs == nil {
return slice, nil
}
return nil, NewMappingError(errs)
case reflect.Map:
if vsrc.IsNil() {
return nil, nil
}
ktype := vsrc.Type().Key()
contract.Assertf(ktype.Kind() == reflect.String, "expected map with string keys, got %v (%v)", ktype, ktype.Kind())
iter := vsrc.MapRange()
mmap := make(map[string]interface{}, vsrc.Len())
var errs []error
for iter.Next() {
if val, err := md.encodeValue(iter.Value()); err != nil {
errs = append(errs, err.Failures()...)
} else {
mmap[iter.Key().String()] = val
}
}
if errs == nil {
return mmap, nil
}
return nil, NewMappingError(errs)
// Structs and interface{}:
case reflect.Struct:
return md.encode(vsrc)
case reflect.Interface:
if vsrc.IsNil() {
return nil, nil
}
return md.encodeValue(vsrc.Elem())
// Cases we don't handle
case reflect.Invalid, reflect.Complex64, reflect.Complex128, reflect.Array,
reflect.Chan, reflect.Func, reflect.UnsafePointer:
contract.Failf("Unrecognized field type '%v' during encoding", k)
default:
contract.Failf("Unrecognized field type '%v' during encoding", k)
}
return nil, nil
}
// 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 mapper
import (
"reflect"
)
// AsObject attempts to coerce an existing value to an object map, returning a non-nil error if it cannot be done.
func AsObject(v interface{}, ty reflect.Type, key string) (map[string]interface{}, FieldError) {
if vmap, ok := v.(map[string]interface{}); ok {
return vmap, nil
}
return nil, NewWrongTypeError(
ty, key, reflect.TypeOf(make(map[string]interface{})), reflect.TypeOf(v))
}
// AsString attempts to coerce an existing value to a string, returning a non-nil error if it cannot be done.
func AsString(v interface{}, ty reflect.Type, key string) (*string, FieldError) {
if s, ok := v.(string); ok {
return &s, nil
}
return nil, NewWrongTypeError(ty, key, reflect.TypeOf(""), reflect.TypeOf(v))
}
// FieldObject looks up a field by name within an object map, coerces it to an object itself, and returns it. If the
// field exists but is not an object map, or it is missing and optional is false, a non-nil error is returned.
func FieldObject(obj map[string]interface{}, ty reflect.Type,
key string, optional bool,
) (map[string]interface{}, FieldError) {
if o, has := obj[key]; has {
return AsObject(o, ty, key)
} else if !optional {
// The field doesn't exist and yet it is required; issue an error.
return nil, NewMissingError(ty, key)
}
return nil, nil
}
// FieldString looks up a field by name within an object map, coerces it to a string, and returns it. If the
// field exists but is not a string, or it is missing and optional is false, a non-nil error is returned.
func FieldString(obj map[string]interface{}, ty reflect.Type, key string, optional bool) (*string, FieldError) {
if s, has := obj[key]; has {
return AsString(s, ty, key)
} else if !optional {
// The field doesn't exist and yet it is required; issue an error.
return nil, NewMissingError(ty, key)
}
return nil, nil
}
// Copyright 2016-2023, 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 result
import (
"errors"
"fmt"
"io"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
type bailError struct {
err error
}
// A BailError represents an expected error or a graceful failure -- that is
// something which is not a bug but a normal (albeit unhappy-path) part of the
// program's execution. A BailError implements the Error interface but will
// prefix its error string with "BAIL: ", which if ever seen in user-facing
// messages indicates that a check for bailing was missed. It also does *not*
// implement Unwrap. To ascertain whether an error is a BailError, use the
// IsBail function.
func BailError(err error) error {
contract.Requiref(err != nil, "err", "must not be nil")
return &bailError{err: err}
}
func (b *bailError) Error() string {
return fmt.Sprintf("BAIL: %v", b.err)
}
// BailErrorf is a helper for BailError(fmt.Errorf(...)).
func BailErrorf(format string, args ...interface{}) error {
return BailError(fmt.Errorf(format, args...))
}
// FprintBailf writes a formatted string to the given writer and returns a BailError with the same message.
func FprintBailf(w io.Writer, msg string, args ...any) error {
msg = fmt.Sprintf(msg, args...)
fmt.Fprintln(w, msg)
return BailError(errors.New(msg))
}
// IsBail returns true if any error in the given error's tree is a BailError.
func IsBail(err error) bool {
if err == nil {
return false
}
var bail *bailError
ok := errors.As(err, &bail)
return ok
}
// MergeBails accepts a set of errors and returns a single error that is the
// result of merging them according to the following criteria:
//
// - If all the errors are nil, MergeBails returns nil.
// - If any of the errors is *not* a BailError, MergeBails returns a single
// error whose message is the concatenation of the messages of all the
// errors which are not bails (that is, if any error is unexpected, MergeBails
// will propagate it).
// - In the remaining case that all errors are either nil or BailErrors, MergeBails
// will return a single BailError whose message is the concatenation of the
// messages of all the BailErrors.
func MergeBails(errs ...error) error {
allNil := true
joinableErrs := []error{}
for _, err := range errs {
if err == nil {
continue
}
allNil = false
if IsBail(err) {
continue
}
joinableErrs = append(joinableErrs, err)
}
if allNil {
return nil
}
if len(joinableErrs) == 0 {
return BailError(errors.Join(errs...))
}
return errors.Join(joinableErrs...)
}
// 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 retry
import (
"context"
"time"
)
type Acceptor struct {
Accept Acceptance // a function that determines when to proceed.
Delay *time.Duration // an optional delay duration.
Backoff *float64 // an optional backoff multiplier.
MaxDelay *time.Duration // an optional maximum delay duration.
}
// Acceptance is meant to accept a condition.
// It returns true when this condition has succeeded, and false otherwise
// (to which we respond by waiting and retrying after a certain period of time).
// If a non-nil error is returned, retrying halts.
// The interface{} data may be used to return final values to the caller.
//
// Try specifies the attempt number,
// zero indicating that this is the first attempt with no retries.
type Acceptance func(try int, nextRetryTime time.Duration) (success bool, result interface{}, err error)
const (
DefaultDelay time.Duration = 100 * time.Millisecond // by default, delay by 100ms
DefaultBackoff float64 = 1.5 // by default, backoff by 1.5x
DefaultMaxDelay time.Duration = 5 * time.Second // by default, no more than 5 seconds
)
// Retryer provides the ability to run and retry a fallible operation
// with exponential backoff.
type Retryer struct {
// Returns a channel that will send the time after the duration elapses.
//
// Defaults to time.After.
After func(time.Duration) <-chan time.Time
}
// Until runs the provided acceptor until one of the following conditions is met:
//
// - the operation succeeds: returns true and the result
// - the context expires: returns false and no result or errors
// - the operation returns an error: returns an error
//
// Note that the number of attempts is not limited.
// The Acceptance function is responsible for determining
// when to stop retrying.
func (r *Retryer) Until(ctx context.Context, acceptor Acceptor) (bool, interface{}, error) {
timeAfter := time.After
if r.After != nil {
timeAfter = r.After
}
// Prepare our delay and backoff variables.
var delay time.Duration
if acceptor.Delay == nil {
delay = DefaultDelay
} else {
delay = *acceptor.Delay
}
var backoff float64
if acceptor.Backoff == nil {
backoff = DefaultBackoff
} else {
backoff = *acceptor.Backoff
}
var maxDelay time.Duration
if acceptor.MaxDelay == nil {
maxDelay = DefaultMaxDelay
} else {
maxDelay = *acceptor.MaxDelay
}
// Loop until the condition is accepted or the context expires, whichever comes first.
try := 0
for {
if delay > maxDelay {
delay = maxDelay
}
// Try the acceptance condition; if it returns true, or an error, we are done.
b, data, err := acceptor.Accept(try, delay)
if b || err != nil {
return b, data, err
}
// Wait for delay or timeout.
select {
case <-timeAfter(delay):
// Continue on.
case <-ctx.Done():
return false, nil, nil
}
delay = time.Duration(float64(delay) * backoff)
try++
}
}
// Until waits until the acceptor accepts the current condition, or the context expires, whichever comes first. A
// return boolean of true means the acceptor eventually accepted; a non-nil error means the acceptor returned an error.
// If an acceptor accepts a condition after the context has expired, we ignore the expiration and return the condition.
//
// This uses [Retryer] with the default settings.
func Until(ctx context.Context, acceptor Acceptor) (bool, interface{}, error) {
return (&Retryer{}).Until(ctx, acceptor)
}
// UntilDeadline creates a child context with the given deadline, and then invokes the above Until function.
func UntilDeadline(ctx context.Context, acceptor Acceptor, deadline time.Time) (bool, interface{}, error) {
var cancel context.CancelFunc
ctx, cancel = context.WithDeadline(ctx, deadline)
b, data, err := Until(ctx, acceptor)
cancel()
return b, data, err
}
// UntilTimeout creates a child context with the given timeout, and then invokes the above Until function.
func UntilTimeout(ctx context.Context, acceptor Acceptor, timeout time.Duration) (bool, interface{}, error) {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
b, data, err := Until(ctx, acceptor)
cancel()
return b, data, err
}
// Copyright 2016-2024, 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 property
import "slices"
// An immutable Array of [Value]s.
//
// An Array is not itself a [Value], but it can be cheaply converted into a [Value] with
// [New].
type Array struct{ arr []Value }
// AsSlice copies the [Array] into a slice.
//
// AsSlice will return nil for an empty slice.
func (a Array) AsSlice() []Value {
// We always return nil because it's generally easy to work with nil slices in Go.
//
// We return a non-nil for Map.AsMap because it is painful to work with nil maps
// in Go.
return copyArray(a.arr)
}
// All calls yield for each element of the list.
//
// If yield returns false, then the iteration terminates.
//
// arr := property.NewArray([]property.Value{
// property.New(1),
// property.New(2),
// property.New(3),
// })
//
// arr.All(func(i int, v Value) bool {
// fmt.Printf("Index: %d, value: %s\n", i, v)
// return true
// })
//
// With Go 1.23, you can use iterator syntax to access each element:
//
// for i, v := range arr.All {
// fmt.Printf("Index: %d, value: %s\n", i, v)
// }
func (a Array) All(yield func(int, Value) bool) {
for k, v := range a.arr {
if !yield(k, v) {
return
}
}
}
// Get the value from an [Array] at an index.
//
// If idx is negative or if idx is greater then or equal to the length of the [Array],
// then this function will panic.
func (a Array) Get(idx int) Value {
return a.arr[idx]
}
// The length of the [Array].
func (a Array) Len() int {
return len(a.arr)
}
// Append a new value to the end of the [Array].
func (a Array) Append(v ...Value) Array {
// We need to copy a.arr since append may mutate the backing array, which may be
// shared.
if len(v) == 0 {
return a
}
return Array{append(copyArray(a.arr), v...)}
}
// NewArray creates a new [Array] from a slice of [Value]s. It is the inverse of
// [Array.AsSlice].
func NewArray(slice []Value) Array {
return Array{copyArray(slice)}
}
func copyArray[T any](a []T) []T {
if len(a) == 0 {
return nil
}
return slices.Clone(a)
}
// Copyright 2016-2024, 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 property
type EqualOption func(*eqOpts)
// See the doc comment for Value.Equals for the effect of EqualRelaxComputed.
func EqualRelaxComputed(opts *eqOpts) {
opts.relaxComputed = true
}
type eqOpts struct {
relaxComputed bool
}
// Check if two Values are equal.
//
// There are two corner cases that need to be called out here:
//
// - Secret equality is enforced. That means that:
//
// {"a", secret: false} == {"a", secret: false}
//
// {"a", secret: true} != {"a", secret: false}
//
// {"b", secret: false} != {"c", secret: false}
//
// - Computed value equality has two different modes. By default, it works like Null
// equality: a.IsComputed() => (a.Equals(b) <=> b.IsComputed()) (up to secrets and
// dependencies).
//
// If [EqualRelaxComputed] is passed, then computed values are considered equal to all
// other values. (up to secrets and dependencies)
func (v Value) Equals(other Value, opts ...EqualOption) bool {
var eqOpts eqOpts
for _, o := range opts {
o(&eqOpts)
}
return v.equals(other, eqOpts)
}
func (v Value) equals(other Value, opts eqOpts) bool {
if v.isSecret != other.isSecret {
return false
}
if len(v.dependencies) != len(other.dependencies) {
return false
}
for i, d := range v.dependencies {
if other.dependencies[i] != d {
return false
}
}
if opts.relaxComputed && (v.IsComputed() || other.IsComputed()) {
return true
}
switch {
case v.IsBool() && other.IsBool():
return v.AsBool() == other.AsBool()
case v.IsNumber() && other.IsNumber():
return v.AsNumber() == other.AsNumber()
case v.IsString() && other.IsString():
return v.AsString() == other.AsString()
case v.IsArray() && other.IsArray():
a1, a2 := v.AsArray(), other.AsArray()
if a1.Len() != a2.Len() {
return false
}
for i := range a1.arr {
if !a1.arr[i].equals(a2.arr[i], opts) {
return false
}
}
return true
case v.IsMap() && other.IsMap():
m1, m2 := v.AsMap(), other.AsMap()
if m1.Len() != m2.Len() {
return false
}
for k, v1 := range m1.m {
v2, ok := m2.m[k]
if !ok || !v1.equals(v2, opts) {
return false
}
}
return true
case v.IsAsset() && other.IsAsset():
a1, a2 := v.asAssetMut(), other.asAssetMut()
return a1.Equals(a2)
case v.IsArchive() && other.IsArchive():
a1, a2 := v.asArchiveMut(), other.asArchiveMut()
return a1.Equals(a2)
case v.IsResourceReference() && other.IsResourceReference():
r1, r2 := v.AsResourceReference(), other.AsResourceReference()
return r1.Equal(r2)
case v.IsNull() && other.IsNull():
return true
case v.IsComputed() && other.IsComputed():
return true
default:
return false
}
}
// Copyright 2016-2025, 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 property
import (
"fmt"
"strconv"
)
// [fmt.GoStringer] lets a type define the go syntax needed to define it.
//
// If this is implemented well, then you can copy a printed value into your code, which
// makes debugging a lot easier. With that goal in mind, we have chosen to print package
// level constructs prefixed by "property.", since most people debugging with property
// values will not be authors of the property package.
var (
_ fmt.GoStringer = Value{}
_ fmt.GoStringer = Map{}
_ fmt.GoStringer = Array{}
_ fmt.GoStringer = Null
_ fmt.GoStringer = Computed
)
func (v Value) GoString() string {
value := func(s string) string {
var withSecret, withDependencies string
if v.isSecret {
withSecret = ".WithSecret(true)"
}
if len(v.dependencies) > 0 {
withDependencies = fmt.Sprintf(".WithDependencies(%#v)", v.dependencies)
}
return fmt.Sprintf("property.New(%s)%s%s", s, withSecret, withDependencies)
}
valuef := func(a any) string { return value(fmt.Sprintf("%#v", a)) }
switch {
case v.IsBool(), v.IsString(), v.IsComputed(),
v.IsAsset(), v.IsArchive(), v.IsResourceReference():
return valuef(v.v)
// Go doesn't allow New(1), since 1 is a int literal, not a float64 literal.
//
// We want to make sure that we always print a valid float64 literal.
case v.IsNumber():
n := v.AsNumber()
s := strconv.FormatFloat(n, 'f', -1, 64)
if float64(int(n)) == n {
return value(s + ".0")
}
return value(s)
// Null is normalized to nil, so that Value{} is the same as New(Null).
case v.IsNull():
return valuef(Null)
// [New] accepts both an [Array] or a []Value,
case v.IsArray():
a := v.AsArray()
if len(a.arr) == 0 {
return valuef(a)
}
return valuef(a.arr)
case v.IsMap():
m := v.AsMap()
if len(m.m) == 0 {
return valuef(m)
}
return valuef(v.AsMap().m)
default:
panic(fmt.Sprintf("impossible - unknown type %T within a value", v.v))
}
}
func (a Array) GoString() string {
if len(a.arr) == 0 {
return "property.Array{}"
}
return fmt.Sprintf("property.NewArray(%#v)", a.arr)
}
func (a Map) GoString() string {
if len(a.m) == 0 {
return "property.Map{}"
}
return fmt.Sprintf("property.NewMap(%#v)", a.m)
}
func (null) GoString() string { return "property.Null" }
func (computed) GoString() string { return "property.Computed" }
// Copyright 2016-2024, 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 property
import (
"slices"
)
// An immutable Map of [Value]s.
type Map struct{ m map[string]Value }
// AsMap converts the [Map] into a native Go map from strings to [Values].
//
// AsMap always returns a non-nil map.
func (m Map) AsMap() map[string]Value {
// We always return non-nil because it's generally painful to work with nil maps
// in Go.
//
// We return a nil for Array.AsSlice because it is easy to work with nil slices in
// Go.
return copyMapNonNil(m.m)
}
// All calls yield for each key value pair in the Map. All iterates in random order, just
// like Go's native maps. For stable iteration order, use [Map.AllStable].
//
// If yield returns false, then the iteration terminates.
//
// m := property.NewMap(map[]property.Value{
// "one": property.New(1),
// "two": property.New(2),
// "three": property.New(3),
// })
//
// m.All(func(k string, v Value) bool {
// fmt.Printf("Key: %s, value: %s\n", k, v)
// return true
// })
//
// With Go 1.23, you can use iterator syntax to access each element:
//
// for k, v := range arr.All {
// fmt.Printf("Index: %s, value: %s\n", k, v)
// }
func (m Map) All(yield func(string, Value) bool) {
for k, v := range m.m {
if !yield(k, v) {
return
}
}
}
// AllStable calls yield for each key value pair in the Map in sorted key order.
//
// For usage, see [Map.All].
func (m Map) AllStable(yield func(string, Value) bool) {
keys := make([]string, 0, len(m.m))
for k := range m.m {
keys = append(keys, k)
}
slices.Sort(keys)
for _, k := range keys {
if !yield(k, m.m[k]) {
return
}
}
}
// Get retrieves the [Value] associated with key in the [Map]. If key is not in [Map],
// then a [Null] value is returned.
//
// To distinguish between a zero value and no value, use [Map.GetOk].
func (m Map) Get(key string) Value {
return m.m[key]
}
func (m Map) GetOk(key string) (Value, bool) {
v, ok := m.m[key]
return v, ok
}
// The number of elements in the [Map].
func (m Map) Len() int {
return len(m.m)
}
// Set produces a new map identical to the receiver with key mapped to value.
//
// Set does not mutate it's receiver.
func (m Map) Set(key string, value Value) Map {
cp := copyMapNonNil(m.m)
cp[key] = value
return Map{cp}
}
// Delete produces a new map identical to the receiver with given keys removed.
func (m Map) Delete(keys ...string) Map {
cp := copyMapMaybeNil(m.m)
for _, k := range keys {
delete(cp, k)
}
if len(cp) == 0 {
return Map{}
}
return Map{cp}
}
// NewMap creates a new map from m.
func NewMap(m map[string]Value) Map { return Map{copyMapMaybeNil(m)} }
func copyMapMaybeNil(m map[string]Value) map[string]Value {
if len(m) == 0 {
return nil
}
return copyMapNonNil(m)
}
func copyMapNonNil(m map[string]Value) map[string]Value {
cp := make(map[string]Value, len(m))
for k, v := range m {
cp[k] = v
}
return cp
}
// Copyright 2016-2024, 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 property
import (
"fmt"
)
// Path provides access and alteration methods on [Value]s.
//
// Paths are composed of [PathSegment]s, which can be one of:
//
// - [KeySegment]: For indexing into [Map]s.
// - [IndexSegment]: For indexing into [Array]s.
type Path []PathSegment
// Get the [Value] from v by applying the [Path].
//
// value := property.New(map[string]property.Value{
// "cities": property.New([]property.Value{
// property.New("Seattle"),
// property.New("London"),
// }),
// })
//
// firstCity := property.Path{
// property.NewSegment("cities"),
// property.NewSegment(0),
// }
//
// city, _ := firstCity.Get(value) // Seattle
//
// If the [Path] does not describe a value in v, then an error will be returned. The
// returned error can be safely cast to [PathApplyFailure].
func (p Path) Get(v Value) (Value, error) {
for _, segment := range p {
var err PathApplyFailure
v, err = segment.apply(v)
if err != nil {
return Value{}, err
}
}
return v, nil
}
// Set the value described by the path in src to newValue.
//
// Set does not mutate src, instead a copy of src with the change applied is returned. is
// returned that holds the change.
//
// Any returned error will implement [PathApplyFailure].
func (p Path) Set(src, newValue Value) (Value, error) {
if len(p) == 0 {
return newValue, nil
}
butLast, last := p[:len(p)-1], p[len(p)-1]
v, err := butLast.Get(src)
if err != nil {
return Value{}, err
}
switch {
case v.IsArray():
i, ok := last.(IndexSegment)
if !ok {
return Value{}, pathErrorf(v, "expected an IndexSegment, found %T", last)
}
slice := v.AsArray().AsSlice()
if i.int < 0 || i.int >= len(slice) {
return Value{}, pathApplyIndexOutOfBoundsError{found: v.AsArray(), idx: i.int}
}
slice[i.int] = newValue
return butLast.Set(src, New(slice))
case v.IsMap():
k, ok := last.(KeySegment)
if !ok {
return Value{}, pathErrorf(v, "expected a KeySegment, found %T", last)
}
return butLast.Set(src, New(v.AsMap().Set(k.string, newValue)))
default:
return Value{}, pathApplyKeyExpectedMapError{found: v}
}
}
// Alter changes the value at p by applying f.
//
// To preserve metadata, use [WithGoValue] in conjunction with Alter:
//
// p.Alter(v, func(v Value) Value) {
// return property.WithGoValue(v, "new-value")
// })
//
// This will preserve any secrets or dependencies encoded in `v`.
//
// Any returned error will implement [PathApplyFailure].
func (p Path) Alter(v Value, f func(v Value) Value) (Value, error) {
oldValue, err := p.Get(v)
if err != nil {
return Value{}, err
}
return p.Set(v, f(oldValue))
}
type PathSegment interface {
apply(Value) (Value, PathApplyFailure)
}
// NewSegment creates a new [PathSegment] suitable for use in [Path].
func NewSegment[T interface{ string | int }](v T) PathSegment {
switch v := any(v).(type) {
case string:
return KeySegment{v}
case int:
return IndexSegment{v}
default:
panic("impossible")
}
}
type PathApplyFailure interface {
error
// The last value in a path traversal successfully reached.
Found() Value
}
// KeySegment represents a traversal into a [Map] by a key.
//
// KeySegment does not support glob ("*") expansion. Values are treated as is.
//
// To create an KeySegment, use [NewSegment].
type KeySegment struct{ string }
func (k KeySegment) apply(v Value) (Value, PathApplyFailure) {
if v.IsMap() {
m := v.AsMap()
r, ok := m.GetOk(k.string)
if ok {
return r, nil
}
return Value{}, pathApplyKeyMissingError{found: m, needle: k.string}
}
return Value{}, pathApplyKeyExpectedMapError{found: v}
}
// IndexSegment represents an index into an [Array].
//
// To create an IndexSegment, use [NewSegment].
type IndexSegment struct{ int }
func (k IndexSegment) apply(v Value) (Value, PathApplyFailure) {
if v.IsArray() {
a := v.AsArray()
if k.int < 0 || k.int >= a.Len() {
return Value{}, pathApplyIndexOutOfBoundsError{found: a, idx: k.int}
}
return a.Get(k.int), nil
}
return Value{}, pathApplyIndexExpectedArrayError{found: v}
}
type pathApplyKeyExpectedMapError struct {
found Value
}
func (err pathApplyKeyExpectedMapError) Error() string {
return "expected a map, found a " + typeString(err.found)
}
func (err pathApplyKeyExpectedMapError) Found() Value {
return err.found
}
type pathApplyKeyMissingError struct {
found Map
needle string
}
func (err pathApplyKeyMissingError) Error() string {
return fmt.Sprintf("missing key %q in map", err.needle)
}
func (err pathApplyKeyMissingError) Found() Value {
return New(err.found)
}
type pathApplyIndexExpectedArrayError struct {
found Value
}
func (err pathApplyIndexExpectedArrayError) Error() string {
return "expected an array, found a " + typeString(err.found)
}
func (err pathApplyIndexExpectedArrayError) Found() Value {
return err.found
}
type pathApplyIndexOutOfBoundsError struct {
found Array
idx int
}
func (err pathApplyIndexOutOfBoundsError) Found() Value {
return New(err.found)
}
func (err pathApplyIndexOutOfBoundsError) Error() string {
return fmt.Sprintf("index %d out of bounds of an array of length %d",
err.idx, err.found.Len())
}
func pathErrorf(v Value, msg string, a ...any) PathApplyFailure {
return pathApplyError{found: v, msg: fmt.Sprintf(msg, a...)}
}
type pathApplyError struct {
found Value
msg string
}
func (err pathApplyError) Error() string { return err.msg }
func (err pathApplyError) Found() Value { return err.found }
// Copyright 2024, 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 property
import "github.com/pulumi/pulumi/sdk/v3/go/common/resource/urn"
// ResourceReference is a property value that represents a reference to a Resource. The reference captures the
// resource's URN, ID, and the version of its containing package. Note that there are several cases to consider with
// respect to the ID:
//
// - The reference may not contain an ID if the referenced resource is a component resource. In this case, the ID will
// be Null.
// - The ID may be unknown (in which case it will be the Computed property value)
// - Otherwise, the ID must be a string.
type ResourceReference struct {
URN urn.URN
ID Value
PackageVersion string
}
func (ref ResourceReference) IDString() (value string, hasID bool) {
switch {
case ref.ID.IsComputed():
return "", true
case ref.ID.IsString():
return ref.ID.AsString(), true
default:
return "", false
}
}
func (ref ResourceReference) Equal(other ResourceReference) bool {
if ref.URN != other.URN {
return false
}
if ref.PackageVersion != other.PackageVersion {
return false
}
vid, oid := ref.ID, other.ID
return vid.Equals(oid, EqualRelaxComputed)
}
// Copyright 2024, 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 property
import "fmt"
// typeString provides a type name for v.
//
// typeString should be used only to generate error messages. Do not rely on typeString
// providing stable output.
func typeString(v Value) string {
switch {
case v.IsArchive():
return "archive"
case v.IsArray():
return "array"
case v.IsAsset():
return "asset"
case v.IsBool():
return "bool"
case v.IsComputed():
return "computed"
case v.IsMap():
return "map"
case v.IsNull():
return "null"
case v.IsNumber():
return "number"
case v.IsResourceReference():
return "resource reference"
case v.IsString():
return "string"
default:
panic(fmt.Sprintf("unknown type %T", v.v))
}
}
// Copyright 2016-2024, 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 property
import (
"fmt"
"slices"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/archive"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/asset"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/urn"
)
type (
Asset = *asset.Asset
Archive = *archive.Archive
)
// Value is an imitable representation of a Pulumi property value. To create a new Value
// from a typed Go value, see [New]. To create a new Value from an untyped any value, see
// [Any].
//
// It may represent any type in [GoValue], included the [Computed] value. In addition,
// values may be secret and/or have resource dependencies.
//
// The zero value of Value is null, and is valid for use.
type Value struct {
isSecret bool
dependencies []urn.URN // the dependencies associated with this value.
// The inner go value for the Value.
//
// Note: null{} is not a valid value for v. null{} should be normalized to nil
// during creation, so that the zero value of Value is bit for bit equivalent to
// `New(Null)`.
v any
}
// GoValue defines the set of go values that can be contained inside a [Value].
//
// Value can also be a null value.
type GoValue interface {
bool | float64 | string | // Primitive types
Map | map[string]Value | // Map types
Array | []Value | // Array types
Asset | Archive | // Pulumi types
ResourceReference | // Resource references
computed | null // marker singletons
}
// New creates a new Value from a GoValue.
//
// To create a new value from an unknown type, use [Any].
func New[T GoValue](goValue T) Value {
return Value{v: normalize(goValue)}
}
func normalize(goValue any) any {
switch goValue := goValue.(type) {
case map[string]Value:
if goValue == nil {
return nil
}
return NewMap(goValue)
case []Value:
if goValue == nil {
return nil
}
return NewArray(goValue)
case Archive:
if goValue == nil {
return nil
}
return copyArchive(goValue)
case Asset:
if goValue == nil {
return nil
}
return copyAsset(goValue)
case null:
return nil
}
return goValue
}
// Any creates a new [Value] from a [GoValue] of unknown type. An error is returned if
// goValue is not a member of [GoValue].
func Any(goValue any) (Value, error) {
switch goValue := goValue.(type) {
case bool:
return New(goValue), nil
case float64:
return New(goValue), nil
case string:
return New(goValue), nil
case Array:
return New(goValue), nil
case []Value:
return New(goValue), nil
case Map:
return New(goValue), nil
case map[string]Value:
return New(goValue), nil
case Asset:
return New(goValue), nil
case Archive:
return New(goValue), nil
case ResourceReference:
return New(goValue), nil
case computed:
return New(goValue), nil
case nil, null:
return Value{}, nil
default:
return Value{}, fmt.Errorf("invalid type: %s of type %[1]T", goValue)
}
}
// Computed and Null are marker values of distinct singleton types.
//
// Because the type of the variable is a singleton, it is not possible to mutate these
// values (there is no other value to mutate to).
var (
// Mark a property as an untyped computed value.
//
// value := property.New(property.Computed)
Computed computed
// Mark a property as an untyped null value.
//
// value := property.New(property.Null)
//
// [Value]s can be null, and a null value *is not* equivalent to the absence of a
// value.
Null null
)
// Singleton marker types.
//
// These types are intentionally private. Users should instead use the available exported
// values.
type (
computed struct{}
null struct{}
)
func is[T GoValue](v Value) bool {
_, ok := v.v.(T)
return ok
}
func asMut[T GoValue](v Value) T { return v.v.(T) }
func (v Value) IsBool() bool { return is[bool](v) }
func (v Value) IsNumber() bool { return is[float64](v) }
func (v Value) IsString() bool { return is[string](v) }
func (v Value) IsArray() bool { return is[Array](v) }
func (v Value) IsMap() bool { return is[Map](v) }
func (v Value) IsAsset() bool { return is[Asset](v) }
func (v Value) IsArchive() bool { return is[Archive](v) }
func (v Value) IsResourceReference() bool { return is[ResourceReference](v) }
func (v Value) IsNull() bool { return v.v == nil }
func (v Value) IsComputed() bool { return is[computed](v) }
// Copy by value types don't distinguish between mutable and non-mutable copies.
func (v Value) AsBool() bool { return asMut[bool](v) }
func (v Value) AsNumber() float64 { return asMut[float64](v) }
func (v Value) AsString() string { return asMut[string](v) }
func (v Value) AsResourceReference() ResourceReference { return asMut[ResourceReference](v) }
func (v Value) AsAsset() Asset { return copyAsset(asMut[Asset](v)) }
func (v Value) AsArchive() Archive { return copyArchive(asMut[Archive](v)) }
func (v Value) AsArray() Array { return asMut[Array](v) }
func (v Value) AsMap() Map { return asMut[Map](v) }
// copyAsset peforms a deep copy of an asset.
func copyAsset(a Asset) Asset {
return &asset.Asset{
Sig: a.Sig,
Hash: a.Hash,
Text: a.Text,
Path: a.Path,
URI: a.URI,
}
}
func copyArchive(a Archive) Archive {
assets := make(map[string]any, len(a.Assets))
for k, v := range a.Assets {
switch v := v.(type) {
case Asset:
assets[k] = copyAsset(v)
case Archive:
assets[k] = copyArchive(v)
case nil:
assets[k] = v
default:
msg := "Unknown type within property.Archive, expected either property.Asset or property.Archive, found %T"
panic(fmt.Sprintf(msg, v))
}
}
return &archive.Archive{
Sig: a.Sig,
Hash: a.Hash,
Assets: assets,
Path: a.Path,
URI: a.URI,
}
}
// as*Mut act as interior escapes
func (v Value) asAssetMut() Asset { return asMut[Asset](v) }
func (v Value) asArchiveMut() Archive { return asMut[Archive](v) }
// Secret returns true if the [Value] is secret.
//
// It does not check if there are nested values that are secret. To recursively check if
// the [Value] contains a secret, use [Value.HasSecrets].
func (v Value) Secret() bool { return v.isSecret }
// HasSecrets returns true if the Value or any nested Value is secret.
func (v Value) HasSecrets() bool {
var hasSecret bool
v.visit(func(v Value) bool {
hasSecret = v.isSecret
return !hasSecret
})
return hasSecret
}
// WithSecret produces a new [Value] identical to it's receiver except that it's secret
// market is set to isSecret.
func (v Value) WithSecret(isSecret bool) Value {
v.isSecret = isSecret
return v
}
// HasComputed returns true if the Value or any nested Value is computed.
//
// To check if the receiver is itself computed, use [Value.IsComputed].
func (v Value) HasComputed() bool {
var hasComputed bool
v.visit(func(v Value) bool {
hasComputed = v.IsComputed()
return !hasComputed
})
return hasComputed
}
// Dependencies returns the dependency set of v.
//
// To set the dependencies of a value, use [Value.WithDependencies].
func (v Value) Dependencies() []urn.URN {
// Create a copy of v.dependencies to keep v immutable.
return slices.Clone(v.dependencies)
}
// WithDependencies returns a new value identical to the receiver, except that it has as
// it's dependencies the passed in value.
func (v Value) WithDependencies(dependencies []urn.URN) Value {
// Create a copy of dependencies to keep v immutable.
//
// We don't want exiting references to dependencies to be able to effect
// v.dependencies.
cp := copyArray(dependencies)
// Sort the dependencies on ingestion so that Equals doesn't care about
// dependency order.
slices.Sort(cp)
// Finally, deduplicate the dependencies, since a Value can't depend on the same
// resource more then once.
v.dependencies = slices.Compact(cp)
return v
}
// WithGoValue creates a new Value with the inner value newGoValue.
//
// To set a [Value] to a null or computed value, pass [Null] or [Computed] as the new
// value.
func WithGoValue[T GoValue](value Value, newGoValue T) Value {
value.v = normalize(newGoValue)
return value
}
// Copyright 2016-2024, 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 property
// Visit each Value in the within v.
//
// Parents are visited before their children.
func (v Value) visit(f func(Value) (continueWalking bool)) bool {
cont := f(v)
if !cont {
return false
}
switch {
case v.IsArray():
for _, v := range v.AsArray().arr {
if !v.visit(f) {
return false
}
}
case v.IsMap():
for _, v := range v.AsMap().m {
if !v.visit(f) {
return false
}
}
}
return true
}