Merge branch 'main' of github.com:hashicorp/terraform into gcs-imp-v2
This commit is contained in:
commit
b243dbd93d
|
@ -1,6 +1,8 @@
|
|||
## Next Major Release
|
||||
|
||||
(nothing yet)
|
||||
NEW FEATURES:
|
||||
|
||||
* lang/funcs: add a new `type()` function, only available in `terraform console` [GH-28501]
|
||||
|
||||
## Previous Releases
|
||||
|
||||
|
|
|
@ -107,7 +107,7 @@ func (b *Local) opApply(
|
|||
|
||||
v, err := op.UIIn.Input(stopCtx, &terraform.InputOpts{
|
||||
Id: "approve",
|
||||
Query: query,
|
||||
Query: "\n" + query,
|
||||
Description: desc,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
|
@ -6,11 +6,11 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
|
||||
"github.com/hashicorp/terraform/version"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
k8sSchema "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
@ -114,16 +114,17 @@ func New() backend.Backend {
|
|||
DefaultFunc: schema.EnvDefaultFunc("KUBE_CLUSTER_CA_CERT_DATA", ""),
|
||||
Description: "PEM-encoded root certificates bundle for TLS authentication.",
|
||||
},
|
||||
"config_paths": {
|
||||
Type: schema.TypeList,
|
||||
Elem: &schema.Schema{Type: schema.TypeString},
|
||||
Optional: true,
|
||||
Description: "A list of paths to kube config files. Can be set with KUBE_CONFIG_PATHS environment variable.",
|
||||
},
|
||||
"config_path": {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
DefaultFunc: schema.MultiEnvDefaultFunc(
|
||||
[]string{
|
||||
"KUBE_CONFIG",
|
||||
"KUBECONFIG",
|
||||
},
|
||||
"~/.kube/config"),
|
||||
Description: "Path to the kube config file, defaults to ~/.kube/config",
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
DefaultFunc: schema.EnvDefaultFunc("KUBE_CONFIG_PATH", ""),
|
||||
Description: "Path to the kube config file. Can be set with KUBE_CONFIG_PATH environment variable.",
|
||||
},
|
||||
"config_context": {
|
||||
Type: schema.TypeString,
|
||||
|
@ -285,15 +286,7 @@ func getInitialConfig(data *schema.ResourceData) (*restclient.Config, error) {
|
|||
var cfg *restclient.Config
|
||||
var err error
|
||||
|
||||
c := &cli.BasicUi{Writer: os.Stdout}
|
||||
|
||||
inCluster := data.Get("in_cluster_config").(bool)
|
||||
cf := data.Get("load_config_file").(bool)
|
||||
|
||||
if !inCluster && !cf {
|
||||
c.Output(noConfigError)
|
||||
}
|
||||
|
||||
if inCluster {
|
||||
cfg, err = restclient.InClusterConfig()
|
||||
if err != nil {
|
||||
|
@ -313,13 +306,34 @@ func getInitialConfig(data *schema.ResourceData) (*restclient.Config, error) {
|
|||
}
|
||||
|
||||
func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) {
|
||||
path, err := homedir.Expand(d.Get("config_path").(string))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
loader := &clientcmd.ClientConfigLoadingRules{}
|
||||
|
||||
configPaths := []string{}
|
||||
if v, ok := d.Get("config_path").(string); ok && v != "" {
|
||||
configPaths = []string{v}
|
||||
} else if v, ok := d.Get("config_paths").([]interface{}); ok && len(v) > 0 {
|
||||
for _, p := range v {
|
||||
configPaths = append(configPaths, p.(string))
|
||||
}
|
||||
} else if v := os.Getenv("KUBE_CONFIG_PATHS"); v != "" {
|
||||
configPaths = filepath.SplitList(v)
|
||||
}
|
||||
|
||||
loader := &clientcmd.ClientConfigLoadingRules{
|
||||
ExplicitPath: path,
|
||||
expandedPaths := []string{}
|
||||
for _, p := range configPaths {
|
||||
path, err := homedir.Expand(p)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] Could not expand path: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("[DEBUG] Using kubeconfig: %s", path)
|
||||
expandedPaths = append(expandedPaths, path)
|
||||
}
|
||||
|
||||
if len(expandedPaths) == 1 {
|
||||
loader.ExplicitPath = expandedPaths[0]
|
||||
} else {
|
||||
loader.Precedence = expandedPaths
|
||||
}
|
||||
|
||||
overrides := &clientcmd.ConfigOverrides{}
|
||||
|
@ -367,13 +381,13 @@ func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) {
|
|||
cfg, err := cc.ClientConfig()
|
||||
if err != nil {
|
||||
if pathErr, ok := err.(*os.PathError); ok && os.IsNotExist(pathErr.Err) {
|
||||
log.Printf("[INFO] Unable to load config file as it doesn't exist at %q", path)
|
||||
log.Printf("[INFO] Unable to load config file as it doesn't exist at %q", pathErr.Path)
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Failed to load config (%s%s): %s", path, ctxSuffix, err)
|
||||
return nil, fmt.Errorf("Failed to initialize kubernetes configuration: %s", err)
|
||||
}
|
||||
|
||||
log.Printf("[INFO] Successfully loaded config file (%s%s)", path, ctxSuffix)
|
||||
log.Printf("[INFO] Successfully initialized config")
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/client-go/dynamic"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth" // Import to initialize client auth plugins.
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
coordinationv1 "k8s.io/api/coordination/v1"
|
||||
|
|
|
@ -704,6 +704,9 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend
|
|||
|
||||
// Check if we need to use the local backend to run the operation.
|
||||
if b.forceLocal || !w.Operations {
|
||||
// Record that we're forced to run operations locally to allow the
|
||||
// command package UI to operate correctly
|
||||
b.forceLocal = true
|
||||
return b.local.Operation(ctx, op)
|
||||
}
|
||||
|
||||
|
@ -949,6 +952,10 @@ func (b *Remote) VerifyWorkspaceTerraformVersion(workspaceName string) tfdiags.D
|
|||
return diags
|
||||
}
|
||||
|
||||
func (b *Remote) IsLocalOperations() bool {
|
||||
return b.forceLocal
|
||||
}
|
||||
|
||||
// Colorize returns the Colorize structure that can be used for colorizing
|
||||
// output. This is guaranteed to always return a non-nil value and so useful
|
||||
// as a helper to wrap any potentially colored strings.
|
||||
|
|
|
@ -82,8 +82,10 @@ func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceReques
|
|||
|
||||
if !envVal.IsNull() {
|
||||
for k, v := range envVal.AsValueMap() {
|
||||
entry := fmt.Sprintf("%s=%s", k, v.AsString())
|
||||
env = append(env, entry)
|
||||
if !v.IsNull() {
|
||||
entry := fmt.Sprintf("%s=%s", k, v.AsString())
|
||||
env = append(env, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,7 +95,9 @@ func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceReques
|
|||
var cmdargs []string
|
||||
if !intrVal.IsNull() && intrVal.LengthInt() > 0 {
|
||||
for _, v := range intrVal.AsValueSlice() {
|
||||
cmdargs = append(cmdargs, v.AsString())
|
||||
if !v.IsNull() {
|
||||
cmdargs = append(cmdargs, v.AsString())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if runtime.GOOS == "windows" {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package localexec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
@ -204,3 +205,48 @@ func TestResourceProvisioner_StopClose(t *testing.T) {
|
|||
p.Stop()
|
||||
p.Close()
|
||||
}
|
||||
|
||||
func TestResourceProvisioner_nullsInOptionals(t *testing.T) {
|
||||
output := cli.NewMockUi()
|
||||
p := New()
|
||||
schema := p.GetSchema().Provisioner
|
||||
|
||||
for i, cfg := range []cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"command": cty.StringVal("echo OK"),
|
||||
"environment": cty.MapVal(map[string]cty.Value{
|
||||
"FOO": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"command": cty.StringVal("echo OK"),
|
||||
"environment": cty.NullVal(cty.Map(cty.String)),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"command": cty.StringVal("echo OK"),
|
||||
"interpreter": cty.ListVal([]cty.Value{cty.NullVal(cty.String)}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"command": cty.StringVal("echo OK"),
|
||||
"interpreter": cty.NullVal(cty.List(cty.String)),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"command": cty.StringVal("echo OK"),
|
||||
"working_dir": cty.NullVal(cty.String),
|
||||
}),
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||
|
||||
cfg, err := schema.CoerceValue(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// verifying there are no panics
|
||||
p.ProvisionResource(provisioners.ProvisionResourceRequest{
|
||||
Config: cfg,
|
||||
UIOutput: output,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -128,6 +128,10 @@ func (p *provisioner) Close() error {
|
|||
func generateScripts(inline cty.Value) ([]string, error) {
|
||||
var lines []string
|
||||
for _, l := range inline.AsValueSlice() {
|
||||
if l.IsNull() {
|
||||
return nil, errors.New("invalid null string in 'scripts'")
|
||||
}
|
||||
|
||||
s := l.AsString()
|
||||
if s == "" {
|
||||
return nil, errors.New("invalid empty string in 'scripts'")
|
||||
|
@ -169,11 +173,14 @@ func collectScripts(v cty.Value) ([]io.ReadCloser, error) {
|
|||
|
||||
if scriptList := v.GetAttr("scripts"); !scriptList.IsNull() {
|
||||
for _, script := range scriptList.AsValueSlice() {
|
||||
if script.IsNull() {
|
||||
return nil, errors.New("invalid null string in 'script'")
|
||||
}
|
||||
s := script.AsString()
|
||||
if s == "" {
|
||||
return nil, errors.New("invalid empty string in 'script'")
|
||||
}
|
||||
scripts = append(scripts, script.AsString())
|
||||
scripts = append(scripts, s)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package remoteexec
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"testing"
|
||||
|
@ -274,3 +275,46 @@ func TestResourceProvisioner_connectionRequired(t *testing.T) {
|
|||
t.Fatalf("expected 'missing connection' error: got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceProvisioner_nullsInOptionals(t *testing.T) {
|
||||
output := cli.NewMockUi()
|
||||
p := New()
|
||||
schema := p.GetSchema().Provisioner
|
||||
|
||||
for i, cfg := range []cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"script": cty.StringVal("echo"),
|
||||
"inline": cty.NullVal(cty.List(cty.String)),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"inline": cty.ListVal([]cty.Value{
|
||||
cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"script": cty.NullVal(cty.String),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"scripts": cty.NullVal(cty.List(cty.String)),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"scripts": cty.ListVal([]cty.Value{
|
||||
cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||
|
||||
cfg, err := schema.CoerceValue(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// verifying there are no panics
|
||||
p.ProvisionResource(provisioners.ProvisionResourceRequest{
|
||||
Config: cfg,
|
||||
UIOutput: output,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
remoteBackend "github.com/hashicorp/terraform/backend/remote"
|
||||
"github.com/hashicorp/terraform/command/arguments"
|
||||
"github.com/hashicorp/terraform/command/views"
|
||||
"github.com/hashicorp/terraform/plans/planfile"
|
||||
|
@ -26,6 +27,11 @@ func (c *ApplyCommand) Run(rawArgs []string) int {
|
|||
common, rawArgs := arguments.ParseView(rawArgs)
|
||||
c.View.Configure(common)
|
||||
|
||||
// Propagate -no-color for the remote backend's legacy use of Ui. This
|
||||
// should be removed when the remote backend is migrated to views.
|
||||
c.Meta.color = !common.NoColor
|
||||
c.Meta.Color = c.Meta.color
|
||||
|
||||
// Parse and validate flags
|
||||
args, diags := arguments.ParseApply(rawArgs)
|
||||
|
||||
|
@ -116,10 +122,13 @@ func (c *ApplyCommand) Run(rawArgs []string) int {
|
|||
return op.Result.ExitStatus()
|
||||
}
|
||||
|
||||
// // Render the resource count and outputs
|
||||
view.ResourceCount(args.State.StateOutPath)
|
||||
if !c.Destroy && op.State != nil {
|
||||
view.Outputs(op.State.RootModule().OutputValues)
|
||||
// Render the resource count and outputs, unless we're using the remote
|
||||
// backend locally, in which case these are rendered remotely
|
||||
if rb, isRemoteBackend := be.(*remoteBackend.Remote); !isRemoteBackend || rb.IsLocalOperations() {
|
||||
view.ResourceCount(args.State.StateOutPath)
|
||||
if !c.Destroy && op.State != nil {
|
||||
view.Outputs(op.State.RootModule().OutputValues)
|
||||
}
|
||||
}
|
||||
|
||||
view.Diagnostics(diags)
|
||||
|
|
|
@ -127,6 +127,10 @@ func (c *ConsoleCommand) Run(args []string) int {
|
|||
c.showDiagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
// set the ConsoleMode to true so any available console-only functions included.
|
||||
scope.ConsoleMode = true
|
||||
|
||||
if diags.HasErrors() {
|
||||
diags = diags.Append(tfdiags.SimpleWarning("Due to the problems above, some expressions may produce unexpected results."))
|
||||
}
|
||||
|
|
105
command/login.go
105
command/login.go
|
@ -4,8 +4,10 @@ import (
|
|||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
|
@ -131,9 +133,9 @@ func (c *LoginCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
// If login service is unavailable, check for a TFE v2 API as fallback
|
||||
var service *url.URL
|
||||
var tfeservice *url.URL
|
||||
if clientConfig == nil {
|
||||
service, err = host.ServiceURL("tfe.v2")
|
||||
tfeservice, err = host.ServiceURL("tfe.v2")
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// Success!
|
||||
|
@ -184,6 +186,8 @@ func (c *LoginCommand) Run(args []string) int {
|
|||
oauthToken, tokenDiags = c.interactiveGetTokenByCode(hostname, credsCtx, clientConfig)
|
||||
case clientConfig.SupportedGrantTypes.Has(disco.OAuthOwnerPasswordGrant) && hostname == svchost.Hostname("app.terraform.io"):
|
||||
// The password grant type is allowed only for Terraform Cloud SaaS.
|
||||
// Note this case is purely theoretical at this point, as TFC currently uses
|
||||
// its own bespoke login protocol (tfe)
|
||||
oauthToken, tokenDiags = c.interactiveGetTokenByPassword(hostname, credsCtx, clientConfig)
|
||||
default:
|
||||
tokenDiags = tokenDiags.Append(tfdiags.Sourceless(
|
||||
|
@ -195,8 +199,8 @@ func (c *LoginCommand) Run(args []string) int {
|
|||
if oauthToken != nil {
|
||||
token = svcauth.HostCredentialsToken(oauthToken.AccessToken)
|
||||
}
|
||||
} else if service != nil {
|
||||
token, tokenDiags = c.interactiveGetTokenByUI(hostname, credsCtx, service)
|
||||
} else if tfeservice != nil {
|
||||
token, tokenDiags = c.interactiveGetTokenByUI(hostname, credsCtx, tfeservice)
|
||||
}
|
||||
|
||||
diags = diags.Append(tokenDiags)
|
||||
|
@ -220,19 +224,104 @@ func (c *LoginCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
c.Ui.Output("\n---------------------------------------------------------------------------------\n")
|
||||
c.Ui.Output(
|
||||
fmt.Sprintf(
|
||||
c.Colorize().Color(strings.TrimSpace(`
|
||||
if hostname == "app.terraform.io" { // Terraform Cloud
|
||||
var motd struct {
|
||||
Message string `json:"msg"`
|
||||
Errors []interface{} `json:"errors"`
|
||||
}
|
||||
|
||||
// Throughout the entire process of fetching a MOTD from TFC, use a default
|
||||
// message if the platform-provided message is unavailable for any reason -
|
||||
// be it the service isn't provided, the request failed, or any sort of
|
||||
// platform error returned.
|
||||
|
||||
motdServiceURL, err := host.ServiceURL("motd.v1")
|
||||
if err != nil {
|
||||
c.logMOTDError(err)
|
||||
c.outputDefaultTFCLoginSuccess()
|
||||
return 0
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", motdServiceURL.String(), nil)
|
||||
if err != nil {
|
||||
c.logMOTDError(err)
|
||||
c.outputDefaultTFCLoginSuccess()
|
||||
return 0
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+token.Token())
|
||||
|
||||
resp, err := httpclient.New().Do(req)
|
||||
if err != nil {
|
||||
c.logMOTDError(err)
|
||||
c.outputDefaultTFCLoginSuccess()
|
||||
return 0
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
c.logMOTDError(err)
|
||||
c.outputDefaultTFCLoginSuccess()
|
||||
return 0
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
json.Unmarshal(body, &motd)
|
||||
|
||||
if motd.Errors == nil && motd.Message != "" {
|
||||
c.Ui.Output(
|
||||
c.Colorize().Color(motd.Message),
|
||||
)
|
||||
return 0
|
||||
} else {
|
||||
c.logMOTDError(fmt.Errorf("platform responded with errors or an empty message"))
|
||||
c.outputDefaultTFCLoginSuccess()
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
if tfeservice != nil { // Terraform Enterprise
|
||||
c.outputDefaultTFELoginSuccess(dispHostname)
|
||||
} else {
|
||||
c.Ui.Output(
|
||||
fmt.Sprintf(
|
||||
c.Colorize().Color(strings.TrimSpace(`
|
||||
[green][bold]Success![reset] [bold]Terraform has obtained and saved an API token.[reset]
|
||||
|
||||
The new API token will be used for any future Terraform command that must make
|
||||
authenticated requests to %s.
|
||||
`)),
|
||||
dispHostname,
|
||||
) + "\n",
|
||||
)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *LoginCommand) outputDefaultTFELoginSuccess(dispHostname string) {
|
||||
c.Ui.Output(
|
||||
fmt.Sprintf(
|
||||
c.Colorize().Color(strings.TrimSpace(`
|
||||
[green][bold]Success![reset] [bold]Logged in to Terraform Enterprise (%s)[reset]
|
||||
`)),
|
||||
dispHostname,
|
||||
) + "\n",
|
||||
)
|
||||
}
|
||||
|
||||
return 0
|
||||
func (c *LoginCommand) outputDefaultTFCLoginSuccess() {
|
||||
c.Ui.Output(
|
||||
fmt.Sprintf(
|
||||
c.Colorize().Color(strings.TrimSpace(`
|
||||
[green][bold]Success![reset] [bold]Logged in to Terraform Cloud[reset]
|
||||
`)),
|
||||
) + "\n",
|
||||
)
|
||||
}
|
||||
|
||||
func (c *LoginCommand) logMOTDError(err error) {
|
||||
log.Printf("[TRACE] login: An error occurred attempting to fetch a message of the day for Terraform Cloud: %s", err)
|
||||
}
|
||||
|
||||
// Help implements cli.Command.
|
||||
|
|
|
@ -56,16 +56,6 @@ func TestLogin(t *testing.T) {
|
|||
svcs := disco.NewWithCredentialsSource(creds)
|
||||
svcs.SetUserAgent(httpclient.TerraformUserAgent(version.String()))
|
||||
|
||||
svcs.ForceHostServices(svchost.Hostname("app.terraform.io"), map[string]interface{}{
|
||||
"login.v1": map[string]interface{}{
|
||||
// On app.terraform.io we use password-based authorization.
|
||||
// That's the only hostname that it's permitted for, so we can't
|
||||
// use a fake hostname here.
|
||||
"client": "terraformcli",
|
||||
"token": s.URL + "/token",
|
||||
"grant_types": []interface{}{"password"},
|
||||
},
|
||||
})
|
||||
svcs.ForceHostServices(svchost.Hostname("example.com"), map[string]interface{}{
|
||||
"login.v1": map[string]interface{}{
|
||||
// For this fake hostname we'll use a conventional OAuth flow,
|
||||
|
@ -86,9 +76,17 @@ func TestLogin(t *testing.T) {
|
|||
"scopes": []interface{}{"app1.full_access", "app2.read_only"},
|
||||
},
|
||||
})
|
||||
svcs.ForceHostServices(svchost.Hostname("app.terraform.io"), map[string]interface{}{
|
||||
// This represents Terraform Cloud, which does not yet support the
|
||||
// login API, but does support its own bespoke tokens API.
|
||||
"tfe.v2": ts.URL + "/api/v2",
|
||||
"tfe.v2.1": ts.URL + "/api/v2",
|
||||
"tfe.v2.2": ts.URL + "/api/v2",
|
||||
"motd.v1": ts.URL + "/api/terraform/motd",
|
||||
})
|
||||
svcs.ForceHostServices(svchost.Hostname("tfe.acme.com"), map[string]interface{}{
|
||||
// This represents a Terraform Enterprise instance which does not
|
||||
// yet support the login API, but does support the TFE tokens API.
|
||||
// yet support the login API, but does support its own bespoke tokens API.
|
||||
"tfe.v2": ts.URL + "/api/v2",
|
||||
"tfe.v2.1": ts.URL + "/api/v2",
|
||||
"tfe.v2.2": ts.URL + "/api/v2",
|
||||
|
@ -109,13 +107,14 @@ func TestLogin(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
t.Run("defaulting to app.terraform.io with password flow", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
|
||||
t.Run("app.terraform.io (no login support)", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
|
||||
// Enter "yes" at the consent prompt, then paste a token with some
|
||||
// accidental whitespace.
|
||||
defer testInputMap(t, map[string]string{
|
||||
"approve": "yes",
|
||||
"username": "foo",
|
||||
"password": "bar",
|
||||
"approve": "yes",
|
||||
"token": " good-token ",
|
||||
})()
|
||||
status := c.Run(nil)
|
||||
status := c.Run([]string{"app.terraform.io"})
|
||||
if status != 0 {
|
||||
t.Fatalf("unexpected error code %d\nstderr:\n%s", status, ui.ErrorWriter.String())
|
||||
}
|
||||
|
@ -128,6 +127,9 @@ func TestLogin(t *testing.T) {
|
|||
if got, want := creds.Token(), "good-token"; got != want {
|
||||
t.Errorf("wrong token %q; want %q", got, want)
|
||||
}
|
||||
if got, want := ui.OutputWriter.String(), "Welcome to Terraform Cloud!"; !strings.Contains(got, want) {
|
||||
t.Errorf("expected output to contain %q, but was:\n%s", want, got)
|
||||
}
|
||||
}))
|
||||
|
||||
t.Run("example.com with authorization code flow", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
|
||||
|
@ -148,6 +150,10 @@ func TestLogin(t *testing.T) {
|
|||
if got, want := creds.Token(), "good-token"; got != want {
|
||||
t.Errorf("wrong token %q; want %q", got, want)
|
||||
}
|
||||
|
||||
if got, want := ui.OutputWriter.String(), "Terraform has obtained and saved an API token."; !strings.Contains(got, want) {
|
||||
t.Errorf("expected output to contain %q, but was:\n%s", want, got)
|
||||
}
|
||||
}))
|
||||
|
||||
t.Run("example.com results in no scopes", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
|
||||
|
@ -179,6 +185,10 @@ func TestLogin(t *testing.T) {
|
|||
if got, want := creds.Token(), "good-token"; got != want {
|
||||
t.Errorf("wrong token %q; want %q", got, want)
|
||||
}
|
||||
|
||||
if got, want := ui.OutputWriter.String(), "Terraform has obtained and saved an API token."; !strings.Contains(got, want) {
|
||||
t.Errorf("expected output to contain %q, but was:\n%s", want, got)
|
||||
}
|
||||
}))
|
||||
|
||||
t.Run("with-scopes.example.com results in expected scopes", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
|
||||
|
@ -216,6 +226,10 @@ func TestLogin(t *testing.T) {
|
|||
if got, want := creds.Token(), "good-token"; got != want {
|
||||
t.Errorf("wrong token %q; want %q", got, want)
|
||||
}
|
||||
|
||||
if got, want := ui.OutputWriter.String(), "Logged in to Terraform Enterprise"; !strings.Contains(got, want) {
|
||||
t.Errorf("expected output to contain %q, but was:\n%s", want, got)
|
||||
}
|
||||
}))
|
||||
|
||||
t.Run("TFE host without login support, incorrectly pasted token", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
|
||||
|
|
|
@ -250,8 +250,14 @@ func (m *Meta) StateOutPath() string {
|
|||
|
||||
// Colorize returns the colorization structure for a command.
|
||||
func (m *Meta) Colorize() *colorstring.Colorize {
|
||||
colors := make(map[string]string)
|
||||
for k, v := range colorstring.DefaultColors {
|
||||
colors[k] = v
|
||||
}
|
||||
colors["purple"] = "38;5;57"
|
||||
|
||||
return &colorstring.Colorize{
|
||||
Colors: colorstring.DefaultColors,
|
||||
Colors: colors,
|
||||
Disable: !m.color,
|
||||
Reset: true,
|
||||
}
|
||||
|
|
|
@ -21,6 +21,11 @@ func (c *PlanCommand) Run(rawArgs []string) int {
|
|||
common, rawArgs := arguments.ParseView(rawArgs)
|
||||
c.View.Configure(common)
|
||||
|
||||
// Propagate -no-color for the remote backend's legacy use of Ui. This
|
||||
// should be removed when the remote backend is migrated to views.
|
||||
c.Meta.color = !common.NoColor
|
||||
c.Meta.Color = c.Meta.color
|
||||
|
||||
// Parse and validate flags
|
||||
args, diags := arguments.ParsePlan(rawArgs)
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
const (
|
||||
goodToken = "good-token"
|
||||
accountDetails = `{"data":{"id":"user-abc123","type":"users","attributes":{"username":"testuser","email":"testuser@example.com"}}}`
|
||||
MOTD = `{"msg":"Welcome to Terraform Cloud!"}`
|
||||
)
|
||||
|
||||
// Handler is an implementation of net/http.Handler that provides a stub
|
||||
|
@ -29,6 +30,8 @@ func (h handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
|||
h.servePing(resp, req)
|
||||
case "/api/v2/account/details":
|
||||
h.serveAccountDetails(resp, req)
|
||||
case "/api/terraform/motd":
|
||||
h.serveMOTD(resp, req)
|
||||
default:
|
||||
fmt.Printf("404 when fetching %s\n", req.URL.String())
|
||||
http.Error(resp, `{"errors":[{"status":"404","title":"not found"}]}`, http.StatusNotFound)
|
||||
|
@ -49,6 +52,11 @@ func (h handler) serveAccountDetails(resp http.ResponseWriter, req *http.Request
|
|||
resp.Write([]byte(accountDetails))
|
||||
}
|
||||
|
||||
func (h handler) serveMOTD(resp http.ResponseWriter, req *http.Request) {
|
||||
resp.WriteHeader(http.StatusOK)
|
||||
resp.Write([]byte(MOTD))
|
||||
}
|
||||
|
||||
func init() {
|
||||
Handler = handler{}
|
||||
}
|
||||
|
|
|
@ -317,10 +317,20 @@ func compactValueStr(val cty.Value) string {
|
|||
// helpful but concise messages in diagnostics. It is not comprehensive
|
||||
// nor intended to be used for other purposes.
|
||||
|
||||
if val.ContainsMarked() {
|
||||
if val.IsMarked() {
|
||||
// We check this in here just to make sure, but note that the caller
|
||||
// of compactValueStr ought to have already checked this and skipped
|
||||
// calling into compactValueStr anyway, so this shouldn't actually
|
||||
// be reachable.
|
||||
return "(sensitive value)"
|
||||
}
|
||||
|
||||
// WARNING: We've only checked that the value isn't sensitive _shallowly_
|
||||
// here, and so we must never show any element values from complex types
|
||||
// in here. However, it's fine to show map keys and attribute names because
|
||||
// those are never sensitive in isolation: the entire value would be
|
||||
// sensitive in that case.
|
||||
|
||||
ty := val.Type()
|
||||
switch {
|
||||
case val.IsNull():
|
||||
|
|
|
@ -360,6 +360,62 @@ func TestNewDiagnostic(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
"error with source code subject and expression referring to a collection containing a sensitive value": {
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Wrong noises",
|
||||
Detail: "Biological sounds are not allowed",
|
||||
Subject: &hcl.Range{
|
||||
Filename: "test.tf",
|
||||
Start: hcl.Pos{Line: 2, Column: 9, Byte: 42},
|
||||
End: hcl.Pos{Line: 2, Column: 26, Byte: 59},
|
||||
},
|
||||
Expression: hcltest.MockExprTraversal(hcl.Traversal{
|
||||
hcl.TraverseRoot{Name: "var"},
|
||||
hcl.TraverseAttr{Name: "boop"},
|
||||
}),
|
||||
EvalContext: &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"var": cty.ObjectVal(map[string]cty.Value{
|
||||
"boop": cty.MapVal(map[string]cty.Value{
|
||||
"hello!": cty.StringVal("bleurgh").Mark("sensitive"),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
&Diagnostic{
|
||||
Severity: "error",
|
||||
Summary: "Wrong noises",
|
||||
Detail: "Biological sounds are not allowed",
|
||||
Range: &DiagnosticRange{
|
||||
Filename: "test.tf",
|
||||
Start: Pos{
|
||||
Line: 2,
|
||||
Column: 9,
|
||||
Byte: 42,
|
||||
},
|
||||
End: Pos{
|
||||
Line: 2,
|
||||
Column: 26,
|
||||
Byte: 59,
|
||||
},
|
||||
},
|
||||
Snippet: &DiagnosticSnippet{
|
||||
Context: strPtr(`resource "test_resource" "test"`),
|
||||
Code: (` foo = var.boop["hello!"]`),
|
||||
StartLine: (2),
|
||||
HighlightStartOffset: (8),
|
||||
HighlightEndOffset: (25),
|
||||
Values: []DiagnosticExpressionValue{
|
||||
{
|
||||
Traversal: `var.boop`,
|
||||
Statement: `is map of string with 1 element`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"error with source code subject and unknown string expression": {
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"severity": "error",
|
||||
"summary": "Wrong noises",
|
||||
"detail": "Biological sounds are not allowed",
|
||||
"range": {
|
||||
"filename": "test.tf",
|
||||
"start": {
|
||||
"line": 2,
|
||||
"column": 9,
|
||||
"byte": 42
|
||||
},
|
||||
"end": {
|
||||
"line": 2,
|
||||
"column": 26,
|
||||
"byte": 59
|
||||
}
|
||||
},
|
||||
"snippet": {
|
||||
"context": "resource \"test_resource\" \"test\"",
|
||||
"code": " foo = var.boop[\"hello!\"]",
|
||||
"start_line": 2,
|
||||
"highlight_start_offset": 8,
|
||||
"highlight_end_offset": 25,
|
||||
"values": [
|
||||
{
|
||||
"traversal": "var.boop",
|
||||
"statement": "is map of string with 1 element"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -329,6 +329,53 @@ func (c *Config) addProviderRequirements(reqs getproviders.Requirements, recurse
|
|||
return diags
|
||||
}
|
||||
|
||||
// resolveProviderTypes walks through the providers in the module and ensures
|
||||
// the true types are assigned based on the provider requirements for the
|
||||
// module.
|
||||
func (c *Config) resolveProviderTypes() {
|
||||
for _, child := range c.Children {
|
||||
child.resolveProviderTypes()
|
||||
}
|
||||
|
||||
// collect the required_providers, and then add any missing default providers
|
||||
providers := map[string]addrs.Provider{}
|
||||
for name, p := range c.Module.ProviderRequirements.RequiredProviders {
|
||||
providers[name] = p.Type
|
||||
}
|
||||
|
||||
// ensure all provider configs know their correct type
|
||||
for _, p := range c.Module.ProviderConfigs {
|
||||
addr, required := providers[p.Name]
|
||||
if required {
|
||||
p.providerType = addr
|
||||
} else {
|
||||
addr := addrs.NewDefaultProvider(p.Name)
|
||||
p.providerType = addr
|
||||
providers[p.Name] = addr
|
||||
}
|
||||
}
|
||||
|
||||
// connect module call providers to the correct type
|
||||
for _, mod := range c.Module.ModuleCalls {
|
||||
for _, p := range mod.Providers {
|
||||
if addr, known := providers[p.InParent.Name]; known {
|
||||
p.InParent.providerType = addr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fill in parent module calls too
|
||||
if c.Parent != nil {
|
||||
for _, mod := range c.Parent.Module.ModuleCalls {
|
||||
for _, p := range mod.Providers {
|
||||
if addr, known := providers[p.InChild.Name]; known {
|
||||
p.InChild.providerType = addr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ProviderTypes returns the FQNs of each distinct provider type referenced
|
||||
// in the receiving configuration.
|
||||
//
|
||||
|
|
|
@ -23,6 +23,10 @@ func BuildConfig(root *Module, walker ModuleWalker) (*Config, hcl.Diagnostics) {
|
|||
cfg.Root = cfg // Root module is self-referential.
|
||||
cfg.Children, diags = buildChildModules(cfg, walker)
|
||||
|
||||
// Now that the config is built, we can connect the provider names to all
|
||||
// the known types for validation.
|
||||
cfg.resolveProviderTypes()
|
||||
|
||||
diags = append(diags, validateProviderConfigs(nil, cfg, false)...)
|
||||
|
||||
return cfg, diags
|
||||
|
|
|
@ -101,15 +101,6 @@ func (b *Block) DecoderSpec() hcldec.Spec {
|
|||
|
||||
childSpec := blockS.Block.DecoderSpec()
|
||||
|
||||
// We can only validate 0 or 1 for MinItems, because a dynamic block
|
||||
// may satisfy any number of min items while only having a single
|
||||
// block in the config. We cannot validate MaxItems because a
|
||||
// configuration may have any number of dynamic blocks.
|
||||
minItems := 0
|
||||
if blockS.MinItems > 1 {
|
||||
minItems = 1
|
||||
}
|
||||
|
||||
switch blockS.Nesting {
|
||||
case NestingSingle, NestingGroup:
|
||||
ret[name] = &hcldec.BlockSpec{
|
||||
|
@ -134,13 +125,15 @@ func (b *Block) DecoderSpec() hcldec.Spec {
|
|||
ret[name] = &hcldec.BlockTupleSpec{
|
||||
TypeName: name,
|
||||
Nested: childSpec,
|
||||
MinItems: minItems,
|
||||
MinItems: blockS.MinItems,
|
||||
MaxItems: blockS.MaxItems,
|
||||
}
|
||||
} else {
|
||||
ret[name] = &hcldec.BlockListSpec{
|
||||
TypeName: name,
|
||||
Nested: childSpec,
|
||||
MinItems: minItems,
|
||||
MinItems: blockS.MinItems,
|
||||
MaxItems: blockS.MaxItems,
|
||||
}
|
||||
}
|
||||
case NestingSet:
|
||||
|
@ -154,7 +147,8 @@ func (b *Block) DecoderSpec() hcldec.Spec {
|
|||
ret[name] = &hcldec.BlockSetSpec{
|
||||
TypeName: name,
|
||||
Nested: childSpec,
|
||||
MinItems: minItems,
|
||||
MinItems: blockS.MinItems,
|
||||
MaxItems: blockS.MaxItems,
|
||||
}
|
||||
case NestingMap:
|
||||
// We prefer to use a list where possible, since it makes our
|
||||
|
|
|
@ -344,15 +344,12 @@ func TestBlockDecoderSpec(t *testing.T) {
|
|||
},
|
||||
&hcl.Block{
|
||||
Type: "foo",
|
||||
Body: hcl.EmptyBody(),
|
||||
Body: unknownBody{hcl.EmptyBody()},
|
||||
},
|
||||
},
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.ListVal([]cty.Value{
|
||||
cty.EmptyObjectVal,
|
||||
cty.EmptyObjectVal,
|
||||
}),
|
||||
"foo": cty.UnknownVal(cty.List(cty.EmptyObject)),
|
||||
}),
|
||||
0, // max items cannot be validated during decode
|
||||
},
|
||||
|
@ -372,14 +369,12 @@ func TestBlockDecoderSpec(t *testing.T) {
|
|||
Blocks: hcl.Blocks{
|
||||
&hcl.Block{
|
||||
Type: "foo",
|
||||
Body: hcl.EmptyBody(),
|
||||
Body: unknownBody{hcl.EmptyBody()},
|
||||
},
|
||||
},
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.ListVal([]cty.Value{
|
||||
cty.EmptyObjectVal,
|
||||
}),
|
||||
"foo": cty.UnknownVal(cty.List(cty.EmptyObject)),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
|
@ -401,6 +396,7 @@ func TestBlockDecoderSpec(t *testing.T) {
|
|||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
spec := test.Schema.DecoderSpec()
|
||||
|
||||
got, diags := hcldec.Decode(test.TestBody, spec, nil)
|
||||
if len(diags) != test.DiagCount {
|
||||
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
|
||||
|
@ -427,6 +423,16 @@ func TestBlockDecoderSpec(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// this satisfies hcldec.UnknownBody to simulate a dynamic block with an
|
||||
// unknown number of values.
|
||||
type unknownBody struct {
|
||||
hcl.Body
|
||||
}
|
||||
|
||||
func (b unknownBody) Unknown() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func TestAttributeDecoderSpec(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
Schema *Attribute
|
||||
|
|
|
@ -25,6 +25,13 @@ type Provider struct {
|
|||
Config hcl.Body
|
||||
|
||||
DeclRange hcl.Range
|
||||
|
||||
// TODO: this may not be set in some cases, so it is not yet suitable for
|
||||
// use outside of this package. We currently only use it for internal
|
||||
// validation, but once we verify that this can be set in all cases, we can
|
||||
// export this so providers don't need to be re-resolved.
|
||||
// This same field is also added to the ProviderConfigRef struct.
|
||||
providerType addrs.Provider
|
||||
}
|
||||
|
||||
func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) {
|
||||
|
|
|
@ -136,9 +136,18 @@ func validateProviderConfigs(call *ModuleCall, cfg *Config, noProviderConfig boo
|
|||
|
||||
// You cannot pass in a provider that cannot be used
|
||||
for name, passed := range passedIn {
|
||||
childTy := passed.InChild.providerType
|
||||
// get a default type if there was none set
|
||||
if childTy.IsZero() {
|
||||
// This means the child module is only using an inferred
|
||||
// provider type. We allow this but will generate a warning to
|
||||
// declare provider_requirements below.
|
||||
childTy = addrs.NewDefaultProvider(passed.InChild.Name)
|
||||
}
|
||||
|
||||
providerAddr := addrs.AbsProviderConfig{
|
||||
Module: cfg.Path,
|
||||
Provider: addrs.NewDefaultProvider(passed.InChild.Name),
|
||||
Provider: childTy,
|
||||
Alias: passed.InChild.Alias,
|
||||
}
|
||||
|
||||
|
@ -172,9 +181,12 @@ func validateProviderConfigs(call *ModuleCall, cfg *Config, noProviderConfig boo
|
|||
}
|
||||
|
||||
// The provider being passed in must also be of the correct type.
|
||||
// While we would like to ensure required_providers exists here,
|
||||
// implied default configuration is still allowed.
|
||||
pTy := addrs.NewDefaultProvider(passed.InParent.Name)
|
||||
pTy := passed.InParent.providerType
|
||||
if pTy.IsZero() {
|
||||
// While we would like to ensure required_providers exists here,
|
||||
// implied default configuration is still allowed.
|
||||
pTy = addrs.NewDefaultProvider(passed.InParent.Name)
|
||||
}
|
||||
|
||||
// use the full address for a nice diagnostic output
|
||||
parentAddr := addrs.AbsProviderConfig{
|
||||
|
|
|
@ -374,6 +374,13 @@ type ProviderConfigRef struct {
|
|||
NameRange hcl.Range
|
||||
Alias string
|
||||
AliasRange *hcl.Range // nil if alias not set
|
||||
|
||||
// TODO: this may not be set in some cases, so it is not yet suitable for
|
||||
// use outside of this package. We currently only use it for internal
|
||||
// validation, but once we verify that this can be set in all cases, we can
|
||||
// export this so providers don't need to be re-resolved.
|
||||
// This same field is also added to the Provider struct.
|
||||
providerType addrs.Provider
|
||||
}
|
||||
|
||||
func decodeProviderConfigRef(expr hcl.Expression, argName string) (*ProviderConfigRef, hcl.Diagnostics) {
|
||||
|
|
|
@ -3,11 +3,13 @@ terraform {
|
|||
bar-test = {
|
||||
source = "bar/test"
|
||||
}
|
||||
foo-test = {
|
||||
source = "foo/test"
|
||||
configuration_aliases = [foo-test.other]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "bar-test" {}
|
||||
|
||||
resource "test_instance" "explicit" {
|
||||
// explicitly setting provider bar-test
|
||||
provider = bar-test
|
||||
|
@ -17,3 +19,7 @@ resource "test_instance" "implicit" {
|
|||
// since the provider type name "test" does not match an entry in
|
||||
// required_providers, the default provider "test" should be used
|
||||
}
|
||||
|
||||
resource "test_instance" "other" {
|
||||
provider = foo-test.other
|
||||
}
|
||||
|
|
|
@ -10,6 +10,9 @@ provider "foo-test" {}
|
|||
|
||||
module "child" {
|
||||
source = "./child"
|
||||
providers = {
|
||||
foo-test.other = foo-test
|
||||
}
|
||||
}
|
||||
|
||||
resource "test_instance" "explicit" {
|
||||
|
|
12
dag/set.go
12
dag/set.go
|
@ -56,13 +56,13 @@ func (s Set) Intersection(other Set) Set {
|
|||
// Difference returns a set with the elements that s has but
|
||||
// other doesn't.
|
||||
func (s Set) Difference(other Set) Set {
|
||||
if other == nil || other.Len() == 0 {
|
||||
return s.Copy()
|
||||
}
|
||||
|
||||
result := make(Set)
|
||||
for k, v := range s {
|
||||
var ok bool
|
||||
if other != nil {
|
||||
_, ok = other[k]
|
||||
}
|
||||
if !ok {
|
||||
if _, ok := other[k]; !ok {
|
||||
result.Add(v)
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ func (s Set) List() []interface{} {
|
|||
|
||||
// Copy returns a shallow copy of the set.
|
||||
func (s Set) Copy() Set {
|
||||
c := make(Set)
|
||||
c := make(Set, len(s))
|
||||
for k, v := range s {
|
||||
c[k] = v
|
||||
}
|
||||
|
|
|
@ -31,6 +31,12 @@ func TestSetDifference(t *testing.T) {
|
|||
[]interface{}{3, 2, 1, 4},
|
||||
[]interface{}{},
|
||||
},
|
||||
{
|
||||
"B is nil",
|
||||
[]interface{}{1, 2, 3},
|
||||
nil,
|
||||
[]interface{}{1, 2, 3},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
|
@ -44,6 +50,9 @@ func TestSetDifference(t *testing.T) {
|
|||
for _, v := range tc.B {
|
||||
two.Add(v)
|
||||
}
|
||||
if tc.B == nil {
|
||||
two = nil
|
||||
}
|
||||
for _, v := range tc.Expected {
|
||||
expected.Add(v)
|
||||
}
|
||||
|
|
6
go.mod
6
go.mod
|
@ -57,7 +57,7 @@ require (
|
|||
github.com/hashicorp/go-immutable-radix v0.0.0-20180129170900-7f3cd4390caa // indirect
|
||||
github.com/hashicorp/go-msgpack v0.5.4 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/go-plugin v1.4.0
|
||||
github.com/hashicorp/go-plugin v1.4.1
|
||||
github.com/hashicorp/go-retryablehttp v0.5.2
|
||||
github.com/hashicorp/go-rootcerts v1.0.0 // indirect
|
||||
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 // indirect
|
||||
|
@ -65,7 +65,7 @@ require (
|
|||
github.com/hashicorp/go-uuid v1.0.1
|
||||
github.com/hashicorp/go-version v1.2.1
|
||||
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f
|
||||
github.com/hashicorp/hcl/v2 v2.9.1
|
||||
github.com/hashicorp/hcl/v2 v2.10.0
|
||||
github.com/hashicorp/memberlist v0.1.0 // indirect
|
||||
github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb // indirect
|
||||
github.com/hashicorp/terraform-config-inspect v0.0.0-20210209133302-4fd17a0faac2
|
||||
|
@ -114,7 +114,7 @@ require (
|
|||
github.com/xanzy/ssh-agent v0.2.1
|
||||
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect
|
||||
github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557
|
||||
github.com/zclconf/go-cty v1.8.1
|
||||
github.com/zclconf/go-cty v1.8.2
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b
|
||||
github.com/zclconf/go-cty-yaml v1.0.2
|
||||
go.uber.org/atomic v1.3.2 // indirect
|
||||
|
|
14
go.sum
14
go.sum
|
@ -338,8 +338,6 @@ github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuD
|
|||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-getter v1.5.1 h1:lM9sM02nvEApQGFgkXxWbhfqtyN+AyhQmi+MaMdBDOI=
|
||||
github.com/hashicorp/go-getter v1.5.1/go.mod h1:a7z7NPPfNQpJWcn4rSWFtdrSldqLdLPEF3d8nFMsSLM=
|
||||
github.com/hashicorp/go-getter v1.5.2 h1:XDo8LiAcDisiqZdv0TKgz+HtX3WN7zA2JD1R1tjsabE=
|
||||
github.com/hashicorp/go-getter v1.5.2/go.mod h1:orNH3BTYLu/fIxGIdLjLoAJHWMDQ/UKQr5O4m3iBuoo=
|
||||
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
|
@ -352,8 +350,8 @@ github.com/hashicorp/go-msgpack v0.5.4/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP
|
|||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-plugin v1.4.0 h1:b0O7rs5uiJ99Iu9HugEzsM67afboErkHUWddUSpUO3A=
|
||||
github.com/hashicorp/go-plugin v1.4.0/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
|
||||
github.com/hashicorp/go-plugin v1.4.1 h1:6UltRQlLN9iZO513VveELp5xyaFxVD2+1OVylE+2E+w=
|
||||
github.com/hashicorp/go-plugin v1.4.1/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.2 h1:AoISa4P4IsW0/m4T6St8Yw38gTl5GtBAgfkhYh1xAz4=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.2/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI=
|
||||
|
@ -380,8 +378,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
|||
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+DbLISwf2B8WXEolNRA8BGCwI9jws=
|
||||
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||
github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90=
|
||||
github.com/hashicorp/hcl/v2 v2.9.1 h1:eOy4gREY0/ZQHNItlfuEZqtcQbXIxzojlP301hDpnac=
|
||||
github.com/hashicorp/hcl/v2 v2.9.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg=
|
||||
github.com/hashicorp/hcl/v2 v2.10.0 h1:1S1UnuhDGlv3gRFV4+0EdwB+znNP5HmcGbIqwnSCByg=
|
||||
github.com/hashicorp/hcl/v2 v2.10.0/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg=
|
||||
github.com/hashicorp/memberlist v0.1.0 h1:qSsCiC0WYD39lbSitKNt40e30uorm2Ss/d4JGU1hzH8=
|
||||
github.com/hashicorp/memberlist v0.1.0/go.mod h1:ncdBp14cuox2iFOq3kDiquKU6fqsTBc3W6JvZwjxxsE=
|
||||
github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb h1:ZbgmOQt8DOg796figP87/EFCVx2v2h9yRvwHF/zceX4=
|
||||
|
@ -618,8 +616,8 @@ github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLE
|
|||
github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
|
||||
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
|
||||
github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
|
||||
github.com/zclconf/go-cty v1.8.1 h1:SI0LqNeNxAgv2WWqWJMlG2/Ad/6aYJ7IVYYMigmfkuI=
|
||||
github.com/zclconf/go-cty v1.8.1/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
|
||||
github.com/zclconf/go-cty v1.8.2 h1:u+xZfBKgpycDnTNjPhGiTEYZS5qS/Sb5MqSfm7vzcjg=
|
||||
github.com/zclconf/go-cty v1.8.2/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
|
||||
github.com/zclconf/go-cty-yaml v1.0.2 h1:dNyg4QLTrv2IfJpm7Wtxi55ed5gLGOlPrZ6kMd51hY0=
|
||||
|
|
|
@ -325,18 +325,6 @@ func TestSignatureAuthentication_success(t *testing.T) {
|
|||
keys []SigningKey
|
||||
result PackageAuthenticationResult
|
||||
}{
|
||||
"official provider": {
|
||||
testHashicorpSignatureGoodBase64,
|
||||
[]SigningKey{
|
||||
{
|
||||
ASCIIArmor: HashicorpPublicKey,
|
||||
},
|
||||
},
|
||||
PackageAuthenticationResult{
|
||||
result: officialProvider,
|
||||
KeyID: testHashiCorpPublicKeyID,
|
||||
},
|
||||
},
|
||||
"partner provider": {
|
||||
testAuthorSignatureGoodBase64,
|
||||
[]SigningKey{
|
||||
|
@ -402,6 +390,49 @@ func TestSignatureAuthentication_success(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNewSignatureAuthentication_success(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
signature string
|
||||
keys []SigningKey
|
||||
result PackageAuthenticationResult
|
||||
}{
|
||||
"official provider": {
|
||||
testHashicorpSignatureGoodBase64,
|
||||
[]SigningKey{
|
||||
{
|
||||
ASCIIArmor: HashicorpPublicKey,
|
||||
},
|
||||
},
|
||||
PackageAuthenticationResult{
|
||||
result: officialProvider,
|
||||
KeyID: testHashiCorpPublicKeyID,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Location is unused
|
||||
location := PackageLocalArchive("testdata/my-package.zip")
|
||||
|
||||
signature, err := base64.StdEncoding.DecodeString(test.signature)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
auth := NewSignatureAuthentication([]byte(testProviderShaSums), signature, test.keys)
|
||||
result, err := auth.AuthenticatePackage(location)
|
||||
|
||||
if result == nil || *result != test.result {
|
||||
t.Errorf("wrong result: got %#v, want %#v", result, test.result)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("wrong err: got %s, want nil", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Signature authentication can fail for many reasons, most of which are due
|
||||
// to OpenPGP failures from malformed keys or signatures.
|
||||
func TestSignatureAuthentication_failure(t *testing.T) {
|
||||
|
@ -621,18 +652,35 @@ const testSignatureBadBase64 = `iQEzBAABCAAdFiEEW/7sQxfnRgCGIZcGN6arO88s` +
|
|||
`n1ayZdaCIw/r4w==`
|
||||
|
||||
// testHashiCorpPublicKeyID is the Key ID of the HashiCorpPublicKey.
|
||||
const testHashiCorpPublicKeyID = `51852D87348FFC4C`
|
||||
const testHashiCorpPublicKeyID = `34365D9472D7468F`
|
||||
|
||||
// testHashicorpSignatureGoodBase64 is a signature of testShaSums signed with
|
||||
const testProviderShaSums = `fea4227271ebf7d9e2b61b89ce2328c7262acd9fd190e1fd6d15a591abfa848e terraform-provider-null_3.1.0_darwin_amd64.zip
|
||||
9ebf4d9704faba06b3ec7242c773c0fbfe12d62db7d00356d4f55385fc69bfb2 terraform-provider-null_3.1.0_darwin_arm64.zip
|
||||
a6576c81adc70326e4e1c999c04ad9ca37113a6e925aefab4765e5a5198efa7e terraform-provider-null_3.1.0_freebsd_386.zip
|
||||
5f9200bf708913621d0f6514179d89700e9aa3097c77dac730e8ba6e5901d521 terraform-provider-null_3.1.0_freebsd_amd64.zip
|
||||
fc39cc1fe71234a0b0369d5c5c7f876c71b956d23d7d6f518289737a001ba69b terraform-provider-null_3.1.0_freebsd_arm.zip
|
||||
c797744d08a5307d50210e0454f91ca4d1c7621c68740441cf4579390452321d terraform-provider-null_3.1.0_linux_386.zip
|
||||
53e30545ff8926a8e30ad30648991ca8b93b6fa496272cd23b26763c8ee84515 terraform-provider-null_3.1.0_linux_amd64.zip
|
||||
cecb6a304046df34c11229f20a80b24b1603960b794d68361a67c5efe58e62b8 terraform-provider-null_3.1.0_linux_arm64.zip
|
||||
e1371aa1e502000d9974cfaff5be4cfa02f47b17400005a16f14d2ef30dc2a70 terraform-provider-null_3.1.0_linux_arm.zip
|
||||
a8a42d13346347aff6c63a37cda9b2c6aa5cc384a55b2fe6d6adfa390e609c53 terraform-provider-null_3.1.0_windows_386.zip
|
||||
02a1675fd8de126a00460942aaae242e65ca3380b5bb192e8773ef3da9073fd2 terraform-provider-null_3.1.0_windows_amd64.zip
|
||||
`
|
||||
|
||||
// testHashicorpSignatureGoodBase64 is a signature of testProviderShaSums signed with
|
||||
// HashicorpPublicKey, which represents the SHA256SUMS.sig file downloaded for
|
||||
// an official release.
|
||||
const testHashicorpSignatureGoodBase64 = `iQFLBAABCAA1FiEEkabn+F0FxlYwvvGJUYUth` +
|
||||
`zSP/EwFAl5w784XHHNlY3VyaXR5QGhhc2hpY29ycC5jb20ACgkQUYUthzSP/EyB8QgAv9ijp` +
|
||||
`kTcoFwDAs+1iEUrcW18h/2cU+bvFtdqNDiffzk7+YJ9ioxeWisPta/Z6hEyhdss2+5L1MNbo` +
|
||||
`oUBLABI+Aebfxa/uYFT2kX6r/eySmlY9kqNVpjXdemOQutS4NNZxdJL7CEbh2qIKCVuyo0ul` +
|
||||
`YrTdDH35vwVyLXImWiZLnrXcT/fXLpQGx/N8PDy6WmCeju5Y5RD7TuntB71eCaCZi7wFe1tR` +
|
||||
`qSoe9tD9A7ONB0rGuCY7BxqUj0S81hhz960YbNR9Q81WoNvF7b5SmcLJ1qJx1yvBLyqya6Su` +
|
||||
`DKjU/YYCh7bwHIYzpk1/nK/7SaTHpisekqojVsfDth4TA+jGA==`
|
||||
const testHashicorpSignatureGoodBase64 = `wsFcBAABCAAQBQJgga+GCRCwtEEJdoW2dgAA` +
|
||||
`o0YQAAW911BGDr2WHLo5NwcZenwHyxL5DX9g+4BknKbc/WxRC1hD8Afi3eygZk1yR6eT4Gp2H` +
|
||||
`yNOwCjGL1PTONBumMfj9udIeuX8onrJMMvjFHh+bORGxBi4FKr4V3b2ZV1IYOjWMEyyTGRDvw` +
|
||||
`SCdxBkp3apH3s2xZLmRoAj84JZ4KaxGF7hlT0j4IkNyQKd2T5cCByN9DV80+x+HtzaOieFwJL` +
|
||||
`97iyGj6aznXfKfslK6S4oIrVTwyLTrQbxSxA0LsdUjRPHnJamL3sFOG77qUEUoXG3r61yi5vW` +
|
||||
`V4P5gCH/+C+VkfGHqaB1s0jHYLxoTEXtwthe66MydDBPe2Hd0J12u9ppOIeK3leeb4uiixWIi` +
|
||||
`rNdpWyjr/LU1KKWPxsDqMGYJ9TexyWkXjEpYmIEiY1Rxar8jrLh+FqVAhxRJajjgSRu5pZj50` +
|
||||
`CNeKmmbyolLhPCmICjYYU/xKPGXSyDFqonVVyMWCSpO+8F38OmwDQHIk5AWyc8hPOAZ+g5N95` +
|
||||
`cfUAzEqlvmNvVHQIU40Y6/Ip2HZzzFCLKQkMP1aDakYHq5w4ZO/ucjhKuoh1HDQMuMnZSu4eo` +
|
||||
`2nMTBzYZnUxwtROrJZF1t103avbmP2QE/GaPvLIQn7o5WMV3ZcPCJ+szzzby7H2e33WIynrY/` +
|
||||
`95ensBxh7mGFbcQ1C59b5o7viwIaaY2`
|
||||
|
||||
// entityString function is used for logging the signing key.
|
||||
func TestEntityString(t *testing.T) {
|
||||
|
@ -654,7 +702,7 @@ func TestEntityString(t *testing.T) {
|
|||
{
|
||||
"HashicorpPublicKey",
|
||||
testReadArmoredEntity(t, HashicorpPublicKey),
|
||||
"51852D87348FFC4C HashiCorp Security <security@hashicorp.com>",
|
||||
"34365D9472D7468F HashiCorp Security (hashicorp.com/security) <security@hashicorp.com>",
|
||||
},
|
||||
{
|
||||
"HashicorpPartnersKey",
|
||||
|
|
|
@ -3,34 +3,126 @@ package getproviders
|
|||
// HashicorpPublicKey is the HashiCorp public key, also available at
|
||||
// https://www.hashicorp.com/security
|
||||
const HashicorpPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: GnuPG v1
|
||||
|
||||
mQENBFMORM0BCADBRyKO1MhCirazOSVwcfTr1xUxjPvfxD3hjUwHtjsOy/bT6p9f
|
||||
W2mRPfwnq2JB5As+paL3UGDsSRDnK9KAxQb0NNF4+eVhr/EJ18s3wwXXDMjpIifq
|
||||
fIm2WyH3G+aRLTLPIpscUNKDyxFOUbsmgXAmJ46Re1fn8uKxKRHbfa39aeuEYWFA
|
||||
3drdL1WoUngvED7f+RnKBK2G6ZEpO+LDovQk19xGjiMTtPJrjMjZJ3QXqPvx5wca
|
||||
KSZLr4lMTuoTI/ZXyZy5bD4tShiZz6KcyX27cD70q2iRcEZ0poLKHyEIDAi3TM5k
|
||||
SwbbWBFd5RNPOR0qzrb/0p9ksKK48IIfH2FvABEBAAG0K0hhc2hpQ29ycCBTZWN1
|
||||
cml0eSA8c2VjdXJpdHlAaGFzaGljb3JwLmNvbT6JATgEEwECACIFAlMORM0CGwMG
|
||||
CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEFGFLYc0j/xMyWIIAIPhcVqiQ59n
|
||||
Jc07gjUX0SWBJAxEG1lKxfzS4Xp+57h2xxTpdotGQ1fZwsihaIqow337YHQI3q0i
|
||||
SqV534Ms+j/tU7X8sq11xFJIeEVG8PASRCwmryUwghFKPlHETQ8jJ+Y8+1asRydi
|
||||
psP3B/5Mjhqv/uOK+Vy3zAyIpyDOMtIpOVfjSpCplVRdtSTFWBu9Em7j5I2HMn1w
|
||||
sJZnJgXKpybpibGiiTtmnFLOwibmprSu04rsnP4ncdC2XRD4wIjoyA+4PKgX3sCO
|
||||
klEzKryWYBmLkJOMDdo52LttP3279s7XrkLEE7ia0fXa2c12EQ0f0DQ1tGUvyVEW
|
||||
WmJVccm5bq25AQ0EUw5EzQEIANaPUY04/g7AmYkOMjaCZ6iTp9hB5Rsj/4ee/ln9
|
||||
wArzRO9+3eejLWh53FoN1rO+su7tiXJA5YAzVy6tuolrqjM8DBztPxdLBbEi4V+j
|
||||
2tK0dATdBQBHEh3OJApO2UBtcjaZBT31zrG9K55D+CrcgIVEHAKY8Cb4kLBkb5wM
|
||||
skn+DrASKU0BNIV1qRsxfiUdQHZfSqtp004nrql1lbFMLFEuiY8FZrkkQ9qduixo
|
||||
mTT6f34/oiY+Jam3zCK7RDN/OjuWheIPGj/Qbx9JuNiwgX6yRj7OE1tjUx6d8g9y
|
||||
0H1fmLJbb3WZZbuuGFnK6qrE3bGeY8+AWaJAZ37wpWh1p0cAEQEAAYkBHwQYAQIA
|
||||
CQUCUw5EzQIbDAAKCRBRhS2HNI/8TJntCAClU7TOO/X053eKF1jqNW4A1qpxctVc
|
||||
z8eTcY8Om5O4f6a/rfxfNFKn9Qyja/OG1xWNobETy7MiMXYjaa8uUx5iFy6kMVaP
|
||||
0BXJ59NLZjMARGw6lVTYDTIvzqqqwLxgliSDfSnqUhubGwvykANPO+93BBx89MRG
|
||||
unNoYGXtPlhNFrAsB1VR8+EyKLv2HQtGCPSFBhrjuzH3gxGibNDDdFQLxxuJWepJ
|
||||
EK1UbTS4ms0NgZ2Uknqn1WRU1Ki7rE4sTy68iZtWpKQXZEJa0IGnuI2sSINGcXCJ
|
||||
oEIgXTMyCILo34Fa/C6VCm2WBgz9zZO8/rHIiQm1J5zqz0DrDwKBUM9C
|
||||
=LYpS
|
||||
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
|
||||
ZF5q4h4I33PSGDdSvGXn9UMY5Isjpg==
|
||||
=7pIB
|
||||
-----END PGP PUBLIC KEY BLOCK-----`
|
||||
|
||||
// HashicorpPartnersKey is a key created by HashiCorp, used to generate and
|
||||
|
|
|
@ -41,6 +41,17 @@ type fixupBody struct {
|
|||
names map[string]struct{}
|
||||
}
|
||||
|
||||
type unknownBlock interface {
|
||||
Unknown() bool
|
||||
}
|
||||
|
||||
func (b *fixupBody) Unknown() bool {
|
||||
if u, ok := b.original.(unknownBlock); ok {
|
||||
return u.Unknown()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Content decodes content from the body. The given schema must be the lower-level
|
||||
// representation of the same schema that was previously passed to FixUpBlockAttrs,
|
||||
// or else the result is undefined.
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/zclconf/go-cty/cty/convert"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
"github.com/zclconf/go-cty/cty/function/stdlib"
|
||||
"github.com/zclconf/go-cty/cty/gocty"
|
||||
)
|
||||
|
||||
var LengthFunc = function.New(&function.Spec{
|
||||
|
@ -381,6 +382,83 @@ var MatchkeysFunc = function.New(&function.Spec{
|
|||
},
|
||||
})
|
||||
|
||||
// OneFunc returns either the first element of a one-element list, or null
|
||||
// if given a zero-element list.
|
||||
var OneFunc = function.New(&function.Spec{
|
||||
Params: []function.Parameter{
|
||||
{
|
||||
Name: "list",
|
||||
Type: cty.DynamicPseudoType,
|
||||
},
|
||||
},
|
||||
Type: func(args []cty.Value) (cty.Type, error) {
|
||||
ty := args[0].Type()
|
||||
switch {
|
||||
case ty.IsListType() || ty.IsSetType():
|
||||
return ty.ElementType(), nil
|
||||
case ty.IsTupleType():
|
||||
etys := ty.TupleElementTypes()
|
||||
switch len(etys) {
|
||||
case 0:
|
||||
// No specific type information, so we'll ultimately return
|
||||
// a null value of unknown type.
|
||||
return cty.DynamicPseudoType, nil
|
||||
case 1:
|
||||
return etys[0], nil
|
||||
}
|
||||
}
|
||||
return cty.NilType, function.NewArgErrorf(0, "must be a list, set, or tuple value with either zero or one elements")
|
||||
},
|
||||
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
|
||||
val := args[0]
|
||||
ty := val.Type()
|
||||
|
||||
// Our parameter spec above doesn't set AllowUnknown or AllowNull,
|
||||
// so we can assume our top-level collection is both known and non-null
|
||||
// in here.
|
||||
|
||||
switch {
|
||||
case ty.IsListType() || ty.IsSetType():
|
||||
lenVal := val.Length()
|
||||
if !lenVal.IsKnown() {
|
||||
return cty.UnknownVal(retType), nil
|
||||
}
|
||||
var l int
|
||||
err := gocty.FromCtyValue(lenVal, &l)
|
||||
if err != nil {
|
||||
// It would be very strange to get here, because that would
|
||||
// suggest that the length is either not a number or isn't
|
||||
// an integer, which would suggest a bug in cty.
|
||||
return cty.NilVal, fmt.Errorf("invalid collection length: %s", err)
|
||||
}
|
||||
switch l {
|
||||
case 0:
|
||||
return cty.NullVal(retType), nil
|
||||
case 1:
|
||||
var ret cty.Value
|
||||
// We'll use an iterator here because that works for both lists
|
||||
// and sets, whereas indexing directly would only work for lists.
|
||||
// Since we've just checked the length, we should only actually
|
||||
// run this loop body once.
|
||||
for it := val.ElementIterator(); it.Next(); {
|
||||
_, ret = it.Element()
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
case ty.IsTupleType():
|
||||
etys := ty.TupleElementTypes()
|
||||
switch len(etys) {
|
||||
case 0:
|
||||
return cty.NullVal(retType), nil
|
||||
case 1:
|
||||
ret := val.Index(cty.NumberIntVal(0))
|
||||
return ret, nil
|
||||
}
|
||||
}
|
||||
return cty.NilVal, function.NewArgErrorf(0, "must be a list, set, or tuple value with either zero or one elements")
|
||||
},
|
||||
})
|
||||
|
||||
// SumFunc constructs a function that returns the sum of all
|
||||
// numbers provided in a list
|
||||
var SumFunc = function.New(&function.Spec{
|
||||
|
@ -595,6 +673,12 @@ func Matchkeys(values, keys, searchset cty.Value) (cty.Value, error) {
|
|||
return MatchkeysFunc.Call([]cty.Value{values, keys, searchset})
|
||||
}
|
||||
|
||||
// One returns either the first element of a one-element list, or null
|
||||
// if given a zero-element list..
|
||||
func One(list cty.Value) (cty.Value, error) {
|
||||
return OneFunc.Call([]cty.Value{list})
|
||||
}
|
||||
|
||||
// Sum adds numbers in a list, set, or tuple
|
||||
func Sum(list cty.Value) (cty.Value, error) {
|
||||
return SumFunc.Call([]cty.Value{list})
|
||||
|
|
|
@ -993,6 +993,287 @@ func TestMatchkeys(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestOne(t *testing.T) {
|
||||
tests := []struct {
|
||||
List cty.Value
|
||||
Want cty.Value
|
||||
Err string
|
||||
}{
|
||||
{
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.NumberIntVal(1),
|
||||
}),
|
||||
cty.NumberIntVal(1),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.ListValEmpty(cty.Number),
|
||||
cty.NullVal(cty.Number),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.NumberIntVal(1),
|
||||
cty.NumberIntVal(2),
|
||||
cty.NumberIntVal(3),
|
||||
}),
|
||||
cty.NilVal,
|
||||
"must be a list, set, or tuple value with either zero or one elements",
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.UnknownVal(cty.Number),
|
||||
}),
|
||||
cty.UnknownVal(cty.Number),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.UnknownVal(cty.Number),
|
||||
cty.UnknownVal(cty.Number),
|
||||
}),
|
||||
cty.NilVal,
|
||||
"must be a list, set, or tuple value with either zero or one elements",
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.List(cty.String)),
|
||||
cty.UnknownVal(cty.String),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.NullVal(cty.List(cty.String)),
|
||||
cty.NilVal,
|
||||
"argument must not be null",
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.NumberIntVal(1),
|
||||
}).Mark("boop"),
|
||||
cty.NumberIntVal(1).Mark("boop"),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.ListValEmpty(cty.Bool).Mark("boop"),
|
||||
cty.NullVal(cty.Bool).Mark("boop"),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.NumberIntVal(1).Mark("boop"),
|
||||
}),
|
||||
cty.NumberIntVal(1).Mark("boop"),
|
||||
"",
|
||||
},
|
||||
|
||||
{
|
||||
cty.SetVal([]cty.Value{
|
||||
cty.NumberIntVal(1),
|
||||
}),
|
||||
cty.NumberIntVal(1),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.SetValEmpty(cty.Number),
|
||||
cty.NullVal(cty.Number),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.SetVal([]cty.Value{
|
||||
cty.NumberIntVal(1),
|
||||
cty.NumberIntVal(2),
|
||||
cty.NumberIntVal(3),
|
||||
}),
|
||||
cty.NilVal,
|
||||
"must be a list, set, or tuple value with either zero or one elements",
|
||||
},
|
||||
{
|
||||
cty.SetVal([]cty.Value{
|
||||
cty.UnknownVal(cty.Number),
|
||||
}),
|
||||
cty.UnknownVal(cty.Number),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.SetVal([]cty.Value{
|
||||
cty.UnknownVal(cty.Number),
|
||||
cty.UnknownVal(cty.Number),
|
||||
}),
|
||||
// The above would be valid if those two unknown values were
|
||||
// equal known values, so this returns unknown rather than failing.
|
||||
cty.UnknownVal(cty.Number),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.Set(cty.String)),
|
||||
cty.UnknownVal(cty.String),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.NullVal(cty.Set(cty.String)),
|
||||
cty.NilVal,
|
||||
"argument must not be null",
|
||||
},
|
||||
{
|
||||
cty.SetVal([]cty.Value{
|
||||
cty.NumberIntVal(1),
|
||||
}).Mark("boop"),
|
||||
cty.NumberIntVal(1).Mark("boop"),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.SetValEmpty(cty.Bool).Mark("boop"),
|
||||
cty.NullVal(cty.Bool).Mark("boop"),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.SetVal([]cty.Value{
|
||||
cty.NumberIntVal(1).Mark("boop"),
|
||||
}),
|
||||
cty.NumberIntVal(1).Mark("boop"),
|
||||
"",
|
||||
},
|
||||
|
||||
{
|
||||
cty.TupleVal([]cty.Value{
|
||||
cty.NumberIntVal(1),
|
||||
}),
|
||||
cty.NumberIntVal(1),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.EmptyTupleVal,
|
||||
cty.NullVal(cty.DynamicPseudoType),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.TupleVal([]cty.Value{
|
||||
cty.NumberIntVal(1),
|
||||
cty.NumberIntVal(2),
|
||||
cty.NumberIntVal(3),
|
||||
}),
|
||||
cty.NilVal,
|
||||
"must be a list, set, or tuple value with either zero or one elements",
|
||||
},
|
||||
{
|
||||
cty.TupleVal([]cty.Value{
|
||||
cty.UnknownVal(cty.Number),
|
||||
}),
|
||||
cty.UnknownVal(cty.Number),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.TupleVal([]cty.Value{
|
||||
cty.UnknownVal(cty.Number),
|
||||
cty.UnknownVal(cty.Number),
|
||||
}),
|
||||
cty.NilVal,
|
||||
"must be a list, set, or tuple value with either zero or one elements",
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.EmptyTuple),
|
||||
// Could actually return null here, but don't for consistency with unknown lists
|
||||
cty.UnknownVal(cty.DynamicPseudoType),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.Tuple([]cty.Type{cty.Bool})),
|
||||
cty.UnknownVal(cty.Bool),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.Tuple([]cty.Type{cty.Bool, cty.Number})),
|
||||
cty.NilVal,
|
||||
"must be a list, set, or tuple value with either zero or one elements",
|
||||
},
|
||||
{
|
||||
cty.NullVal(cty.EmptyTuple),
|
||||
cty.NilVal,
|
||||
"argument must not be null",
|
||||
},
|
||||
{
|
||||
cty.NullVal(cty.Tuple([]cty.Type{cty.Bool})),
|
||||
cty.NilVal,
|
||||
"argument must not be null",
|
||||
},
|
||||
{
|
||||
cty.NullVal(cty.Tuple([]cty.Type{cty.Bool, cty.Number})),
|
||||
cty.NilVal,
|
||||
"argument must not be null",
|
||||
},
|
||||
{
|
||||
cty.TupleVal([]cty.Value{
|
||||
cty.NumberIntVal(1),
|
||||
}).Mark("boop"),
|
||||
cty.NumberIntVal(1).Mark("boop"),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.EmptyTupleVal.Mark("boop"),
|
||||
cty.NullVal(cty.DynamicPseudoType).Mark("boop"),
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.TupleVal([]cty.Value{
|
||||
cty.NumberIntVal(1).Mark("boop"),
|
||||
}),
|
||||
cty.NumberIntVal(1).Mark("boop"),
|
||||
"",
|
||||
},
|
||||
|
||||
{
|
||||
cty.DynamicVal,
|
||||
cty.DynamicVal,
|
||||
"",
|
||||
},
|
||||
{
|
||||
cty.NullVal(cty.DynamicPseudoType),
|
||||
cty.NilVal,
|
||||
"argument must not be null",
|
||||
},
|
||||
{
|
||||
cty.MapValEmpty(cty.String),
|
||||
cty.NilVal,
|
||||
"must be a list, set, or tuple value with either zero or one elements",
|
||||
},
|
||||
{
|
||||
cty.EmptyObjectVal,
|
||||
cty.NilVal,
|
||||
"must be a list, set, or tuple value with either zero or one elements",
|
||||
},
|
||||
{
|
||||
cty.True,
|
||||
cty.NilVal,
|
||||
"must be a list, set, or tuple value with either zero or one elements",
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.Bool),
|
||||
cty.NilVal,
|
||||
"must be a list, set, or tuple value with either zero or one elements",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("one(%#v)", test.List), func(t *testing.T) {
|
||||
got, err := One(test.List)
|
||||
|
||||
if test.Err != "" {
|
||||
if err == nil {
|
||||
t.Fatal("succeeded; want error")
|
||||
} else if got, want := err.Error(), test.Err; got != want {
|
||||
t.Fatalf("wrong error\n got: %s\nwant: %s", got, want)
|
||||
}
|
||||
return
|
||||
} else if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if !test.Want.RawEquals(got) {
|
||||
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSum(t *testing.T) {
|
||||
tests := []struct {
|
||||
List cty.Value
|
||||
|
@ -1260,6 +1541,99 @@ func TestTranspose(t *testing.T) {
|
|||
cty.NilVal,
|
||||
true,
|
||||
},
|
||||
{ // marks (deep or shallow) on any elements will propegate to the entire return value
|
||||
cty.MapVal(map[string]cty.Value{
|
||||
"key1": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("a").Mark("beep"), // mark on the inner list element
|
||||
cty.StringVal("b"),
|
||||
}),
|
||||
"key2": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("a"),
|
||||
cty.StringVal("b"),
|
||||
cty.StringVal("c"),
|
||||
}).Mark("boop"), // mark on the map element
|
||||
"key3": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("c"),
|
||||
}),
|
||||
"key4": cty.ListValEmpty(cty.String),
|
||||
}),
|
||||
cty.MapVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("key1"),
|
||||
cty.StringVal("key2"),
|
||||
}),
|
||||
"b": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("key1"),
|
||||
cty.StringVal("key2"),
|
||||
}),
|
||||
"c": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("key2"),
|
||||
cty.StringVal("key3")}),
|
||||
}).WithMarks(cty.NewValueMarks("beep", "boop")),
|
||||
false,
|
||||
},
|
||||
{ // Marks on the input value will be applied to the return value
|
||||
cty.MapVal(map[string]cty.Value{
|
||||
"key1": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("a"),
|
||||
cty.StringVal("b"),
|
||||
}),
|
||||
"key2": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("a"),
|
||||
cty.StringVal("b"),
|
||||
cty.StringVal("c"),
|
||||
}),
|
||||
"key3": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("c"),
|
||||
}),
|
||||
}).Mark("beep"), // mark on the entire input value
|
||||
cty.MapVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("key1"),
|
||||
cty.StringVal("key2"),
|
||||
}),
|
||||
"b": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("key1"),
|
||||
cty.StringVal("key2"),
|
||||
}),
|
||||
"c": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("key2"),
|
||||
cty.StringVal("key3"),
|
||||
}),
|
||||
}).Mark("beep"),
|
||||
false,
|
||||
},
|
||||
{ // Marks on the entire input value AND inner elements (deep or shallow) ALL apply to the return
|
||||
cty.MapVal(map[string]cty.Value{
|
||||
"key1": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("a"),
|
||||
cty.StringVal("b"),
|
||||
}).Mark("beep"), // mark on the map element
|
||||
"key2": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("a"),
|
||||
cty.StringVal("b"),
|
||||
cty.StringVal("c"),
|
||||
}),
|
||||
"key3": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("c").Mark("boop"), // mark on the inner list element
|
||||
}),
|
||||
}).Mark("bloop"), // mark on the entire input value
|
||||
cty.MapVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("key1"),
|
||||
cty.StringVal("key2"),
|
||||
}),
|
||||
"b": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("key1"),
|
||||
cty.StringVal("key2"),
|
||||
}),
|
||||
"c": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("key2"),
|
||||
cty.StringVal("key3"),
|
||||
}),
|
||||
}).WithMarks(cty.NewValueMarks("beep", "boop", "bloop")),
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package funcs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
|
@ -28,8 +31,9 @@ func MakeToFunc(wantTy cty.Type) function.Function {
|
|||
// messages to be more appropriate for an explicit type
|
||||
// conversion, whereas the cty function system produces
|
||||
// messages aimed at _implicit_ type conversions.
|
||||
Type: cty.DynamicPseudoType,
|
||||
AllowNull: true,
|
||||
Type: cty.DynamicPseudoType,
|
||||
AllowNull: true,
|
||||
AllowMarked: true,
|
||||
},
|
||||
},
|
||||
Type: func(args []cty.Value) (cty.Type, error) {
|
||||
|
@ -65,6 +69,11 @@ func MakeToFunc(wantTy cty.Type) function.Function {
|
|||
// once we note that the value isn't either "true" or "false".
|
||||
gotTy := args[0].Type()
|
||||
switch {
|
||||
case args[0].ContainsMarked():
|
||||
// Generic message so we won't inadvertently disclose
|
||||
// information about sensitive values.
|
||||
return cty.NilVal, function.NewArgErrorf(0, "cannot convert this sensitive %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint())
|
||||
|
||||
case gotTy == cty.String && wantTy == cty.Bool:
|
||||
what := "string"
|
||||
if !args[0].IsNull() {
|
||||
|
@ -85,3 +94,128 @@ func MakeToFunc(wantTy cty.Type) function.Function {
|
|||
},
|
||||
})
|
||||
}
|
||||
|
||||
var TypeFunc = function.New(&function.Spec{
|
||||
Params: []function.Parameter{
|
||||
{
|
||||
Name: "value",
|
||||
Type: cty.DynamicPseudoType,
|
||||
AllowDynamicType: true,
|
||||
AllowUnknown: true,
|
||||
AllowNull: true,
|
||||
},
|
||||
},
|
||||
Type: function.StaticReturnType(cty.String),
|
||||
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||||
return cty.StringVal(TypeString(args[0].Type())).Mark("raw"), nil
|
||||
},
|
||||
})
|
||||
|
||||
// Modified copy of TypeString from go-cty:
|
||||
// https://github.com/zclconf/go-cty-debug/blob/master/ctydebug/type_string.go
|
||||
//
|
||||
// TypeString returns a string representation of a given type that is
|
||||
// reminiscent of Go syntax calling into the cty package but is mainly
|
||||
// intended for easy human inspection of values in tests, debug output, etc.
|
||||
//
|
||||
// The resulting string will include newlines and indentation in order to
|
||||
// increase the readability of complex structures. It always ends with a
|
||||
// newline, so you can print this result directly to your output.
|
||||
func TypeString(ty cty.Type) string {
|
||||
var b strings.Builder
|
||||
writeType(ty, &b, 0)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func writeType(ty cty.Type, b *strings.Builder, indent int) {
|
||||
switch {
|
||||
case ty == cty.NilType:
|
||||
b.WriteString("nil")
|
||||
return
|
||||
case ty.IsObjectType():
|
||||
atys := ty.AttributeTypes()
|
||||
if len(atys) == 0 {
|
||||
b.WriteString("object({})")
|
||||
return
|
||||
}
|
||||
attrNames := make([]string, 0, len(atys))
|
||||
for name := range atys {
|
||||
attrNames = append(attrNames, name)
|
||||
}
|
||||
sort.Strings(attrNames)
|
||||
b.WriteString("object({\n")
|
||||
indent++
|
||||
for _, name := range attrNames {
|
||||
aty := atys[name]
|
||||
b.WriteString(indentSpaces(indent))
|
||||
fmt.Fprintf(b, "%s: ", name)
|
||||
writeType(aty, b, indent)
|
||||
b.WriteString(",\n")
|
||||
}
|
||||
indent--
|
||||
b.WriteString(indentSpaces(indent))
|
||||
b.WriteString("})")
|
||||
case ty.IsTupleType():
|
||||
etys := ty.TupleElementTypes()
|
||||
if len(etys) == 0 {
|
||||
b.WriteString("tuple([])")
|
||||
return
|
||||
}
|
||||
b.WriteString("tuple([\n")
|
||||
indent++
|
||||
for _, ety := range etys {
|
||||
b.WriteString(indentSpaces(indent))
|
||||
writeType(ety, b, indent)
|
||||
b.WriteString(",\n")
|
||||
}
|
||||
indent--
|
||||
b.WriteString(indentSpaces(indent))
|
||||
b.WriteString("])")
|
||||
case ty.IsCollectionType():
|
||||
ety := ty.ElementType()
|
||||
switch {
|
||||
case ty.IsListType():
|
||||
b.WriteString("list(")
|
||||
case ty.IsMapType():
|
||||
b.WriteString("map(")
|
||||
case ty.IsSetType():
|
||||
b.WriteString("set(")
|
||||
default:
|
||||
// At the time of writing there are no other collection types,
|
||||
// but we'll be robust here and just pass through the GoString
|
||||
// of anything we don't recognize.
|
||||
b.WriteString(ty.FriendlyName())
|
||||
return
|
||||
}
|
||||
// Because object and tuple types render split over multiple
|
||||
// lines, a collection type container around them can end up
|
||||
// being hard to see when scanning, so we'll generate some extra
|
||||
// indentation to make a collection of structural type more visually
|
||||
// distinct from the structural type alone.
|
||||
complexElem := ety.IsObjectType() || ety.IsTupleType()
|
||||
if complexElem {
|
||||
indent++
|
||||
b.WriteString("\n")
|
||||
b.WriteString(indentSpaces(indent))
|
||||
}
|
||||
writeType(ty.ElementType(), b, indent)
|
||||
if complexElem {
|
||||
indent--
|
||||
b.WriteString(",\n")
|
||||
b.WriteString(indentSpaces(indent))
|
||||
}
|
||||
b.WriteString(")")
|
||||
default:
|
||||
// For any other type we'll just use its GoString and assume it'll
|
||||
// follow the usual GoString conventions.
|
||||
b.WriteString(ty.FriendlyName())
|
||||
}
|
||||
}
|
||||
|
||||
func indentSpaces(level int) string {
|
||||
return strings.Repeat(" ", level)
|
||||
}
|
||||
|
||||
func Type(input []cty.Value) (cty.Value, error) {
|
||||
return TypeFunc.Call(input)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
|
@ -32,6 +33,18 @@ func TestTo(t *testing.T) {
|
|||
cty.NullVal(cty.String),
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.StringVal("a").Mark("boop"),
|
||||
cty.String,
|
||||
cty.StringVal("a").Mark("boop"),
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.NullVal(cty.String).Mark("boop"),
|
||||
cty.String,
|
||||
cty.NullVal(cty.String).Mark("boop"),
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.True,
|
||||
cty.String,
|
||||
|
@ -44,12 +57,24 @@ func TestTo(t *testing.T) {
|
|||
cty.DynamicVal,
|
||||
`cannot convert "a" to bool; only the strings "true" or "false" are allowed`,
|
||||
},
|
||||
{
|
||||
cty.StringVal("a").Mark("boop"),
|
||||
cty.Bool,
|
||||
cty.DynamicVal,
|
||||
`cannot convert this sensitive string to bool`,
|
||||
},
|
||||
{
|
||||
cty.StringVal("a"),
|
||||
cty.Number,
|
||||
cty.DynamicVal,
|
||||
`cannot convert "a" to number; given string must be a decimal representation of a number`,
|
||||
},
|
||||
{
|
||||
cty.StringVal("a").Mark("boop"),
|
||||
cty.Number,
|
||||
cty.DynamicVal,
|
||||
`cannot convert this sensitive string to number`,
|
||||
},
|
||||
{
|
||||
cty.NullVal(cty.String),
|
||||
cty.Number,
|
||||
|
@ -86,6 +111,30 @@ func TestTo(t *testing.T) {
|
|||
cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("hello"), "bar": cty.StringVal("true")}),
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("hello"), "bar": cty.StringVal("world").Mark("boop")}),
|
||||
cty.Map(cty.String),
|
||||
cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("hello"), "bar": cty.StringVal("world").Mark("boop")}),
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("hello"), "bar": cty.StringVal("world")}).Mark("boop"),
|
||||
cty.Map(cty.String),
|
||||
cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("hello"), "bar": cty.StringVal("world")}).Mark("boop"),
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.TupleVal([]cty.Value{cty.StringVal("hello"), cty.StringVal("world").Mark("boop")}),
|
||||
cty.List(cty.String),
|
||||
cty.ListVal([]cty.Value{cty.StringVal("hello"), cty.StringVal("world").Mark("boop")}),
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.TupleVal([]cty.Value{cty.StringVal("hello"), cty.StringVal("world")}).Mark("boop"),
|
||||
cty.List(cty.String),
|
||||
cty.ListVal([]cty.Value{cty.StringVal("hello"), cty.StringVal("world")}).Mark("boop"),
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.EmptyTupleVal,
|
||||
cty.String,
|
||||
|
@ -129,3 +178,92 @@ func TestTo(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestType(t *testing.T) {
|
||||
tests := []struct {
|
||||
Input cty.Value
|
||||
Want string
|
||||
}{
|
||||
// Primititves
|
||||
{
|
||||
cty.StringVal("a"),
|
||||
"string",
|
||||
},
|
||||
{
|
||||
cty.NumberIntVal(42),
|
||||
"number",
|
||||
},
|
||||
{
|
||||
cty.BoolVal(true),
|
||||
"bool",
|
||||
},
|
||||
// Collections
|
||||
{
|
||||
cty.EmptyObjectVal,
|
||||
`object({})`,
|
||||
},
|
||||
{
|
||||
cty.EmptyTupleVal,
|
||||
`tuple([])`,
|
||||
},
|
||||
{
|
||||
cty.ListValEmpty(cty.String),
|
||||
`list(string)`,
|
||||
},
|
||||
{
|
||||
cty.MapValEmpty(cty.String),
|
||||
`map(string)`,
|
||||
},
|
||||
{
|
||||
cty.SetValEmpty(cty.String),
|
||||
`set(string)`,
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{cty.StringVal("a")}),
|
||||
`list(string)`,
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{cty.ListVal([]cty.Value{cty.NumberIntVal(42)})}),
|
||||
`list(list(number))`,
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{cty.MapValEmpty(cty.String)}),
|
||||
`list(map(string))`,
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("bar"),
|
||||
})}),
|
||||
"list(\n object({\n foo: string,\n }),\n)",
|
||||
},
|
||||
// Unknowns and Nulls
|
||||
{
|
||||
cty.UnknownVal(cty.String),
|
||||
"string",
|
||||
},
|
||||
{
|
||||
cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"foo": cty.String,
|
||||
})),
|
||||
"object({\n foo: string,\n})",
|
||||
},
|
||||
{ // irrelevant marks do nothing
|
||||
cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("bar").Mark("ignore me"),
|
||||
})}),
|
||||
"list(\n object({\n foo: string,\n }),\n)",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
got, err := Type([]cty.Value{test.Input})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
// The value is marked to help with formatting
|
||||
got, _ = got.Unmark()
|
||||
|
||||
if got.AsString() != test.Want {
|
||||
t.Errorf("wrong result:\n%s", cmp.Diff(got.AsString(), test.Want))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,13 +16,15 @@ import (
|
|||
var DefaultsFunc = function.New(&function.Spec{
|
||||
Params: []function.Parameter{
|
||||
{
|
||||
Name: "input",
|
||||
Type: cty.DynamicPseudoType,
|
||||
AllowNull: true,
|
||||
Name: "input",
|
||||
Type: cty.DynamicPseudoType,
|
||||
AllowNull: true,
|
||||
AllowMarked: true,
|
||||
},
|
||||
{
|
||||
Name: "defaults",
|
||||
Type: cty.DynamicPseudoType,
|
||||
Name: "defaults",
|
||||
Type: cty.DynamicPseudoType,
|
||||
AllowMarked: true,
|
||||
},
|
||||
},
|
||||
Type: func(args []cty.Value) (cty.Type, error) {
|
||||
|
@ -69,8 +71,14 @@ var DefaultsFunc = function.New(&function.Spec{
|
|||
|
||||
func defaultsApply(input, fallback cty.Value) cty.Value {
|
||||
wantTy := input.Type()
|
||||
if !(input.IsKnown() && fallback.IsKnown()) {
|
||||
return cty.UnknownVal(wantTy)
|
||||
|
||||
umInput, inputMarks := input.Unmark()
|
||||
umFb, fallbackMarks := fallback.Unmark()
|
||||
|
||||
// If neither are known, we very conservatively return an unknown value
|
||||
// with the union of marks on both input and default.
|
||||
if !(umInput.IsKnown() && umFb.IsKnown()) {
|
||||
return cty.UnknownVal(wantTy).WithMarks(inputMarks).WithMarks(fallbackMarks)
|
||||
}
|
||||
|
||||
// For the rest of this function we're assuming that the given defaults
|
||||
|
@ -83,15 +91,15 @@ func defaultsApply(input, fallback cty.Value) cty.Value {
|
|||
case wantTy.IsPrimitiveType():
|
||||
// For leaf primitive values the rule is relatively simple: use the
|
||||
// input if it's non-null, or fallback if input is null.
|
||||
if !input.IsNull() {
|
||||
if !umInput.IsNull() {
|
||||
return input
|
||||
}
|
||||
v, err := convert.Convert(fallback, wantTy)
|
||||
v, err := convert.Convert(umFb, wantTy)
|
||||
if err != nil {
|
||||
// Should not happen because we checked in defaultsAssertSuitableFallback
|
||||
panic(err.Error())
|
||||
}
|
||||
return v
|
||||
return v.WithMarks(fallbackMarks)
|
||||
|
||||
case wantTy.IsObjectType():
|
||||
// For structural types, a null input value must be passed through. We
|
||||
|
@ -101,18 +109,18 @@ func defaultsApply(input, fallback cty.Value) cty.Value {
|
|||
// We also pass through the input if the fallback value is null. This
|
||||
// can happen if the given defaults do not include a value for this
|
||||
// attribute.
|
||||
if input.IsNull() || fallback.IsNull() {
|
||||
if umInput.IsNull() || umFb.IsNull() {
|
||||
return input
|
||||
}
|
||||
atys := wantTy.AttributeTypes()
|
||||
ret := map[string]cty.Value{}
|
||||
for attr, aty := range atys {
|
||||
inputSub := input.GetAttr(attr)
|
||||
inputSub := umInput.GetAttr(attr)
|
||||
fallbackSub := cty.NullVal(aty)
|
||||
if fallback.Type().HasAttribute(attr) {
|
||||
fallbackSub = fallback.GetAttr(attr)
|
||||
if umFb.Type().HasAttribute(attr) {
|
||||
fallbackSub = umFb.GetAttr(attr)
|
||||
}
|
||||
ret[attr] = defaultsApply(inputSub, fallbackSub)
|
||||
ret[attr] = defaultsApply(inputSub.WithMarks(inputMarks), fallbackSub.WithMarks(fallbackMarks))
|
||||
}
|
||||
return cty.ObjectVal(ret)
|
||||
|
||||
|
@ -124,16 +132,16 @@ func defaultsApply(input, fallback cty.Value) cty.Value {
|
|||
// We also pass through the input if the fallback value is null. This
|
||||
// can happen if the given defaults do not include a value for this
|
||||
// attribute.
|
||||
if input.IsNull() || fallback.IsNull() {
|
||||
if umInput.IsNull() || umFb.IsNull() {
|
||||
return input
|
||||
}
|
||||
|
||||
l := wantTy.Length()
|
||||
ret := make([]cty.Value, l)
|
||||
for i := 0; i < l; i++ {
|
||||
inputSub := input.Index(cty.NumberIntVal(int64(i)))
|
||||
fallbackSub := fallback.Index(cty.NumberIntVal(int64(i)))
|
||||
ret[i] = defaultsApply(inputSub, fallbackSub)
|
||||
inputSub := umInput.Index(cty.NumberIntVal(int64(i)))
|
||||
fallbackSub := umFb.Index(cty.NumberIntVal(int64(i)))
|
||||
ret[i] = defaultsApply(inputSub.WithMarks(inputMarks), fallbackSub.WithMarks(fallbackMarks))
|
||||
}
|
||||
return cty.TupleVal(ret)
|
||||
|
||||
|
@ -148,10 +156,10 @@ func defaultsApply(input, fallback cty.Value) cty.Value {
|
|||
case wantTy.IsMapType():
|
||||
newVals := map[string]cty.Value{}
|
||||
|
||||
if !input.IsNull() {
|
||||
for it := input.ElementIterator(); it.Next(); {
|
||||
if !umInput.IsNull() {
|
||||
for it := umInput.ElementIterator(); it.Next(); {
|
||||
k, v := it.Element()
|
||||
newVals[k.AsString()] = defaultsApply(v, fallback)
|
||||
newVals[k.AsString()] = defaultsApply(v.WithMarks(inputMarks), fallback.WithMarks(fallbackMarks))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -162,10 +170,10 @@ func defaultsApply(input, fallback cty.Value) cty.Value {
|
|||
case wantTy.IsListType(), wantTy.IsSetType():
|
||||
var newVals []cty.Value
|
||||
|
||||
if !input.IsNull() {
|
||||
for it := input.ElementIterator(); it.Next(); {
|
||||
if !umInput.IsNull() {
|
||||
for it := umInput.ElementIterator(); it.Next(); {
|
||||
_, v := it.Element()
|
||||
newV := defaultsApply(v, fallback)
|
||||
newV := defaultsApply(v.WithMarks(inputMarks), fallback.WithMarks(fallbackMarks))
|
||||
newVals = append(newVals, newV)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,30 @@ func TestDefaults(t *testing.T) {
|
|||
Want cty.Value
|
||||
WantErr string
|
||||
}{
|
||||
{ // When *either* input or default are unknown, an unknown is returned.
|
||||
Input: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
Defaults: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.StringVal("hello"),
|
||||
}),
|
||||
Want: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
},
|
||||
{
|
||||
// When *either* input or default are unknown, an unknown is
|
||||
// returned with marks from both input and defaults.
|
||||
Input: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
Defaults: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.StringVal("hello").Mark("marked"),
|
||||
}),
|
||||
Want: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.UnknownVal(cty.String).Mark("marked"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
Input: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.NullVal(cty.String),
|
||||
|
@ -494,6 +518,110 @@ func TestDefaults(t *testing.T) {
|
|||
}),
|
||||
WantErr: ".a: invalid default value for bool: bool required",
|
||||
},
|
||||
// marks: we should preserve marks from both input value and defaults as leafily as possible
|
||||
{
|
||||
Input: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.NullVal(cty.String),
|
||||
}),
|
||||
Defaults: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.StringVal("hello").Mark("world"),
|
||||
}),
|
||||
Want: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.StringVal("hello").Mark("world"),
|
||||
}),
|
||||
},
|
||||
{ // "unused" marks don't carry over
|
||||
Input: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.NullVal(cty.String).Mark("a"),
|
||||
}),
|
||||
Defaults: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.StringVal("hello"),
|
||||
}),
|
||||
Want: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.StringVal("hello"),
|
||||
}),
|
||||
},
|
||||
{ // Marks on tuples remain attached to individual elements
|
||||
Input: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.TupleVal([]cty.Value{
|
||||
cty.NullVal(cty.String),
|
||||
cty.StringVal("hey").Mark("input"),
|
||||
cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
Defaults: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.TupleVal([]cty.Value{
|
||||
cty.StringVal("hello 0").Mark("fallback"),
|
||||
cty.StringVal("hello 1"),
|
||||
cty.StringVal("hello 2"),
|
||||
}),
|
||||
}),
|
||||
Want: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.TupleVal([]cty.Value{
|
||||
cty.StringVal("hello 0").Mark("fallback"),
|
||||
cty.StringVal("hey").Mark("input"),
|
||||
cty.StringVal("hello 2"),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{ // Marks from list elements
|
||||
Input: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.NullVal(cty.String),
|
||||
cty.StringVal("hey").Mark("input"),
|
||||
cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
Defaults: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.StringVal("hello 0").Mark("fallback"),
|
||||
}),
|
||||
Want: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("hello 0").Mark("fallback"),
|
||||
cty.StringVal("hey").Mark("input"),
|
||||
cty.StringVal("hello 0").Mark("fallback"),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{
|
||||
// Sets don't allow individually-marked elements, so the marks
|
||||
// end up aggregating on the set itself anyway in this case.
|
||||
Input: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.SetVal([]cty.Value{
|
||||
cty.NullVal(cty.String),
|
||||
cty.NullVal(cty.String),
|
||||
cty.StringVal("hey").Mark("input"),
|
||||
}),
|
||||
}),
|
||||
Defaults: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.StringVal("hello 0").Mark("fallback"),
|
||||
}),
|
||||
Want: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.SetVal([]cty.Value{
|
||||
cty.StringVal("hello 0"),
|
||||
cty.StringVal("hey"),
|
||||
cty.StringVal("hello 0"),
|
||||
}).WithMarks(cty.NewValueMarks("fallback", "input")),
|
||||
}),
|
||||
},
|
||||
{
|
||||
Input: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
Defaults: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.StringVal("hello").Mark("beep"),
|
||||
}).Mark("boop"),
|
||||
// This is the least-intuitive case. The mark "boop" is attached to
|
||||
// the default object, not it's elements, but both marks end up
|
||||
// aggregated on the list element.
|
||||
Want: cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("hello").WithMarks(cty.NewValueMarks("beep", "boop")),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
|
|
@ -48,7 +48,7 @@ var NonsensitiveFunc = function.New(&function.Spec{
|
|||
return args[0].Type(), nil
|
||||
},
|
||||
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
|
||||
if !args[0].HasMark("sensitive") {
|
||||
if args[0].IsKnown() && !args[0].HasMark("sensitive") {
|
||||
return cty.DynamicVal, function.NewArgErrorf(0, "the given value is not sensitive, so this call is redundant")
|
||||
}
|
||||
v, marks := args[0].Unmark()
|
||||
|
|
|
@ -133,17 +133,20 @@ func TestNonsensitive(t *testing.T) {
|
|||
cty.NumberIntVal(1),
|
||||
`the given value is not sensitive, so this call is redundant`,
|
||||
},
|
||||
{
|
||||
cty.DynamicVal,
|
||||
`the given value is not sensitive, so this call is redundant`,
|
||||
},
|
||||
{
|
||||
cty.NullVal(cty.String),
|
||||
`the given value is not sensitive, so this call is redundant`,
|
||||
},
|
||||
|
||||
// Unknown values may become sensitive once they are known, so we
|
||||
// permit them to be marked nonsensitive.
|
||||
{
|
||||
cty.DynamicVal,
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.String),
|
||||
`the given value is not sensitive, so this call is redundant`,
|
||||
``,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -93,6 +93,7 @@ func (s *Scope) Functions() map[string]function.Function {
|
|||
"md5": funcs.Md5Func,
|
||||
"merge": stdlib.MergeFunc,
|
||||
"min": stdlib.MinFunc,
|
||||
"one": funcs.OneFunc,
|
||||
"parseint": stdlib.ParseIntFunc,
|
||||
"pathexpand": funcs.PathExpandFunc,
|
||||
"pow": stdlib.PowFunc,
|
||||
|
@ -151,6 +152,11 @@ func (s *Scope) Functions() map[string]function.Function {
|
|||
return s.funcs
|
||||
})
|
||||
|
||||
if s.ConsoleMode {
|
||||
// The type function is only available in terraform console.
|
||||
s.funcs["type"] = funcs.TypeFunc
|
||||
}
|
||||
|
||||
if s.PureOnly {
|
||||
// Force our few impure functions to return unknown so that we
|
||||
// can defer evaluating them until a later pass.
|
||||
|
|
|
@ -614,6 +614,17 @@ func TestFunctions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
|
||||
"one": {
|
||||
{
|
||||
`one([])`,
|
||||
cty.NullVal(cty.DynamicPseudoType),
|
||||
},
|
||||
{
|
||||
`one([true])`,
|
||||
cty.True,
|
||||
},
|
||||
},
|
||||
|
||||
"parseint": {
|
||||
{
|
||||
`parseint("100", 10)`,
|
||||
|
|
|
@ -37,6 +37,10 @@ type Scope struct {
|
|||
// considered as active in the module that this scope will be used for.
|
||||
// Callers can populate it by calling the SetActiveExperiments method.
|
||||
activeExperiments experiments.Set
|
||||
|
||||
// ConsoleMode can be set to true to request any console-only functions are
|
||||
// included in this scope.
|
||||
ConsoleMode bool
|
||||
}
|
||||
|
||||
// SetActiveExperiments allows a caller to declare that a set of experiments
|
||||
|
|
|
@ -75,20 +75,12 @@ func assertObjectCompatible(schema *configschema.Block, planned, actual cty.Valu
|
|||
plannedV, _ := planned.GetAttr(name).Unmark()
|
||||
actualV, _ := actual.GetAttr(name).Unmark()
|
||||
|
||||
// As a special case, if there were any blocks whose leaf attributes
|
||||
// are all unknown then we assume (possibly incorrectly) that the
|
||||
// HCL dynamic block extension is in use with an unknown for_each
|
||||
// argument, and so we will do looser validation here that allows
|
||||
// for those blocks to have expanded into a different number of blocks
|
||||
// if the for_each value is now known.
|
||||
maybeUnknownBlocks := couldHaveUnknownBlockPlaceholder(plannedV, blockS, false)
|
||||
|
||||
path := append(path, cty.GetAttrStep{Name: name})
|
||||
switch blockS.Nesting {
|
||||
case configschema.NestingSingle, configschema.NestingGroup:
|
||||
// If an unknown block placeholder was present then the placeholder
|
||||
// may have expanded out into zero blocks, which is okay.
|
||||
if maybeUnknownBlocks && actualV.IsNull() {
|
||||
if !plannedV.IsKnown() && actualV.IsNull() {
|
||||
continue
|
||||
}
|
||||
moreErrs := assertObjectCompatible(&blockS.Block, plannedV, actualV, path)
|
||||
|
@ -102,14 +94,6 @@ func assertObjectCompatible(schema *configschema.Block, planned, actual cty.Valu
|
|||
continue
|
||||
}
|
||||
|
||||
if maybeUnknownBlocks {
|
||||
// When unknown blocks are present the final blocks may be
|
||||
// at different indices than the planned blocks, so unfortunately
|
||||
// we can't do our usual checks in this case without generating
|
||||
// false negatives.
|
||||
continue
|
||||
}
|
||||
|
||||
plannedL := plannedV.LengthInt()
|
||||
actualL := actualV.LengthInt()
|
||||
if plannedL != actualL {
|
||||
|
@ -144,7 +128,7 @@ func assertObjectCompatible(schema *configschema.Block, planned, actual cty.Valu
|
|||
moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.GetAttrStep{Name: k}))
|
||||
errs = append(errs, moreErrs...)
|
||||
}
|
||||
if !maybeUnknownBlocks { // new blocks may appear if unknown blocks were present in the plan
|
||||
if plannedV.IsKnown() { // new blocks may appear if unknown blocks were present in the plan
|
||||
for k := range actualAtys {
|
||||
if _, ok := plannedAtys[k]; !ok {
|
||||
errs = append(errs, path.NewErrorf("new block key %q has appeared", k))
|
||||
|
@ -158,7 +142,7 @@ func assertObjectCompatible(schema *configschema.Block, planned, actual cty.Valu
|
|||
}
|
||||
plannedL := plannedV.LengthInt()
|
||||
actualL := actualV.LengthInt()
|
||||
if plannedL != actualL && !maybeUnknownBlocks { // new blocks may appear if unknown blocks were persent in the plan
|
||||
if plannedL != actualL && plannedV.IsKnown() { // new blocks may appear if unknown blocks were persent in the plan
|
||||
errs = append(errs, path.NewErrorf("block count changed from %d to %d", plannedL, actualL))
|
||||
continue
|
||||
}
|
||||
|
@ -177,7 +161,7 @@ func assertObjectCompatible(schema *configschema.Block, planned, actual cty.Valu
|
|||
continue
|
||||
}
|
||||
|
||||
if maybeUnknownBlocks {
|
||||
if !plannedV.IsKnown() {
|
||||
// When unknown blocks are present the final number of blocks
|
||||
// may be different, either because the unknown set values
|
||||
// become equal and are collapsed, or the count is unknown due
|
||||
|
@ -328,96 +312,6 @@ func indexStrForErrors(v cty.Value) string {
|
|||
}
|
||||
}
|
||||
|
||||
// couldHaveUnknownBlockPlaceholder is a heuristic that recognizes how the
|
||||
// HCL dynamic block extension behaves when it's asked to expand a block whose
|
||||
// for_each argument is unknown. In such cases, it generates a single placeholder
|
||||
// block with all leaf attribute values unknown, and once the for_each
|
||||
// expression becomes known the placeholder may be replaced with any number
|
||||
// of blocks, so object compatibility checks would need to be more liberal.
|
||||
//
|
||||
// Set "nested" if testing a block that is nested inside a candidate block
|
||||
// placeholder; this changes the interpretation of there being no blocks of
|
||||
// a type to allow for there being zero nested blocks.
|
||||
func couldHaveUnknownBlockPlaceholder(v cty.Value, blockS *configschema.NestedBlock, nested bool) bool {
|
||||
switch blockS.Nesting {
|
||||
case configschema.NestingSingle, configschema.NestingGroup:
|
||||
if nested && v.IsNull() {
|
||||
return true // for nested blocks, a single block being unset doesn't disqualify from being an unknown block placeholder
|
||||
}
|
||||
return couldBeUnknownBlockPlaceholderElement(v, blockS)
|
||||
default:
|
||||
// These situations should be impossible for correct providers, but
|
||||
// we permit the legacy SDK to produce some incorrect outcomes
|
||||
// for compatibility with its existing logic, and so we must be
|
||||
// tolerant here.
|
||||
if !v.IsKnown() {
|
||||
return true
|
||||
}
|
||||
if v.IsNull() {
|
||||
return false // treated as if the list were empty, so we would see zero iterations below
|
||||
}
|
||||
|
||||
// Unmark before we call ElementIterator in case this iterable is marked sensitive.
|
||||
// This can arise in the case where a member of a Set is sensitive, and thus the
|
||||
// whole Set is marked sensitive
|
||||
v, _ := v.Unmark()
|
||||
// For all other nesting modes, our value should be something iterable.
|
||||
for it := v.ElementIterator(); it.Next(); {
|
||||
_, ev := it.Element()
|
||||
if couldBeUnknownBlockPlaceholderElement(ev, blockS) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Our default changes depending on whether we're testing the candidate
|
||||
// block itself or something nested inside of it: zero blocks of a type
|
||||
// can never contain a dynamic block placeholder, but a dynamic block
|
||||
// placeholder might contain zero blocks of one of its own nested block
|
||||
// types, if none were set in the config at all.
|
||||
return nested
|
||||
}
|
||||
}
|
||||
|
||||
func couldBeUnknownBlockPlaceholderElement(v cty.Value, schema *configschema.NestedBlock) bool {
|
||||
if v.IsNull() {
|
||||
return false // null value can never be a placeholder element
|
||||
}
|
||||
if !v.IsKnown() {
|
||||
return true // this should never happen for well-behaved providers, but can happen with the legacy SDK opt-outs
|
||||
}
|
||||
for name := range schema.Attributes {
|
||||
av := v.GetAttr(name)
|
||||
|
||||
// Unknown block placeholders contain only unknown or null attribute
|
||||
// values, depending on whether or not a particular attribute was set
|
||||
// explicitly inside the content block. Note that this is imprecise:
|
||||
// non-placeholders can also match this, so this function can generate
|
||||
// false positives.
|
||||
if av.IsKnown() && !av.IsNull() {
|
||||
|
||||
// FIXME: only required for the legacy SDK, but we don't have a
|
||||
// separate codepath to switch the comparisons, and we still want
|
||||
// the rest of the checks from AssertObjectCompatible to apply.
|
||||
//
|
||||
// The legacy SDK cannot handle missing strings from set elements,
|
||||
// and will insert an empty string into the planned value.
|
||||
// Skipping these treats them as null values in this case,
|
||||
// preventing false alerts from AssertObjectCompatible.
|
||||
if schema.Nesting == configschema.NestingSet && av.Type() == cty.String && av.AsString() == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
for name, blockS := range schema.BlockTypes {
|
||||
if !couldHaveUnknownBlockPlaceholder(v.GetAttr(name), blockS, true) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// assertSetValuesCompatible checks that each of the elements in a can
|
||||
// be correlated with at least one equivalent element in b and vice-versa,
|
||||
// using the given correlation function.
|
||||
|
|
|
@ -30,10 +30,6 @@ func TestAssertObjectCompatible(t *testing.T) {
|
|||
"foo": cty.StringVal("bar"),
|
||||
"bar": cty.NullVal(cty.String), // simulating the situation where bar isn't set in the config at all
|
||||
})
|
||||
fooBarBlockDynamicPlaceholder := cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.UnknownVal(cty.String),
|
||||
"bar": cty.NullVal(cty.String), // simulating the situation where bar isn't set in the config at all
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
Schema *configschema.Block
|
||||
|
@ -919,13 +915,11 @@ func TestAssertObjectCompatible(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"key": cty.ObjectVal(map[string]cty.Value{
|
||||
// One wholly unknown block is what "dynamic" blocks
|
||||
// generate when the for_each expression is unknown.
|
||||
"foo": cty.UnknownVal(cty.String),
|
||||
cty.UnknownVal(cty.Object(map[string]cty.Type{
|
||||
"key": cty.Object(map[string]cty.Type{
|
||||
"foo": cty.String,
|
||||
}),
|
||||
}),
|
||||
})),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"key": cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"foo": cty.String,
|
||||
|
@ -1011,11 +1005,9 @@ func TestAssertObjectCompatible(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"key": cty.ListVal([]cty.Value{
|
||||
fooBarBlockDynamicPlaceholder, // the presence of this disables some of our checks
|
||||
}),
|
||||
}),
|
||||
cty.UnknownVal(cty.Object(map[string]cty.Type{
|
||||
"key": cty.List(fooBarBlockValue.Type()),
|
||||
})),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"key": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
|
@ -1026,35 +1018,7 @@ func TestAssertObjectCompatible(t *testing.T) {
|
|||
}),
|
||||
}),
|
||||
}),
|
||||
nil, // a single block whose attrs are all unknown is allowed to expand into multiple, because that's how dynamic blocks behave when for_each is unknown
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"key": {
|
||||
Nesting: configschema.NestingList,
|
||||
Block: schemaWithFooBar,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"key": cty.ListVal([]cty.Value{
|
||||
fooBarBlockValue, // the presence of one static block does not negate that the following element looks like a dynamic placeholder
|
||||
fooBarBlockDynamicPlaceholder, // the presence of this disables some of our checks
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"key": cty.ListVal([]cty.Value{
|
||||
fooBlockValue,
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("hello"),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("world"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
nil, // as above, the presence of a block whose attrs are all unknown indicates dynamic block expansion, so our usual count checks don't apply
|
||||
nil, // an unknown block is allowed to expand into multiple, because that's how dynamic blocks behave when for_each is unknown
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
|
@ -1195,14 +1159,11 @@ func TestAssertObjectCompatible(t *testing.T) {
|
|||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"block": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.UnknownVal(cty.String),
|
||||
"block": cty.UnknownVal(cty.Set(
|
||||
cty.Object(map[string]cty.Type{
|
||||
"foo": cty.String,
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
)),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"block": cty.SetVal([]cty.Value{
|
||||
|
@ -1221,47 +1182,6 @@ func TestAssertObjectCompatible(t *testing.T) {
|
|||
// indicates this may be a dynamic block, and the length is unknown
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"block": {
|
||||
Nesting: configschema.NestingSet,
|
||||
Block: schemaWithFooBar,
|
||||
},
|
||||
},
|
||||
},
|
||||
// The legacy SDK cannot handle missing strings in sets, and will
|
||||
// insert empty strings to the planned value. Empty strings should
|
||||
// be handled as nulls, and this object should represent a possible
|
||||
// dynamic block.
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"block": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.UnknownVal(cty.String),
|
||||
"bar": cty.StringVal(""),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"block": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("hello"),
|
||||
"bar": cty.StringVal(""),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("world"),
|
||||
"bar": cty.StringVal(""),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("nope"),
|
||||
"bar": cty.StringVal(""),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
// there is no error here, because the presence of unknowns
|
||||
// indicates this may be a dynamic block, and the length is unknown
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
|
@ -1335,11 +1255,7 @@ func TestAssertObjectCompatible(t *testing.T) {
|
|||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"block": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
"block": cty.UnknownVal(cty.Set(fooBlockValue.Type())),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"block": cty.SetVal([]cty.Value{
|
||||
|
@ -1364,11 +1280,7 @@ func TestAssertObjectCompatible(t *testing.T) {
|
|||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"block2": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
"block2": cty.UnknownVal(cty.Set(fooBlockValue.Type())),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"block2": cty.SetValEmpty(cty.Object(map[string]cty.Type{
|
||||
|
@ -1406,37 +1318,6 @@ func TestAssertObjectCompatible(t *testing.T) {
|
|||
}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"block": {
|
||||
Nesting: configschema.NestingSet,
|
||||
Block: schemaWithFooBar,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"block": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.UnknownVal(cty.String),
|
||||
"bar": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"block": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("a"),
|
||||
"bar": cty.StringVal(""),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("b"),
|
||||
"bar": cty.StringVal(""),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
|
|
|
@ -93,6 +93,12 @@ func proposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value
|
|||
}
|
||||
|
||||
func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.Value) cty.Value {
|
||||
// The only time we should encounter an entirely unknown block is from the
|
||||
// use of dynamic with an unknown for_each expression.
|
||||
if !config.IsKnown() {
|
||||
return config
|
||||
}
|
||||
|
||||
var newV cty.Value
|
||||
|
||||
switch schema.Nesting {
|
||||
|
@ -103,7 +109,7 @@ func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.
|
|||
case configschema.NestingList:
|
||||
// Nested blocks are correlated by index.
|
||||
configVLen := 0
|
||||
if config.IsKnown() && !config.IsNull() {
|
||||
if !config.IsNull() {
|
||||
configVLen = config.LengthInt()
|
||||
}
|
||||
if configVLen > 0 {
|
||||
|
|
|
@ -16,7 +16,11 @@ func FormatValue(v cty.Value, indent int) string {
|
|||
if !v.IsKnown() {
|
||||
return "(known after apply)"
|
||||
}
|
||||
if v.IsMarked() {
|
||||
if v.Type().Equals(cty.String) && v.HasMark("raw") {
|
||||
raw, _ := v.Unmark()
|
||||
return raw.AsString()
|
||||
}
|
||||
if v.HasMark("sensitive") {
|
||||
return "(sensitive)"
|
||||
}
|
||||
if v.IsNull() {
|
||||
|
|
|
@ -633,7 +633,7 @@ func (c *Context) destroyPlan() (*plans.Plan, tfdiags.Diagnostics) {
|
|||
}
|
||||
|
||||
// Do the walk
|
||||
walker, walkDiags := c.walk(graph, walkPlan)
|
||||
walker, walkDiags := c.walk(graph, walkPlanDestroy)
|
||||
diags = diags.Append(walker.NonFatalDiagnostics)
|
||||
diags = diags.Append(walkDiags)
|
||||
if walkDiags.HasErrors() {
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
"github.com/hashicorp/terraform/configs/configschema"
|
||||
"github.com/hashicorp/terraform/providers"
|
||||
"github.com/hashicorp/terraform/states"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
@ -367,3 +368,80 @@ resource "aws_instance" "bin" {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
func TestContext2Apply_additionalSensitiveFromState(t *testing.T) {
|
||||
// Ensure we're not trying to double-mark values decoded from state
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
variable "secret" {
|
||||
sensitive = true
|
||||
default = ["secret"]
|
||||
}
|
||||
|
||||
resource "test_resource" "a" {
|
||||
sensitive_attr = var.secret
|
||||
}
|
||||
|
||||
resource "test_resource" "b" {
|
||||
value = test_resource.a.id
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
p := new(MockProvider)
|
||||
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||
ResourceTypes: map[string]*configschema.Block{
|
||||
"test_resource": {
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"value": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
"sensitive_attr": {
|
||||
Type: cty.List(cty.String),
|
||||
Optional: true,
|
||||
Sensitive: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
state := states.BuildState(func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(
|
||||
mustResourceInstanceAddr(`test_resource.a`),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{"id":"a","sensitive_attr":["secret"]}`),
|
||||
AttrSensitivePaths: []cty.PathValueMarks{
|
||||
{
|
||||
Path: cty.GetAttrPath("sensitive_attr"),
|
||||
Marks: cty.NewValueMarks("sensitive"),
|
||||
},
|
||||
},
|
||||
Status: states.ObjectReady,
|
||||
}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
|
||||
)
|
||||
})
|
||||
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Config: m,
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
State: state,
|
||||
})
|
||||
|
||||
_, diags := ctx.Plan()
|
||||
if diags.HasErrors() {
|
||||
t.Fatal(diags.ErrWithWarnings())
|
||||
}
|
||||
|
||||
_, diags = ctx.Apply()
|
||||
if diags.HasErrors() {
|
||||
t.Fatal(diags.ErrWithWarnings())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12467,9 +12467,10 @@ func TestContext2Apply_errorRestoreStatus(t *testing.T) {
|
|||
|
||||
state := states.BuildState(func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectTainted,
|
||||
AttrsJSON: []byte(`{"test_string":"foo"}`),
|
||||
Private: []byte("private"),
|
||||
Status: states.ObjectTainted,
|
||||
AttrsJSON: []byte(`{"test_string":"foo"}`),
|
||||
Private: []byte("private"),
|
||||
Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.b")},
|
||||
}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
|
||||
})
|
||||
|
||||
|
@ -12506,6 +12507,10 @@ func TestContext2Apply_errorRestoreStatus(t *testing.T) {
|
|||
t.Fatal("resource should still be tainted in the state")
|
||||
}
|
||||
|
||||
if len(res.Current.Dependencies) != 1 || !res.Current.Dependencies[0].Equal(mustConfigResourceAddr("test_object.b")) {
|
||||
t.Fatalf("incorrect dependencies, got %q", res.Current.Dependencies)
|
||||
}
|
||||
|
||||
if string(res.Current.Private) != "private" {
|
||||
t.Fatalf("incorrect private data, got %q", res.Current.Private)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package terraform
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
|
@ -377,3 +378,140 @@ resource "test_object" "a" {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_unmarkingSensitiveAttributeForOutput(t *testing.T) {
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
resource "test_resource" "foo" {
|
||||
}
|
||||
|
||||
output "result" {
|
||||
value = nonsensitive(test_resource.foo.sensitive_attr)
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
p := new(MockProvider)
|
||||
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||
ResourceTypes: map[string]*configschema.Block{
|
||||
"test_resource": {
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"sensitive_attr": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
Sensitive: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
|
||||
return providers.PlanResourceChangeResponse{
|
||||
PlannedState: cty.UnknownVal(cty.Object(map[string]cty.Type{
|
||||
"id": cty.String,
|
||||
"sensitive_attr": cty.String,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
state := states.NewState()
|
||||
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Config: m,
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
State: state,
|
||||
})
|
||||
|
||||
plan, diags := ctx.Plan()
|
||||
if diags.HasErrors() {
|
||||
t.Fatal(diags.ErrWithWarnings())
|
||||
}
|
||||
|
||||
for _, res := range plan.Changes.Resources {
|
||||
if res.Action != plans.Create {
|
||||
t.Fatalf("expected create, got: %q %s", res.Addr, res.Action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_destroyNoProviderConfig(t *testing.T) {
|
||||
// providers do not need to be configured during a destroy plan
|
||||
p := simpleMockProvider()
|
||||
p.ValidateProviderConfigFn = func(req providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) {
|
||||
v := req.Config.GetAttr("test_string")
|
||||
if v.IsNull() || !v.IsKnown() || v.AsString() != "ok" {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(errors.New("invalid provider configuration"))
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
locals {
|
||||
value = "ok"
|
||||
}
|
||||
|
||||
provider "test" {
|
||||
test_string = local.value
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
addr := mustResourceInstanceAddr("test_object.a")
|
||||
state := states.BuildState(func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{"test_string":"foo"}`),
|
||||
Status: states.ObjectReady,
|
||||
}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
|
||||
})
|
||||
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Config: m,
|
||||
State: state,
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
Destroy: true,
|
||||
})
|
||||
|
||||
_, diags := ctx.Plan()
|
||||
if diags.HasErrors() {
|
||||
t.Fatal(diags.Err())
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_invalidSensitiveModuleOutput(t *testing.T) {
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"child/main.tf": `
|
||||
output "out" {
|
||||
value = sensitive("xyz")
|
||||
}`,
|
||||
"main.tf": `
|
||||
module "child" {
|
||||
source = "./child"
|
||||
}
|
||||
|
||||
output "root" {
|
||||
value = module.child.out
|
||||
}`,
|
||||
})
|
||||
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Config: m,
|
||||
})
|
||||
|
||||
_, diags := ctx.Plan()
|
||||
if !diags.HasErrors() {
|
||||
t.Fatal("succeeded; want errors")
|
||||
}
|
||||
if got, want := diags.Err().Error(), "Output refers to sensitive values"; !strings.Contains(got, want) {
|
||||
t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1379,7 +1379,7 @@ resource "aws_instance" "foo" {
|
|||
}
|
||||
}
|
||||
|
||||
func TestContext2Validate_invalidSensitiveModuleOutput(t *testing.T) {
|
||||
func TestContext2Validate_sensitiveRootModuleOutput(t *testing.T) {
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"child/main.tf": `
|
||||
variable "foo" {
|
||||
|
@ -1395,27 +1395,19 @@ module "child" {
|
|||
source = "./child"
|
||||
}
|
||||
|
||||
resource "aws_instance" "foo" {
|
||||
foo = module.child.out
|
||||
output "root" {
|
||||
value = module.child.out
|
||||
sensitive = true
|
||||
}`,
|
||||
})
|
||||
|
||||
p := testProvider("aws")
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Config: m,
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
|
||||
diags := ctx.Validate()
|
||||
if !diags.HasErrors() {
|
||||
t.Fatal("succeeded; want errors")
|
||||
}
|
||||
// Should get this error:
|
||||
// Output refers to sensitive values: Expressions used in outputs can only refer to sensitive values if the sensitive attribute is true.
|
||||
if got, want := diags.Err().Error(), "Output refers to sensitive values"; !strings.Contains(got, want) {
|
||||
t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want)
|
||||
if diags.HasErrors() {
|
||||
t.Fatal(diags.Err())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2008,3 +2000,95 @@ func TestContext2Validate_sensitiveProvisionerConfig(t *testing.T) {
|
|||
t.Fatal("ValidateProvisionerConfig not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_validateMinMaxDynamicBlock(t *testing.T) {
|
||||
p := new(MockProvider)
|
||||
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||
ResourceTypes: map[string]*configschema.Block{
|
||||
"test_instance": {
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"things": {
|
||||
Type: cty.List(cty.String),
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"foo": {
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bar": {Type: cty.String, Optional: true},
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingList,
|
||||
MinItems: 2,
|
||||
MaxItems: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
resource "test_instance" "a" {
|
||||
// MinItems 2
|
||||
foo {
|
||||
bar = "a"
|
||||
}
|
||||
foo {
|
||||
bar = "b"
|
||||
}
|
||||
}
|
||||
|
||||
resource "test_instance" "b" {
|
||||
// one dymamic block can satisfy MinItems of 2
|
||||
dynamic "foo" {
|
||||
for_each = test_instance.a.things
|
||||
content {
|
||||
bar = foo.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "test_instance" "c" {
|
||||
// we may have more than MaxItems dynamic blocks when they are unknown
|
||||
foo {
|
||||
bar = "b"
|
||||
}
|
||||
dynamic "foo" {
|
||||
for_each = test_instance.a.things
|
||||
content {
|
||||
bar = foo.value
|
||||
}
|
||||
}
|
||||
dynamic "foo" {
|
||||
for_each = test_instance.a.things
|
||||
content {
|
||||
bar = "${foo.value}-2"
|
||||
}
|
||||
}
|
||||
dynamic "foo" {
|
||||
for_each = test_instance.b.things
|
||||
content {
|
||||
bar = foo.value
|
||||
}
|
||||
}
|
||||
}
|
||||
`})
|
||||
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Config: m,
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
|
||||
diags := ctx.Validate()
|
||||
if diags.HasErrors() {
|
||||
t.Fatal(diags.ErrWithWarnings())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -459,7 +459,7 @@ func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.Sourc
|
|||
continue
|
||||
}
|
||||
|
||||
instance[cfg.Name] = change.After
|
||||
instance[cfg.Name] = change.After.MarkWithPaths(changeSrc.AfterValMarks)
|
||||
|
||||
if change.Sensitive && !change.After.HasMark("sensitive") {
|
||||
instance[cfg.Name] = change.After.Mark("sensitive")
|
||||
|
@ -782,9 +782,15 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc
|
|||
|
||||
val := ios.Value
|
||||
|
||||
// If our schema contains sensitive values, mark those as sensitive
|
||||
// If our schema contains sensitive values, mark those as sensitive.
|
||||
// Since decoding the instance object can also apply sensitivity marks,
|
||||
// we must remove and combine those before remarking to avoid a double-
|
||||
// mark error.
|
||||
if schema.ContainsSensitive() {
|
||||
val = markProviderSensitiveAttributes(schema, val)
|
||||
var marks []cty.PathValueMarks
|
||||
val, marks = val.UnmarkDeepWithPaths()
|
||||
marks = append(marks, getValMarks(schema, val, nil)...)
|
||||
val = val.MarkWithPaths(marks)
|
||||
}
|
||||
instances[key] = val
|
||||
}
|
||||
|
@ -954,12 +960,6 @@ func moduleDisplayAddr(addr addrs.ModuleInstance) string {
|
|||
}
|
||||
}
|
||||
|
||||
// markProviderSensitiveAttributes returns an updated value
|
||||
// where attributes that are Sensitive are marked
|
||||
func markProviderSensitiveAttributes(schema *configschema.Block, val cty.Value) cty.Value {
|
||||
return val.MarkWithPaths(getValMarks(schema, val, nil))
|
||||
}
|
||||
|
||||
func getValMarks(schema *configschema.Block, val cty.Value, path cty.Path) []cty.PathValueMarks {
|
||||
var pvm []cty.PathValueMarks
|
||||
for name, attrS := range schema.Attributes {
|
||||
|
|
|
@ -563,7 +563,7 @@ func evaluatorForModule(stateSync *states.SyncState, changesSync *plans.ChangesS
|
|||
}
|
||||
}
|
||||
|
||||
func TestMarkProviderSensitive(t *testing.T) {
|
||||
func TestGetValMarks(t *testing.T) {
|
||||
schema := &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"unsensitive": {
|
||||
|
@ -647,7 +647,7 @@ func TestMarkProviderSensitive(t *testing.T) {
|
|||
},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%#v", tc.given), func(t *testing.T) {
|
||||
got := markProviderSensitiveAttributes(schema, tc.given)
|
||||
got := tc.given.MarkWithPaths(getValMarks(schema, tc.given, nil))
|
||||
if !got.RawEquals(tc.expect) {
|
||||
t.Fatalf("\nexpected: %#v\ngot: %#v\n", tc.expect, got)
|
||||
}
|
||||
|
|
|
@ -275,16 +275,24 @@ func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) (diags
|
|||
// depends_on expressions here too
|
||||
diags = diags.Append(validateDependsOn(ctx, n.Config.DependsOn))
|
||||
|
||||
// Ensure that non-sensitive outputs don't include sensitive values
|
||||
// For root module outputs in particular, an output value must be
|
||||
// statically declared as sensitive in order to dynamically return
|
||||
// a sensitive result, to help avoid accidental exposure in the state
|
||||
// of a sensitive value that the user doesn't want to include there.
|
||||
_, marks := val.UnmarkDeep()
|
||||
_, hasSensitive := marks["sensitive"]
|
||||
if !n.Config.Sensitive && hasSensitive {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Output refers to sensitive values",
|
||||
Detail: "Expressions used in outputs can only refer to sensitive values if the sensitive attribute is true.",
|
||||
Subject: n.Config.DeclRange.Ptr(),
|
||||
})
|
||||
if n.Addr.Module.IsRoot() {
|
||||
if !n.Config.Sensitive && hasSensitive {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Output refers to sensitive values",
|
||||
Detail: `To reduce the risk of accidentally exporting sensitive data that was intended to be only internal, Terraform requires that any root module output containing sensitive data be explicitly marked as sensitive, to confirm your intent.
|
||||
|
||||
If you do intend to export this data, annotate the output value as sensitive by adding the following argument:
|
||||
sensitive = true`,
|
||||
Subject: n.Config.DeclRange.Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -454,7 +462,7 @@ func (n *NodeApplyableOutput) setValue(state *states.SyncState, changes *plans.C
|
|||
sensitiveChange := sensitiveBefore || n.Config.Sensitive
|
||||
|
||||
// strip any marks here just to be sure we don't panic on the True comparison
|
||||
val, _ = val.UnmarkDeep()
|
||||
unmarkedVal, _ := val.UnmarkDeep()
|
||||
|
||||
action := plans.Update
|
||||
switch {
|
||||
|
@ -468,7 +476,7 @@ func (n *NodeApplyableOutput) setValue(state *states.SyncState, changes *plans.C
|
|||
action = plans.Create
|
||||
|
||||
case val.IsWhollyKnown() &&
|
||||
val.Equals(before).True() &&
|
||||
unmarkedVal.Equals(before).True() &&
|
||||
n.Config.Sensitive == sensitiveBefore:
|
||||
// Sensitivity must also match to be a NoOp.
|
||||
// Theoretically marks may not match here, but sensitivity is the
|
||||
|
|
|
@ -2102,6 +2102,13 @@ func (n *NodeAbstractResourceInstance) apply(
|
|||
Private: resp.Private,
|
||||
CreateBeforeDestroy: createBeforeDestroy,
|
||||
}
|
||||
|
||||
// if the resource was being deleted, the dependencies are not going to
|
||||
// be recalculated and we need to restore those as well.
|
||||
if change.Action == plans.Delete {
|
||||
newState.Dependencies = state.Dependencies
|
||||
}
|
||||
|
||||
return newState, diags
|
||||
|
||||
case !newVal.IsNull():
|
||||
|
|
|
@ -282,10 +282,14 @@ the operating system where you are running Terraform:
|
|||
`~/.local/share/terraform/plugins`,
|
||||
`/usr/local/share/terraform/plugins`, and `/usr/share/terraform/plugins`.
|
||||
|
||||
Terraform will create an implied `filesystem_mirror` method block for each of
|
||||
the directories indicated above that exists when Terraform starts up.
|
||||
In addition, if a `terraform.d/plugins` directory exists in the current working
|
||||
directory, it will be added as a filesystem mirror.
|
||||
If a `terraform.d/plugins` directory exists in the current working directory
|
||||
then Terraform will also include that directory, regardless of your operating
|
||||
system.
|
||||
|
||||
Terraform will check each of the paths above to see if it exists, and if so
|
||||
treat it as a filesystem mirror. The directory structure inside each one must
|
||||
therefore match one of the two structures described for `filesystem_mirror`
|
||||
blocks in [Explicit Installation Method Configuration](#explicit-installation-method-configuration).
|
||||
|
||||
In addition to the zero or more implied `filesystem_mirror` blocks, Terraform
|
||||
also creates an implied `direct` block. Terraform will scan all of the
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
layout: "language"
|
||||
page_title: "type - Functions - Configuration Language"
|
||||
sidebar_current: "docs-funcs-conversion-type"
|
||||
description: |-
|
||||
The type function returns the type of a given value.
|
||||
---
|
||||
|
||||
# `type` Function
|
||||
|
||||
-> **Note:** This function is available only in Terraform 1.0 and later.
|
||||
|
||||
`type` retuns the type of a given value.
|
||||
|
||||
Sometimes a Terraform configuration can result in confusing errors regarding
|
||||
inconsistent types. This function displays terraform's evaluation of a given
|
||||
value's type, which is useful in understanding this error message.
|
||||
|
||||
This is a special function which is only available in the `terraform console` command.
|
||||
|
||||
## Examples
|
||||
|
||||
Here we have a conditional `output` which prints either the value of `var.list` or a local named `default_list`:
|
||||
|
||||
```hcl
|
||||
variable "list" {
|
||||
default = []
|
||||
}
|
||||
|
||||
locals {
|
||||
default_list = [
|
||||
{
|
||||
foo = "bar"
|
||||
map = { bleep = "bloop" }
|
||||
},
|
||||
{
|
||||
beep = "boop"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
output "list" {
|
||||
value = var.list != [] ? var.list : local.default_list
|
||||
}
|
||||
```
|
||||
|
||||
Applying this configuration results in the following error:
|
||||
|
||||
```
|
||||
Error: Inconsistent conditional result types
|
||||
|
||||
on main.tf line 18, in output "list":
|
||||
18: value = var.list != [] ? var.list : local.default_list
|
||||
|----------------
|
||||
| local.default_list is tuple with 2 elements
|
||||
| var.list is empty tuple
|
||||
|
||||
The true and false result expressions must have consistent types. The given
|
||||
expressions are tuple and tuple, respectively.
|
||||
```
|
||||
|
||||
While this error message does include some type information, it can be helpful
|
||||
to inspect the exact type that Terraform has determined for each given input.
|
||||
Examining both `var.list` and `local.default_list` using the `type` function
|
||||
provides more context for the error message:
|
||||
|
||||
```
|
||||
> type(var.list)
|
||||
tuple
|
||||
> type(local.default_list)
|
||||
tuple([
|
||||
object({
|
||||
foo: string,
|
||||
map: object({
|
||||
bleep: string,
|
||||
}),
|
||||
}),
|
||||
object({
|
||||
beep: string,
|
||||
}),
|
||||
])
|
||||
```
|
|
@ -8,6 +8,9 @@ description: |-
|
|||
|
||||
# Debugging Terraform
|
||||
|
||||
> **Hands-on:** Try the [Create Dynamic Expressions](https://learn.hashicorp.com/tutorials/terraform/troubleshooting-workflow#bug-reporting-best-practices?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn.
|
||||
|
||||
|
||||
Terraform has detailed logs which can be enabled by setting the `TF_LOG` environment variable to any value. This will cause detailed logs to appear on stderr.
|
||||
|
||||
You can set `TF_LOG` to one of the log levels `TRACE`, `DEBUG`, `INFO`, `WARN` or `ERROR` to change the verbosity of the logs.
|
||||
|
|
|
@ -277,6 +277,36 @@ Note that unlike `count`, splat expressions are _not_ directly applicable to res
|
|||
|
||||
* `values(aws_instance.example)[*].id`
|
||||
|
||||
### Sensitive Resource Attributes
|
||||
|
||||
When defining the schema for a resource type, a provider developer can mark
|
||||
certain attributes as _sensitive_, in which case Terraform will show a
|
||||
placeholder marker `(sensitive)` instead of the actual value when rendering
|
||||
a plan involving that attribute.
|
||||
|
||||
A provider attribute marked as sensitive behaves similarly to an
|
||||
[an input variable declared as sensitive](/docs/language/values/variables.html#suppressing-values-in-cli-output),
|
||||
where Terraform will hide the value in the plan and apply messages and will
|
||||
also hide any other values you derive from it as sensitive.
|
||||
However, there are some limitations to that behavior as described in
|
||||
[Cases where Terraform may disclose a sensitive variable](/docs/language/values/variables.html#cases-where-terraform-may-disclose-a-sensitive-variable).
|
||||
|
||||
If you use a sensitive value from a resource attribute as part of an
|
||||
[output value](/docs/language/values/outputs.html) then Terraform will require
|
||||
you to also mark the output value itself as sensitive, to confirm that you
|
||||
intended to export it.
|
||||
|
||||
Terraform will still record sensitive values in the [state](/docs/language/state/index.html),
|
||||
and so anyone who can access the state data will have access to the sensitive
|
||||
values in cleartext. For more information, see
|
||||
[_Sensitive Data in State_](/docs/language/state/sensitive-data.html).
|
||||
|
||||
-> **Note:** Treating values derived from a sensitive resource attribute as
|
||||
sensitive themselves was introduced in Terraform v0.15. Earlier versions of
|
||||
Terraform will obscure the direct value of a sensitive resource attribute,
|
||||
but will _not_ automatically obscure other values derived from sensitive
|
||||
resource attributes.
|
||||
|
||||
### Values Not Yet Known
|
||||
|
||||
When Terraform is planning a set of changes that will apply your configuration,
|
||||
|
@ -317,44 +347,3 @@ effect:
|
|||
until the apply phase, causing the apply to fail.
|
||||
|
||||
Unknown values appear in the `terraform plan` output as `(not yet known)`.
|
||||
|
||||
### Sensitive Resource Attributes
|
||||
|
||||
When defining the schema for a resource type, a provider developer can mark
|
||||
certain attributes as _sensitive_, in which case Terraform will show a
|
||||
placeholder marker `(sensitive)` instead of the actual value when rendering
|
||||
a plan involving that attribute.
|
||||
|
||||
The treatment of these particular sensitive values is currently different than
|
||||
for values in
|
||||
[input variables](/docs/language/values/variables.html)
|
||||
and
|
||||
[output values](/docs/language/values/outputs.html)
|
||||
that have `sensitive = true` set. Sensitive resource attributes will be
|
||||
obscured in the plan when they appear directly, but other values that you
|
||||
_derive_ from a sensitive resource attribute will not themselves be considered
|
||||
sensitive, and so Terraform will include those derived values in its output
|
||||
without redacting them.
|
||||
|
||||
Terraform v0.15.0 and later treats resource attributes that are marked as
|
||||
sensitive (by the provider) in the same way as sensitive input variables and
|
||||
output values, so that Terraform will consider any derived values as sensitive too.
|
||||
|
||||
If you are using Terraform v0.14.x, this feature is considered experimental.
|
||||
You can activate that experiment for your module using the
|
||||
`provider_sensitive_attrs` experiment keyword:
|
||||
|
||||
```hcl
|
||||
terraform {
|
||||
experiments = [provider_sensitive_attrs]
|
||||
}
|
||||
```
|
||||
|
||||
The behavior of this experiment might change even in future patch releases of
|
||||
Terraform, so we don't recommend using this experiment in modules you use
|
||||
to describe production infrastructure.
|
||||
|
||||
If you enable this experiment and you have exported any sensitive resource
|
||||
attributes via your module's output values then you will see an error unless
|
||||
you also mark the output value as `sensitive = true`, confirming your intent
|
||||
to export it.
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
---
|
||||
layout: "language"
|
||||
page_title: "one - Functions - Configuration Language"
|
||||
sidebar_current: "docs-funcs-collection-one"
|
||||
description: |-
|
||||
The 'one' function transforms a list with either zero or one elements into
|
||||
either a null value or the value of the first element.
|
||||
---
|
||||
|
||||
# `one` Function
|
||||
|
||||
-> **Note:** This function is available only in Terraform v0.15 and later.
|
||||
|
||||
`one` takes a list, set, or tuple value with either zero or one elements.
|
||||
If the collection is empty, `one` returns `null`. Otherwise, `one` returns
|
||||
the first element. If there are two or more elements then `one` will return
|
||||
an error.
|
||||
|
||||
This is a specialized function intended for the common situation where a
|
||||
conditional item is represented as either a zero- or one-element list, where
|
||||
a module author wishes to return a single value that might be null instead.
|
||||
|
||||
For example:
|
||||
|
||||
```hcl
|
||||
variable "include_ec2_instance" {
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
resource "aws_instance" "example" {
|
||||
count = var.include_ec2_instance ? 1 : 0
|
||||
|
||||
# (other resource arguments...)
|
||||
}
|
||||
|
||||
output "instance_ip_address" {
|
||||
value = one(aws_instance.example[*].private_ip)
|
||||
}
|
||||
```
|
||||
|
||||
Because the `aws_instance` resource above has the `count` argument set to a
|
||||
conditional that returns either zero or one, the value of
|
||||
`aws_instance.example` is a list of either zero or one elements. The
|
||||
`instance_ip_address` output value uses the `one` function as a concise way
|
||||
to return either the private IP address of a single instance, or `null` if
|
||||
no instances were created.
|
||||
|
||||
## Relationship to the "Splat" Operator
|
||||
|
||||
The Terraform language has a built-in operator `[*]`, known as
|
||||
[the _splat_ operator](../expressions/splat.html), and one if its functions
|
||||
is to translate a primitive value that might be null into a list of either
|
||||
zero or one elements:
|
||||
|
||||
```hcl
|
||||
variable "ec2_instance_type" {
|
||||
description = "The type of instance to create. If set to null, no instance will be created."
|
||||
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
resource "aws_instance" "example" {
|
||||
count = length(var.ec2_instance_type[*])
|
||||
|
||||
instance_type = var.ec2_instance_type
|
||||
# (other resource arguments...)
|
||||
}
|
||||
|
||||
output "instance_ip_address" {
|
||||
value = one(aws_instance.example[*].private_ip)
|
||||
}
|
||||
```
|
||||
|
||||
In this case we can see that the `one` function is, in a sense, the opposite
|
||||
of applying `[*]` to a primitive-typed value. Splat can convert a possibly-null
|
||||
value into a zero-or-one list, and `one` can reverse that to return to a
|
||||
primitive value that might be null.
|
||||
|
||||
## Examples
|
||||
|
||||
```
|
||||
> one([])
|
||||
null
|
||||
> one(["hello"])
|
||||
"hello"
|
||||
> one(["hello", "goodbye"])
|
||||
|
||||
Error: Invalid function argument
|
||||
|
||||
Invalid value for "list" parameter: must be a list, set, or tuple value with
|
||||
either zero or one elements.
|
||||
```
|
||||
|
||||
### Using `one` with sets
|
||||
|
||||
The `one` function can be particularly helpful in situations where you have a
|
||||
set that you know has only zero or one elements. Set values don't support
|
||||
indexing, so it's not valid to write `var.set[0]` to extract the "first"
|
||||
element of a set, but if you know that there's only one item then `one` can
|
||||
isolate and return that single item:
|
||||
|
||||
```
|
||||
> one(toset([]))
|
||||
null
|
||||
> one(toset(["hello"]))
|
||||
"hello"
|
||||
```
|
||||
|
||||
Don't use `one` with sets that might have more than one element. This function
|
||||
will fail in that case:
|
||||
|
||||
```
|
||||
> one(toset(["hello","goodbye"]))
|
||||
|
||||
Error: Invalid function argument
|
||||
|
||||
Invalid value for "list" parameter: must be a list, set, or tuple value with
|
||||
either zero or one elements.
|
||||
```
|
|
@ -20,18 +20,18 @@ Stores the state in a [Kubernetes secret](https://kubernetes.io/docs/concepts/co
|
|||
terraform {
|
||||
backend "kubernetes" {
|
||||
secret_suffix = "state"
|
||||
load_config_file = true
|
||||
config_path = "~/.kube/config"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This assumes the user/service account running terraform has [permissions](https://kubernetes.io/docs/reference/access-authn-authz/authorization/) to read/write secrets in the [namespace](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/) used to store the secret.
|
||||
|
||||
If the `load_config_file` flag is set the backend will attempt to use a [kubeconfig file](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) to gain access to the cluster.
|
||||
If the `config_path` or `config_paths` attribute is set the backend will attempt to use a [kubeconfig file](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) to gain access to the cluster.
|
||||
|
||||
If the `in_cluster_config` flag is set the backend will attempt to use a [service account](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) to access the cluster. This can be used if Terraform is being run from within a pod running in the Kubernetes cluster.
|
||||
|
||||
For most use cases either `in_cluster_config` or `load_config_file` will need to be set to `true`. If both flags are set the configuration from `load_config_file` will be used.
|
||||
For most use cases either `in_cluster_config`, `config_path`, or `config_paths` will need to be set. If all flags are set the configuration at `config_path` will be used.
|
||||
|
||||
Note that for the access credentials we recommend using a [partial configuration](/docs/language/settings/backends/configuration.html#partial-configuration).
|
||||
|
||||
|
@ -56,7 +56,6 @@ The following configuration options are supported:
|
|||
* `labels` - (Optional) Map of additional labels to be applied to the secret and lease.
|
||||
* `namespace` - (Optional) Namespace to store the secret and lease in. Can be sourced from `KUBE_NAMESPACE`.
|
||||
* `in_cluster_config` - (Optional) Used to authenticate to the cluster from inside a pod. Can be sourced from `KUBE_IN_CLUSTER_CONFIG`.
|
||||
* `load_config_file` - (Optional) Use a kubeconfig file to access the cluster. Can be sourced from `KUBE_LOAD_CONFIG_FILE`.
|
||||
* `host` - (Optional) The hostname (in form of URI) of Kubernetes master. Can be sourced from `KUBE_HOST`. Defaults to `https://localhost`.
|
||||
* `username` - (Optional) The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint. Can be sourced from `KUBE_USER`.
|
||||
* `password` - (Optional) The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint. Can be sourced from `KUBE_PASSWORD`.
|
||||
|
@ -64,7 +63,8 @@ The following configuration options are supported:
|
|||
* `client_certificate` - (Optional) PEM-encoded client certificate for TLS authentication. Can be sourced from `KUBE_CLIENT_CERT_DATA`.
|
||||
* `client_key` - (Optional) PEM-encoded client certificate key for TLS authentication. Can be sourced from `KUBE_CLIENT_KEY_DATA`.
|
||||
* `cluster_ca_certificate` - (Optional) PEM-encoded root certificates bundle for TLS authentication. Can be sourced from `KUBE_CLUSTER_CA_CERT_DATA`.
|
||||
* `config_path` - (Optional) Path to the kube config file. Can be sourced from `KUBE_CONFIG` or `KUBECONFIG`. Defaults to `~/.kube/config`.
|
||||
* `config_path` - (Optional) Path to the kube config file. Can be sourced from `KUBE_CONFIG_PATH`.
|
||||
* `config_paths` - (Optional) List of paths to kube config files. Can be sourced from `KUBE_CONFIG_PATHS`.
|
||||
* `config_context` - (Optional) Context to choose from the config file. Can be sourced from `KUBE_CTX`.
|
||||
* `config_context_auth_info` - (Optional) Authentication info context of the kube config (name of the kubeconfig user, `--user` flag in `kubectl`). Can be sourced from `KUBE_CTX_AUTH_INFO`.
|
||||
* `config_context_cluster` - (Optional) Cluster context of the kube config (name of the kubeconfig cluster, `--cluster` flag in `kubectl`). Can be sourced from `KUBE_CTX_CLUSTER`.
|
||||
|
|
|
@ -102,9 +102,10 @@ output "db_password" {
|
|||
}
|
||||
```
|
||||
|
||||
Setting an output value as sensitive prevents Terraform from showing its value
|
||||
in `plan` and `apply`. In the following scenario, our root module has an output declared as sensitive
|
||||
and a module call with a sensitive output, which we then use in a resource attribute.
|
||||
Terraform will hide values marked as sensitive in the messages from
|
||||
`terraform plan` and `terraform apply`. In the following scenario, our root
|
||||
module has an output declared as sensitive and a module call with a
|
||||
sensitive output, which we then use in a resource attribute.
|
||||
|
||||
```hcl
|
||||
# main.tf
|
||||
|
@ -130,11 +131,9 @@ output "a" {
|
|||
}
|
||||
```
|
||||
|
||||
When we run a `plan` or `apply`, the sensitive value is redacted from output:
|
||||
When we run a plan or apply, the sensitive value is redacted from output:
|
||||
|
||||
```
|
||||
# CLI output
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# test_instance.x will be created
|
||||
|
@ -148,11 +147,15 @@ Changes to Outputs:
|
|||
+ out = (sensitive value)
|
||||
```
|
||||
|
||||
-> **Note:** In Terraform versions prior to Terraform 0.14, setting an output value in the root module as sensitive would prevent Terraform from showing its value in the list of outputs at the end of `terraform apply`. However, the value could still display in the CLI output for other reasons, like if the value is referenced in an expression for a resource argument.
|
||||
-> **Note:** In Terraform versions prior to Terraform 0.14, setting an output
|
||||
value in the root module as sensitive would prevent Terraform from showing its
|
||||
value in the list of outputs at the end of `terraform apply`. However, the
|
||||
value could still display in the CLI output for other reasons, like if the
|
||||
value is referenced in an expression for a resource argument.
|
||||
|
||||
Sensitive output values are still recorded in the
|
||||
[state](/docs/language/state/index.html), and so will be visible to anyone who is able
|
||||
to access the state data. For more information, see
|
||||
Terraform will still record sensitive values in the [state](/docs/language/state/index.html),
|
||||
and so anyone who can access the state data will have access to the sensitive
|
||||
values in cleartext. For more information, see
|
||||
[_Sensitive Data in State_](/docs/language/state/sensitive-data.html).
|
||||
|
||||
<a id="depends_on"></a>
|
||||
|
|
|
@ -211,13 +211,16 @@ using a sentence structure similar to the above examples.
|
|||
|
||||
> **Hands-on:** Try the [Protect Sensitive Input Variables](https://learn.hashicorp.com/tutorials/terraform/sensitive-variables?in=terraform/configuration-language&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn.
|
||||
|
||||
Setting a variable as `sensitive` prevents Terraform from showing its value in the `plan` or `apply` output, when that variable is used within a configuration.
|
||||
Setting a variable as `sensitive` prevents Terraform from showing its value in
|
||||
the `plan` or `apply` output, when you use that variable elsewhere in your
|
||||
configuration.
|
||||
|
||||
Sensitive values are still recorded in the [state](/docs/language/state/index.html), and so will be visible to anyone who is able to access the state data. For more information, see [_Sensitive Data in State_](/docs/language/state/sensitive-data.html).
|
||||
Terraform will still record sensitive values in the [state](/docs/language/state/index.html),
|
||||
and so anyone who can access the state data will have access to the sensitive
|
||||
values in cleartext. For more information, see
|
||||
[_Sensitive Data in State_](/docs/language/state/sensitive-data.html).
|
||||
|
||||
A provider can define [an attribute as sensitive](/docs/extend/best-practices/sensitive-state.html#using-the-sensitive-flag), which prevents the value of that attribute from being displayed in logs or regular output. The `sensitive` argument on variables allows users to replicate this behavior for values in their configuration, by defining a variable as `sensitive`.
|
||||
|
||||
Define a variable as sensitive by setting the `sensitive` argument to `true`:
|
||||
Declare a variable as sensitive by setting the `sensitive` argument to `true`:
|
||||
|
||||
```
|
||||
variable "user_information" {
|
||||
|
@ -234,7 +237,9 @@ resource "some_resource" "a" {
|
|||
}
|
||||
```
|
||||
|
||||
Using this variable throughout your configuration will obfuscate the value from display in `plan` or `apply` output:
|
||||
Any expressions whose result depends on the sensitive variable will be treated
|
||||
as sensitive themselves, and so in the above example the two arguments of
|
||||
`resource "some_resource" "a"` will also be hidden in the plan output:
|
||||
|
||||
```
|
||||
Terraform will perform the following actions:
|
||||
|
@ -248,22 +253,12 @@ Terraform will perform the following actions:
|
|||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
```
|
||||
|
||||
In some cases where a sensitive variable is used in a nested block, the whole block can be redacted. This happens with resources which can have multiple blocks of the same type, where the values must be unique. This looks like:
|
||||
In some cases where you use a sensitive variable inside a nested block, Terraform
|
||||
may treat the entire block as redacted. This happens for resource types where
|
||||
all of the blocks of a particular type are required to be unique, and so
|
||||
disclosing the content of one block might imply the content of a sibling block.
|
||||
|
||||
```
|
||||
# main.tf
|
||||
|
||||
resource "some_resource" "a" {
|
||||
nested_block {
|
||||
user_information = var.user_information # a sensitive variable
|
||||
other_information = "not sensitive data"
|
||||
}
|
||||
}
|
||||
|
||||
# CLI output
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# some_resource.a will be updated in-place
|
||||
~ resource "some_resource" "a" {
|
||||
~ nested_block {
|
||||
|
@ -271,9 +266,19 @@ Terraform will perform the following actions:
|
|||
# so its contents will not be displayed.
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
A provider can also
|
||||
[declare an attribute as sensitive](/docs/extend/best-practices/sensitive-state.html#using-the-sensitive-flag),
|
||||
which will cause Terraform to hide it from regular output regardless of how
|
||||
you assign it a value. For more information, see
|
||||
[Sensitive Resource Attributes](/docs/language/expressions/references.html#sensitive-resource-attributes).
|
||||
|
||||
If you use a sensitive value from as part of an
|
||||
[output value](/docs/language/values/outputs.html) then Terraform will require
|
||||
you to also mark the output value itself as sensitive, to confirm that you
|
||||
intended to export it.
|
||||
|
||||
#### Cases where Terraform may disclose a sensitive variable
|
||||
|
||||
A `sensitive` variable is a configuration-centered concept, and values are sent to providers without any obfuscation. A provider error could disclose a value if that value is included in the error message. For example, a provider might return the following error even if "foo" is a sensitive value: `"Invalid value 'foo' for field"`
|
||||
|
|
|
@ -523,6 +523,10 @@
|
|||
<a href="/docs/language/functions/merge.html">merge</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/docs/language/functions/one.html">one</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/docs/language/functions/range.html">range</a>
|
||||
</li>
|
||||
|
@ -967,6 +971,10 @@
|
|||
<a href="/upgrade-guides/index.html">Overview</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/upgrade-guides/0-15.html">Upgrading to v0.15</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/upgrade-guides/0-14.html">Upgrading to v0.14</a>
|
||||
</li>
|
||||
|
|
|
@ -46,11 +46,134 @@ may be able to reproduce it and offer advice.
|
|||
|
||||
Upgrade guide sections:
|
||||
|
||||
* [Sensitive Output Values](#sensitive-output-values)
|
||||
* [Legacy Configuration Language Features](#legacy-configuration-language-features)
|
||||
* [Alternative (Aliased) Provider Configurations Within Modules](#alternative-provider-configurations-within-modules)
|
||||
* [Commands Accepting a Configuration Directory Argument](#commands-accepting-a-configuration-directory-argument)
|
||||
* [Microsoft Windows Terminal Support](#microsoft-windows-terminal-support)
|
||||
* [Other Minor Command Line Behavior Changes](#other-minor-command-line-behavior-changes)
|
||||
* [Azure Backend `arm_`-prefixed Arguments](#azure-backend-removed-arguments)
|
||||
|
||||
## Sensitive Output Values
|
||||
|
||||
Terraform v0.14 previously introduced the ability for Terraform to track and
|
||||
propagate the "sensitivity" of values through expressions that include
|
||||
references to sensitive input variables and output values. For example:
|
||||
|
||||
```hcl
|
||||
variable "example" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
resource "example" "example" {
|
||||
# The following value is also treated as sensitive, because it's derived
|
||||
# from var.example.
|
||||
name = "foo-${var.example}"
|
||||
}
|
||||
```
|
||||
|
||||
As part of that feature, Terraform also began requiring you to mark an output
|
||||
value as sensitive if its definition includes any sensitive values itself:
|
||||
|
||||
```hcl
|
||||
output "example" {
|
||||
value = "foo-${var.example}"
|
||||
|
||||
# Must mark this output value as sensitive, because it's derived from
|
||||
# var.example that is declared as sensitive.
|
||||
sensitive = true
|
||||
}
|
||||
```
|
||||
|
||||
Terraform v0.15 extends this mechanism to also work for values derived from
|
||||
resource attributes that the provider has declared as being sensitive.
|
||||
Provider developers will typically mark an attribute as sensitive if the
|
||||
remote system has documented the corresponding field as being sensitive, such
|
||||
as if the attribute always contains a password or a private key.
|
||||
|
||||
As a result of that, after upgrading to Terraform v0.15 you may find that
|
||||
Terraform now reports some of your output values as invalid, if they were
|
||||
derived from sensitive attributes without also being marked as sensitive:
|
||||
|
||||
```
|
||||
╷
|
||||
│ Error: Output refers to sensitive values
|
||||
│
|
||||
│ on sensitive-resource-attr.tf line 5:
|
||||
│ 5: output "private_key" {
|
||||
│
|
||||
│ Expressions used in outputs can only refer to sensitive values if the
|
||||
│ sensitive attribute is true.
|
||||
╵
|
||||
```
|
||||
|
||||
If you were intentionally exporting a sensitive value, you can address the
|
||||
error by adding an explicit declaration `sensitive = true` to the output
|
||||
value declaration:
|
||||
|
||||
```hcl
|
||||
output "private_key" {
|
||||
value = tls_private_key.example.private_key_pem
|
||||
sensitive = true
|
||||
}
|
||||
```
|
||||
|
||||
With that addition, if this output value was a root module output value then
|
||||
Terraform will hide its value in the `terraform plan` and `terraform apply`
|
||||
output:
|
||||
|
||||
```
|
||||
Changes to Outputs:
|
||||
+ private_key = (sensitive value)
|
||||
```
|
||||
|
||||
-> **Note:** The declaration of an output value as sensitive must be made
|
||||
within the module that declares the output, so if you depend on a third-party
|
||||
module that has a sensitive output value that is lacking this declaration then
|
||||
you will need to wait for a new version of that module before you can upgrade
|
||||
to Terraform v0.15.
|
||||
|
||||
The value is only hidden in the main UI, and so the sensitive value
|
||||
will still be recorded in the state. If you declared this output value in order
|
||||
to use it as part of integration with other software, you can still retrieve
|
||||
the cleartext value using commands intended for machine rather than human
|
||||
consumption, such as `terraform output -json` or `terraform output -raw`:
|
||||
|
||||
```shellsession
|
||||
$ terraform output -raw private_key
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAoahsvJ1rIxTIOOmJZ7yErs5eOq/Kv9+5l3h0LbxW78K8//Kb
|
||||
OMU3v8F3h8jp+AB/1zGr5UBYfnYp5ncJm/OTCXLFAHxGibEbRnf1m2A3o0hEaWsw
|
||||
# (etc...)
|
||||
```
|
||||
|
||||
If you consider Terraform's treatment of a sensitive value to be too
|
||||
conservative and you'd like to force Terraform to treat a sensitive value as
|
||||
non-sensitive, you can use
|
||||
[the `nonsensitive` function](/docs/language/functions/nonsensitive.html) to
|
||||
override Terraform's automatic detection:
|
||||
|
||||
```hcl
|
||||
output "private_key" {
|
||||
# WARNING: Terraform will display this result as cleartext
|
||||
value = nonsensitive(tls_private_key.example.private_key_pem)
|
||||
}
|
||||
```
|
||||
|
||||
For more information on the various situations where sensitive values can
|
||||
originate in Terraform, refer to the following sections:
|
||||
|
||||
* [Sensitive Input Variables](/docs/language/values/variables.html#suppressing-values-in-cli-output)
|
||||
* [Sensitive Resource Attributes](/docs/language/expressions/references.html#sensitive-resource-attributes)
|
||||
* [Sensitive Output Values](/docs/language/values/outputs.html#sensitive)
|
||||
|
||||
-> **Note:** The new behavior described in this section was previously
|
||||
available in Terraform v0.14 as the
|
||||
[language experiment](/docs/language/settings/#experimental-language-features)
|
||||
`provider_sensitive_attrs`. That experiment has now concluded, so if you were
|
||||
participating in that experiment then you'll now need to remove the experiment
|
||||
opt-in from your module as part of upgrading to Terraform v0.15.
|
||||
|
||||
## Legacy Configuration Language Features
|
||||
|
||||
|
@ -80,7 +203,7 @@ upgrading to Terraform v0.15:
|
|||
```
|
||||
|
||||
If you need to update a module which was using the `map` function, you
|
||||
can get the same result by replacing `map(...)` with `tomap([...])`.
|
||||
can get the same result by replacing `map(...)` with `tomap({...})`.
|
||||
For example:
|
||||
|
||||
```diff
|
||||
|
@ -371,3 +494,44 @@ cleanup of obsolete features and improved consistency:
|
|||
|
||||
If you are using `-force` in an automated call to `terraform destroy`,
|
||||
change to using `-auto-approve` instead.
|
||||
|
||||
## Azure Backend Removed Arguments
|
||||
|
||||
In an earlier release the `azure` backend changed to remove the `arm_` prefix
|
||||
from a number of the configuration arguments:
|
||||
|
||||
| Old Name | New Name |
|
||||
|-----------------------|-------------------|
|
||||
| `arm_client_id` | `client_id` |
|
||||
| `arm_client_secret` | `client_secret` |
|
||||
| `arm_subscription_id` | `subscription_id` |
|
||||
| `arm_tenant_id` | `tenant_id` |
|
||||
|
||||
The old names were previously deprecated, but we've removed them altogether
|
||||
in Terraform v0.15 in order to conclude that deprecation cycle.
|
||||
|
||||
If you have a backend configuration using the old names then you may see
|
||||
errors like the following when upgrading to Terraform v0.15:
|
||||
|
||||
```
|
||||
╷
|
||||
│ Error: Invalid backend configuration argument
|
||||
│
|
||||
│ The backend configuration argument "arm_client_id" given on
|
||||
│ the command line is not expected for the selected backend type.
|
||||
╵
|
||||
```
|
||||
|
||||
If you see errors like this, rename the arguments in your backend configuration
|
||||
as shown in the table above and then run the following to re-initialize your
|
||||
backend configuration:
|
||||
|
||||
```
|
||||
terraform init -reconfigure
|
||||
```
|
||||
|
||||
The `-reconfigure` argument instructs Terraform to just replace the old
|
||||
configuration with the new configuration directly, rather than offering to
|
||||
migrate the latest state snapshots from the old to the new configuration.
|
||||
Migration would not be appropriate in this case because the old and new
|
||||
configurations are equivalent and refer to the same remote objects.
|
||||
|
|
Loading…
Reference in New Issue