// 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 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{})
// Stringify stringifies a diagnostic into a prefix and message that is appropriate for printing.
Stringify(sev Severity, diag *Diag, args ...interface{}) (string, string)
}
// 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 {
Pwd string // the working directory.
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 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", "")
// 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.`)
// 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 nil, errors.New("BatchEncrypt not supported for base64Crypter")
}
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"
"strconv"
"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 := adjustObjectValue(v)
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 = adjustObjectValue(v)
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
}
// adjustObjectValue returns a more suitable value for objects:
func adjustObjectValue(v Value) (object, error) {
// If it's a secure value or an object, return as-is.
if v.Secure() || v.Object() {
return v.unmarshalObject()
}
if v.typ == TypeString {
return newObject(v.value), nil
} else if v.typ == TypeInt {
i, err := strconv.Atoi(v.value)
if err != nil {
return object{}, err
}
return newObject(int64(i)), nil
} else if v.typ == TypeBool {
return newObject(v.value == "true"), nil
} else if v.typ == TypeFloat {
f, err := strconv.ParseFloat(v.value, 64)
if err != nil {
return object{}, err
}
return newObject(f), nil
}
// If "false" or "true", return the boolean value.
if v.value == "false" {
return newObject(false), nil
} else if v.value == "true" {
return newObject(true), nil
}
// 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.value) > 1 && v.value[0] == '0' {
return v.unmarshalObject()
}
// If it's convertible to an int, return the int.
if i, err := strconv.ParseInt(v.value, 10, 64); err == nil {
return newObject(i), nil
}
if i, err := strconv.ParseUint(v.value, 10, 64); err == nil {
return newObject(i), nil
}
// Otherwise, just return the string value.
return v.unmarshalObject()
}
// 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"
"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
secure bool
}
// 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 | []object | map[string]object
}
// newObject creates a new object with the given representation.
func newObject[T objectType](v T) object {
return object{value: v}
}
// newSecureObject creates a new secure object with the given ciphertext.
func newSecureObject(ciphertext string) object {
return object{value: ciphertext, secure: true}
}
// 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 string:
return c.secure
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:
if !c.secure {
return NewPlaintext(v), nil
}
plaintext, err := decrypter.DecryptValue(ctx, v)
if err != nil {
return Plaintext{}, fmt.Errorf("%v: %w", path, err)
}
return NewSecurePlaintext(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, c.secure = newContainer(path[0]), false
} 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 string:
if c.secure {
plaintext, err := dec.DecryptValue(context.TODO(), v)
if err != nil {
return nil, err
}
return []string{plaintext}, nil
}
return nil, 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 string:
if !root && c.secure {
return map[string]any{"secure": c.value}
}
return c.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, c.secure, 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 {
c.value, c.secure = text, secure
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 newSecureObject(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 str, ok := c.value.(string); ok && c.secure {
type secureValue struct {
Secure string `json:"secure" yaml:"secure"`
}
return secureValue{Secure: str}
}
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, string) {
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, valString
}
}
}
return false, ""
}
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
}
// 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 | []Plaintext | map[string]Plaintext
}
// Plaintext is a single plaintext config value.
type Plaintext struct {
value any
secure bool
}
// 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}
}
// NewSecurePlaintext creates a new secure string with the given plaintext.
func NewSecurePlaintext(plaintext string) Plaintext {
return Plaintext{value: plaintext, secure: true}
}
// 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 string:
return c.secure
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
default:
return v
}
}
func (c Plaintext) PropertyValue() resource.PropertyValue {
var prop resource.PropertyValue
switch v := c.Value().(type) {
case bool:
prop = resource.NewBoolProperty(v)
case int64:
prop = resource.NewNumberProperty(float64(v))
case uint64:
prop = resource.NewNumberProperty(float64(v))
case float64:
prop = resource.NewNumberProperty(v)
case string:
prop = resource.NewStringProperty(v)
case []Plaintext:
vs := make([]resource.PropertyValue, len(v))
for i, v := range v {
vs[i] = v.PropertyValue()
}
prop = resource.NewArrayProperty(vs)
case map[string]Plaintext:
vs := make(map[resource.PropertyKey]resource.PropertyValue, len(v))
for k, v := range v {
vs[resource.PropertyKey(k)] = v.PropertyValue()
}
prop = resource.NewObjectProperty(vs)
case nil:
prop = resource.NewNullProperty()
default:
contract.Failf("unexpected value of type %T", v)
return resource.PropertyValue{}
}
if c.secure {
prop = resource.MakeSecret(prop)
}
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:
if !c.secure {
return newObject(v), nil
}
ciphertext, err := encrypter.EncryptValue(ctx, v)
if err != nil {
return object{}, fmt.Errorf("%v: %w", path, err)
}
return newSecureObject(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) {
if str, ok := c.Value().(string); ok {
return str, 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"
)
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
}
func (c Value) unmarshalObject() (object, error) {
var obj object
if c.object || c.typ == TypeUnknown {
err := obj.UnmarshalString(c.value, c.secure, c.object)
return obj, err
}
return adjustObjectValue(c)
}
// 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 = NewArrayProperty(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 = NewArrayProperty(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 = NewObjectProperty(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()
// 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
}
for i := range oldArray {
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
}
}
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()
// 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
}
for i := range oldArray {
if !p[1:].reset(oldArray[i], newArray[i], oldIsSecret, newIsSecret) {
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(NewObjectProperty(old), NewObjectProperty(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 = NewBoolProperty(v.AsBool())
case v.IsNumber():
r = NewNumberProperty(v.AsNumber())
case v.IsString():
r = NewStringProperty(v.AsString())
case v.IsArray():
vArr := v.AsArray().AsSlice()
arr := make([]PropertyValue, len(vArr))
for i, vElem := range vArr {
arr[i] = ToResourcePropertyValue(vElem)
}
r = NewArrayProperty(arr)
case v.IsMap():
r = NewObjectProperty(ToResourcePropertyMap(v.AsMap()))
case v.IsAsset():
r = NewAssetProperty(v.AsAsset())
case v.IsArchive():
r = NewArchiveProperty(v.AsArchive())
case v.IsResourceReference():
ref := v.AsResourceReference()
r = NewResourceReferenceProperty(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 = NewOutputProperty(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
}
// NewGoal allocates a new resource goal state.
func NewGoal(t tokens.Type, name string, custom bool, props PropertyMap,
parent URN, protect *bool, dependencies []URN, provider string, initErrors []string,
propertyDependencies map[PropertyKey][]URN, deleteBeforeReplace *bool, ignoreChanges []string,
additionalSecretOutputs []PropertyKey, aliases []Alias, id ID, customTimeouts *CustomTimeouts,
replaceOnChanges []string, retainOnDelete *bool, deletedWith URN, sourcePosition string,
) *Goal {
g := &Goal{
Type: t,
Name: name,
Custom: custom,
Properties: props,
Parent: parent,
Protect: protect,
Dependencies: dependencies,
Provider: provider,
InitErrors: initErrors,
PropertyDependencies: propertyDependencies,
DeleteBeforeReplace: deleteBeforeReplace,
IgnoreChanges: ignoreChanges,
AdditionalSecretOutputs: additionalSecretOutputs,
Aliases: aliases,
ID: id,
ReplaceOnChanges: replaceOnChanges,
RetainOnDelete: retainOnDelete,
DeletedWith: deletedWith,
SourcePosition: sourcePosition,
}
if customTimeouts != nil {
g.CustomTimeouts = *customTimeouts
}
return g
}
// 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}
}
// 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.
//
//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).
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
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.
}
// 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,
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,
IgnoreChanges: s.IgnoreChanges,
ReplaceOnChanges: s.ReplaceOnChanges,
}
}
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 creates a new resource value from existing resource state information.
func NewState(t tokens.Type, urn URN, custom bool, del bool, id ID,
inputs PropertyMap, outputs PropertyMap, parent URN, protect bool,
external bool, dependencies []URN, initErrors []string, provider string,
propertyDependencies map[PropertyKey][]URN, pendingReplacement bool,
additionalSecretOutputs []PropertyKey, aliases []URN, timeouts *CustomTimeouts,
importID ID, retainOnDelete bool, deletedWith URN, created *time.Time, modified *time.Time,
sourcePosition string, ignoreChanges []string, replaceOnChanges []string,
) *State {
contract.Assertf(t != "", "type was empty")
contract.Assertf(custom || id == "", "is custom or had empty ID")
s := &State{
Type: t,
URN: urn,
Custom: custom,
Delete: del,
ID: id,
Inputs: inputs,
Outputs: outputs,
Parent: parent,
Protect: protect,
External: external,
Dependencies: dependencies,
InitErrors: initErrors,
Provider: provider,
PropertyDependencies: propertyDependencies,
PendingReplacement: pendingReplacement,
AdditionalSecretOutputs: additionalSecretOutputs,
Aliases: aliases,
ImportID: importID,
RetainOnDelete: retainOnDelete,
DeletedWith: deletedWith,
Created: created,
Modified: modified,
SourcePosition: sourcePosition,
IgnoreChanges: ignoreChanges,
ReplaceOnChanges: replaceOnChanges,
}
if timeouts != nil {
s.CustomTimeouts = *timeouts
}
return s
}
// 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
}
// 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 {
var err error
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-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"
)
// 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 that 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 = map[SystemName]system{
AppVeyor: baseCI{
Name: AppVeyor,
EnvVarsToDetect: []string{"APPVEYOR"},
},
AWSCodeBuild: baseCI{
Name: AWSCodeBuild,
EnvVarsToDetect: []string{"CODEBUILD_BUILD_ARN"},
},
AtlassianBamboo: baseCI{
Name: AtlassianBamboo,
EnvVarsToDetect: []string{"bamboo_planKey"},
},
AtlassianBitbucketPipelines: bitbucketPipelinesCI{
baseCI: baseCI{
Name: AtlassianBitbucketPipelines,
EnvVarsToDetect: []string{"BITBUCKET_COMMIT"},
},
},
AzurePipelines: azurePipelinesCI{
baseCI: baseCI{
Name: AzurePipelines,
EnvVarsToDetect: []string{"TF_BUILD"},
},
},
Buildkite: buildkiteCI{
baseCI: baseCI{
Name: Buildkite,
EnvVarsToDetect: []string{"BUILDKITE"},
},
},
CircleCI: circleCICI{
baseCI: baseCI{
Name: CircleCI,
EnvVarsToDetect: []string{"CIRCLECI"},
},
},
Codefresh: codefreshCI{
baseCI: baseCI{
Name: Codefresh,
EnvVarsToDetect: []string{"CF_BUILD_URL"},
},
},
Codeship: baseCI{
Name: Codeship,
EnvValuesToDetect: map[string]string{"CI_NAME": "codeship"},
},
Drone: baseCI{
Name: Drone,
EnvVarsToDetect: []string{"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: genericCICI{
baseCI: baseCI{
Name: SystemName(os.Getenv("PULUMI_CI_SYSTEM")),
EnvVarsToDetect: []string{"PULUMI_CI_SYSTEM"},
},
},
GitHubActions: githubActionsCI{
baseCI{
Name: GitHubActions,
EnvVarsToDetect: []string{"GITHUB_ACTIONS"},
},
},
GitLab: gitlabCI{
baseCI: baseCI{
Name: GitLab,
EnvVarsToDetect: []string{"GITLAB_CI"},
},
},
GoCD: baseCI{
Name: GoCD,
EnvVarsToDetect: []string{"GO_PIPELINE_LABEL"},
},
Hudson: baseCI{
Name: Hudson,
EnvVarsToDetect: []string{"HUDSON_URL"},
},
Jenkins: jenkinsCI{
baseCI: baseCI{
Name: Jenkins,
EnvVarsToDetect: []string{"JENKINS_URL"},
},
},
MagnumCI: baseCI{
Name: MagnumCI,
EnvVarsToDetect: []string{"MAGNUM"},
},
Semaphore: baseCI{
Name: Semaphore,
EnvVarsToDetect: []string{"SEMAPHORE"},
},
Spacelift: 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",
},
},
TaskCluster: baseCI{
Name: TaskCluster,
EnvVarsToDetect: []string{"TASK_ID", "RUN_ID"},
},
TeamCity: baseCI{
Name: TeamCity,
EnvVarsToDetect: []string{"TEAMCITY_VERSION"},
},
Travis: 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"
)
// 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}
}
// 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.Stdout, 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.Stdout, 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 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"
"golang.org/x/sys/unix"
)
// 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 unix.Kill(-pid, unix.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, unix.ESRCH) || // no such process
errors.Is(err, unix.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 {
case endpointURL.Scheme == "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 endpointURL.Scheme == "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"
"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 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.
// The Pulumi value system (formerly resource.PropertyValue)
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 value.
//
// It may represent any type in GoValue. In addition, values may be secret or
// computed. It may have resource dependencies.
//
// The zero value of Value is null.
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.
v.dependencies = copyArray(dependencies)
// Sort the dependencies on ingestion so that Equals doesn't care about
// dependency order.
slices.Sort(v.dependencies)
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
}