Map CLI workspaces by TFC tags
This commit is contained in:
parent
7a243379fb
commit
6dcd0db265
|
@ -26,6 +26,7 @@ import (
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
"github.com/mitchellh/colorstring"
|
"github.com/mitchellh/colorstring"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
"github.com/zclconf/go-cty/cty/gocty"
|
||||||
|
|
||||||
backendLocal "github.com/hashicorp/terraform/internal/backend/local"
|
backendLocal "github.com/hashicorp/terraform/internal/backend/local"
|
||||||
)
|
)
|
||||||
|
@ -104,17 +105,17 @@ func (b *Cloud) ConfigSchema() *configschema.Block {
|
||||||
"hostname": {
|
"hostname": {
|
||||||
Type: cty.String,
|
Type: cty.String,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
Description: schemaDescriptions["hostname"],
|
Description: schemaDescriptionHostname,
|
||||||
},
|
},
|
||||||
"organization": {
|
"organization": {
|
||||||
Type: cty.String,
|
Type: cty.String,
|
||||||
Required: true,
|
Required: true,
|
||||||
Description: schemaDescriptions["organization"],
|
Description: schemaDescriptionOrganization,
|
||||||
},
|
},
|
||||||
"token": {
|
"token": {
|
||||||
Type: cty.String,
|
Type: cty.String,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
Description: schemaDescriptions["token"],
|
Description: schemaDescriptionToken,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -125,12 +126,17 @@ func (b *Cloud) ConfigSchema() *configschema.Block {
|
||||||
"name": {
|
"name": {
|
||||||
Type: cty.String,
|
Type: cty.String,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
Description: schemaDescriptions["name"],
|
Description: schemaDescriptionName,
|
||||||
},
|
},
|
||||||
"prefix": {
|
"prefix": {
|
||||||
Type: cty.String,
|
Type: cty.String,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
Description: schemaDescriptions["prefix"],
|
Description: schemaDescriptionPrefix,
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
Type: cty.Set(cty.String),
|
||||||
|
Optional: true,
|
||||||
|
Description: schemaDescriptionTags,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -159,6 +165,12 @@ func (b *Cloud) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) {
|
||||||
if val := workspaces.GetAttr("prefix"); !val.IsNull() {
|
if val := workspaces.GetAttr("prefix"); !val.IsNull() {
|
||||||
workspaceMapping.prefix = val.AsString()
|
workspaceMapping.prefix = val.AsString()
|
||||||
}
|
}
|
||||||
|
if val := workspaces.GetAttr("tags"); !val.IsNull() {
|
||||||
|
err := gocty.FromCtyValue(val, &workspaceMapping.tags)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("An unxpected error occurred: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch workspaceMapping.strategy() {
|
switch workspaceMapping.strategy() {
|
||||||
|
@ -328,6 +340,15 @@ func (b *Cloud) setConfigurationFields(obj cty.Value) tfdiags.Diagnostics {
|
||||||
if val := workspaces.GetAttr("prefix"); !val.IsNull() {
|
if val := workspaces.GetAttr("prefix"); !val.IsNull() {
|
||||||
b.workspaceMapping.prefix = val.AsString()
|
b.workspaceMapping.prefix = val.AsString()
|
||||||
}
|
}
|
||||||
|
if val := workspaces.GetAttr("tags"); !val.IsNull() {
|
||||||
|
var tags []string
|
||||||
|
err := gocty.FromCtyValue(val, &tags)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("An unxpected error occurred: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.workspaceMapping.tags = tags
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if we are forced to use the local backend.
|
// Determine if we are forced to use the local backend.
|
||||||
|
@ -526,6 +547,9 @@ func (b *Cloud) workspaces() ([]string, error) {
|
||||||
options.Search = tfe.String(b.workspaceMapping.name)
|
options.Search = tfe.String(b.workspaceMapping.name)
|
||||||
case workspacePrefixStrategy:
|
case workspacePrefixStrategy:
|
||||||
options.Search = tfe.String(b.workspaceMapping.prefix)
|
options.Search = tfe.String(b.workspaceMapping.prefix)
|
||||||
|
case workspaceTagsStrategy:
|
||||||
|
taglist := strings.Join(b.workspaceMapping.tags, ",")
|
||||||
|
options.Tags = &taglist
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a slice to contain all the names.
|
// Create a slice to contain all the names.
|
||||||
|
@ -551,7 +575,7 @@ func (b *Cloud) workspaces() ([]string, error) {
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// Pass-through. "name" and "prefix" strategies are naive and do
|
// Pass-through. "name" and "prefix" strategies are naive and do
|
||||||
// client-side filtering above, but for any other future
|
// client-side filtering above, but for tags and any other future
|
||||||
// strategy this filtering should be left to the API.
|
// strategy this filtering should be left to the API.
|
||||||
names = append(names, w.Name)
|
names = append(names, w.Name)
|
||||||
}
|
}
|
||||||
|
@ -627,6 +651,13 @@ func (b *Cloud) StateMgr(name string) (statemgr.Full, error) {
|
||||||
Name: tfe.String(name),
|
Name: tfe.String(name),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var tags []*tfe.Tag
|
||||||
|
for _, tag := range b.workspaceMapping.tags {
|
||||||
|
t := tfe.Tag{Name: tag}
|
||||||
|
tags = append(tags, &t)
|
||||||
|
}
|
||||||
|
options.Tags = tags
|
||||||
|
|
||||||
// We only set the Terraform Version for the new workspace if this is
|
// We only set the Terraform Version for the new workspace if this is
|
||||||
// a release candidate or a final release.
|
// a release candidate or a final release.
|
||||||
if tfversion.Prerelease == "" || strings.HasPrefix(tfversion.Prerelease, "rc") {
|
if tfversion.Prerelease == "" || strings.HasPrefix(tfversion.Prerelease, "rc") {
|
||||||
|
@ -985,11 +1016,13 @@ func (b *Cloud) cliColorize() *colorstring.Colorize {
|
||||||
type workspaceMapping struct {
|
type workspaceMapping struct {
|
||||||
name string
|
name string
|
||||||
prefix string
|
prefix string
|
||||||
|
tags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type workspaceStrategy string
|
type workspaceStrategy string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
workspaceTagsStrategy workspaceStrategy = "tags"
|
||||||
workspaceNameStrategy workspaceStrategy = "name"
|
workspaceNameStrategy workspaceStrategy = "name"
|
||||||
workspacePrefixStrategy workspaceStrategy = "prefix"
|
workspacePrefixStrategy workspaceStrategy = "prefix"
|
||||||
workspaceNoneStrategy workspaceStrategy = "none"
|
workspaceNoneStrategy workspaceStrategy = "none"
|
||||||
|
@ -998,11 +1031,13 @@ const (
|
||||||
|
|
||||||
func (wm workspaceMapping) strategy() workspaceStrategy {
|
func (wm workspaceMapping) strategy() workspaceStrategy {
|
||||||
switch {
|
switch {
|
||||||
case wm.name != "" && wm.prefix == "":
|
case len(wm.tags) > 0 && wm.name == "" && wm.prefix == "":
|
||||||
|
return workspaceTagsStrategy
|
||||||
|
case len(wm.tags) == 0 && wm.name != "" && wm.prefix == "":
|
||||||
return workspaceNameStrategy
|
return workspaceNameStrategy
|
||||||
case wm.name == "" && wm.prefix != "":
|
case len(wm.tags) == 0 && wm.name == "" && wm.prefix != "":
|
||||||
return workspacePrefixStrategy
|
return workspacePrefixStrategy
|
||||||
case wm.name == "" && wm.prefix == "":
|
case len(wm.tags) == 0 && wm.name == "" && wm.prefix == "":
|
||||||
return workspaceNoneStrategy
|
return workspaceNoneStrategy
|
||||||
default:
|
default:
|
||||||
// Any other combination is invalid as each strategy is mutually exclusive
|
// Any other combination is invalid as each strategy is mutually exclusive
|
||||||
|
@ -1070,22 +1105,33 @@ const operationNotCanceled = `
|
||||||
[reset][red]The remote operation was not cancelled.[reset]
|
[reset][red]The remote operation was not cancelled.[reset]
|
||||||
`
|
`
|
||||||
|
|
||||||
var schemaDescriptions = map[string]string{
|
var (
|
||||||
"hostname": "The Terraform Enterprise hostname to connect to. This optional argument defaults to app.terraform.io for use with Terraform Cloud.",
|
workspaceConfigurationHelp = fmt.Sprintf(
|
||||||
"organization": "The name of the organization containing the targeted workspace(s).",
|
`The 'workspaces' block configures how Terraform CLI maps its workspaces for this single
|
||||||
"token": "The token used to authenticate with Terraform Cloud/Enterprise. Typically this argument should not be set,\n" +
|
configuration to workspaces within a Terraform Cloud organization. Three strategies are available:
|
||||||
"and 'terraform login' used instead; your credentials will then be fetched from your CLI configuration file or configured credential helper.",
|
|
||||||
"name": "The name of a single Terraform Cloud workspace to be used with this configuration.\n" +
|
|
||||||
"When configured only the specified workspace can be used. This option conflicts\n" +
|
|
||||||
"with \"prefix\".",
|
|
||||||
"prefix": "A name prefix used to select remote Terraform Cloud workspaces to be used for this\n" +
|
|
||||||
"single configuration. New workspaces will automatically be prefixed with this prefix. This option conflicts with \"name\".",
|
|
||||||
}
|
|
||||||
|
|
||||||
var workspaceConfigurationHelp = fmt.Sprintf(`The 'workspaces' block configures how Terraform CLI maps its workspaces for this
|
[bold]tags[reset] - %s
|
||||||
single configuration to workspaces within a Terraform Cloud organization. Two strategies are available:
|
|
||||||
|
|
||||||
[bold]name[reset] - %s
|
[bold]name[reset] - %s
|
||||||
|
|
||||||
[bold]prefix[reset] - %s
|
[bold]prefix[reset] - %s`, schemaDescriptionTags, schemaDescriptionName, schemaDescriptionPrefix)
|
||||||
`, schemaDescriptions["name"], schemaDescriptions["prefix"])
|
|
||||||
|
schemaDescriptionHostname = `The Terraform Enterprise hostname to connect to. This optional argument defaults to app.terraform.io
|
||||||
|
for use with Terraform Cloud.`
|
||||||
|
|
||||||
|
schemaDescriptionOrganization = `The name of the organization containing the targeted workspace(s).`
|
||||||
|
|
||||||
|
schemaDescriptionToken = `The token used to authenticate with Terraform Cloud/Enterprise. Typically this argument should not
|
||||||
|
be set, and 'terraform login' used instead; your credentials will then be fetched from your CLI
|
||||||
|
configuration file or configured credential helper.`
|
||||||
|
|
||||||
|
schemaDescriptionTags = `A set of tags used to select remote Terraform Cloud workspaces to be used for this single
|
||||||
|
configuration. New workspaces will automatically be tagged with these tag values. Generally, this
|
||||||
|
is the primary and recommended strategy to use. This option conflicts with "prefix" and "name".`
|
||||||
|
|
||||||
|
schemaDescriptionName = `The name of a single Terraform Cloud workspace to be used with this configuration When configured
|
||||||
|
only the specified workspace can be used. This option conflicts with "tags" and "prefix".`
|
||||||
|
|
||||||
|
schemaDescriptionPrefix = `DEPRECATED. A name prefix used to select remote Terraform Cloud to be used for this single configuration. New
|
||||||
|
workspaces will automatically be prefixed with this prefix. This option conflicts with "tags" and "name".`
|
||||||
|
)
|
||||||
|
|
|
@ -51,6 +51,7 @@ func TestCloud_PrepareConfig(t *testing.T) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.StringVal("prod"),
|
"name": cty.StringVal("prod"),
|
||||||
"prefix": cty.NullVal(cty.String),
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
expectedErr: `Invalid organization value: The "organization" attribute value must not be empty.`,
|
expectedErr: `Invalid organization value: The "organization" attribute value must not be empty.`,
|
||||||
|
@ -60,17 +61,18 @@ func TestCloud_PrepareConfig(t *testing.T) {
|
||||||
"organization": cty.StringVal("org"),
|
"organization": cty.StringVal("org"),
|
||||||
"workspaces": cty.NullVal(cty.String),
|
"workspaces": cty.NullVal(cty.String),
|
||||||
}),
|
}),
|
||||||
expectedErr: `Invalid workspaces configuration: Either workspace "name" or "prefix" is required.`,
|
expectedErr: `Invalid workspaces configuration: Missing workspace mapping strategy. Either workspace "tags", "name", or "prefix" is required.`,
|
||||||
},
|
},
|
||||||
"workspace: empty name and empty prefix": {
|
"workspace: empty tags, name, and prefix": {
|
||||||
config: cty.ObjectVal(map[string]cty.Value{
|
config: cty.ObjectVal(map[string]cty.Value{
|
||||||
"organization": cty.StringVal("org"),
|
"organization": cty.StringVal("org"),
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.NullVal(cty.String),
|
"name": cty.NullVal(cty.String),
|
||||||
"prefix": cty.NullVal(cty.String),
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
expectedErr: `Invalid workspaces configuration: Either workspace "name" or "prefix" is required.`,
|
expectedErr: `Invalid workspaces configuration: Missing workspace mapping strategy. Either workspace "tags", "name", or "prefix" is required.`,
|
||||||
},
|
},
|
||||||
"workspace: name and prefix present": {
|
"workspace: name and prefix present": {
|
||||||
config: cty.ObjectVal(map[string]cty.Value{
|
config: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
@ -78,9 +80,25 @@ func TestCloud_PrepareConfig(t *testing.T) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.StringVal("prod"),
|
"name": cty.StringVal("prod"),
|
||||||
"prefix": cty.StringVal("app-"),
|
"prefix": cty.StringVal("app-"),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
expectedErr: `Invalid workspaces configuration: Only one of workspace "name" or "prefix" is allowed.`,
|
expectedErr: `Invalid workspaces configuration: Only one of workspace "tags", "name", or "prefix" is allowed.`,
|
||||||
|
},
|
||||||
|
"workspace: name and tags present": {
|
||||||
|
config: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"organization": cty.StringVal("org"),
|
||||||
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"name": cty.StringVal("prod"),
|
||||||
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.SetVal(
|
||||||
|
[]cty.Value{
|
||||||
|
cty.StringVal("billing"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
expectedErr: `Invalid workspaces configuration: Only one of workspace "tags", "name", or "prefix" is allowed.`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,6 +131,7 @@ func TestCloud_config(t *testing.T) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.StringVal("prod"),
|
"name": cty.StringVal("prod"),
|
||||||
"prefix": cty.NullVal(cty.String),
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
confErr: "organization \"nonexisting\" at host app.terraform.io not found",
|
confErr: "organization \"nonexisting\" at host app.terraform.io not found",
|
||||||
|
@ -125,6 +144,7 @@ func TestCloud_config(t *testing.T) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.StringVal("prod"),
|
"name": cty.StringVal("prod"),
|
||||||
"prefix": cty.NullVal(cty.String),
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
confErr: "Failed to request discovery document",
|
confErr: "Failed to request discovery document",
|
||||||
|
@ -138,10 +158,27 @@ func TestCloud_config(t *testing.T) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.StringVal("prod"),
|
"name": cty.StringVal("prod"),
|
||||||
"prefix": cty.NullVal(cty.String),
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
confErr: "terraform login localhost",
|
confErr: "terraform login localhost",
|
||||||
},
|
},
|
||||||
|
"with_tags": {
|
||||||
|
config: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"hostname": cty.NullVal(cty.String),
|
||||||
|
"organization": cty.StringVal("hashicorp"),
|
||||||
|
"token": cty.NullVal(cty.String),
|
||||||
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"name": cty.NullVal(cty.String),
|
||||||
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.SetVal(
|
||||||
|
[]cty.Value{
|
||||||
|
cty.StringVal("billing"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
"with_a_name": {
|
"with_a_name": {
|
||||||
config: cty.ObjectVal(map[string]cty.Value{
|
config: cty.ObjectVal(map[string]cty.Value{
|
||||||
"hostname": cty.NullVal(cty.String),
|
"hostname": cty.NullVal(cty.String),
|
||||||
|
@ -150,6 +187,7 @@ func TestCloud_config(t *testing.T) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.StringVal("prod"),
|
"name": cty.StringVal("prod"),
|
||||||
"prefix": cty.NullVal(cty.String),
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
@ -161,10 +199,11 @@ func TestCloud_config(t *testing.T) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.NullVal(cty.String),
|
"name": cty.NullVal(cty.String),
|
||||||
"prefix": cty.StringVal("my-app-"),
|
"prefix": cty.StringVal("my-app-"),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
"without_either_a_name_and_a_prefix": {
|
"without_a_name_prefix_or_tags": {
|
||||||
config: cty.ObjectVal(map[string]cty.Value{
|
config: cty.ObjectVal(map[string]cty.Value{
|
||||||
"hostname": cty.NullVal(cty.String),
|
"hostname": cty.NullVal(cty.String),
|
||||||
"organization": cty.StringVal("hashicorp"),
|
"organization": cty.StringVal("hashicorp"),
|
||||||
|
@ -172,9 +211,10 @@ func TestCloud_config(t *testing.T) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.NullVal(cty.String),
|
"name": cty.NullVal(cty.String),
|
||||||
"prefix": cty.NullVal(cty.String),
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
valErr: `Either workspace "name" or "prefix" is required`,
|
valErr: `Missing workspace mapping strategy.`,
|
||||||
},
|
},
|
||||||
"with_both_a_name_and_a_prefix": {
|
"with_both_a_name_and_a_prefix": {
|
||||||
config: cty.ObjectVal(map[string]cty.Value{
|
config: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
@ -184,9 +224,27 @@ func TestCloud_config(t *testing.T) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.StringVal("prod"),
|
"name": cty.StringVal("prod"),
|
||||||
"prefix": cty.StringVal("my-app-"),
|
"prefix": cty.StringVal("my-app-"),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
valErr: `Only one of workspace "name" or "prefix" is allowed`,
|
valErr: `Only one of workspace "tags", "name", or "prefix" is allowed.`,
|
||||||
|
},
|
||||||
|
"with_both_a_name_and_tags": {
|
||||||
|
config: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"hostname": cty.NullVal(cty.String),
|
||||||
|
"organization": cty.StringVal("hashicorp"),
|
||||||
|
"token": cty.NullVal(cty.String),
|
||||||
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"name": cty.StringVal("prod"),
|
||||||
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.SetVal(
|
||||||
|
[]cty.Value{
|
||||||
|
cty.StringVal("billing"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
valErr: `Only one of workspace "tags", "name", or "prefix" is allowed.`,
|
||||||
},
|
},
|
||||||
"null config": {
|
"null config": {
|
||||||
config: cty.NullVal(cty.EmptyObject),
|
config: cty.NullVal(cty.EmptyObject),
|
||||||
|
@ -222,6 +280,7 @@ func TestCloud_setConfigurationFields(t *testing.T) {
|
||||||
expectedOrganziation string
|
expectedOrganziation string
|
||||||
expectedWorkspacePrefix string
|
expectedWorkspacePrefix string
|
||||||
expectedWorkspaceName string
|
expectedWorkspaceName string
|
||||||
|
expectedWorkspaceTags []string
|
||||||
expectedForceLocal bool
|
expectedForceLocal bool
|
||||||
setEnv func()
|
setEnv func()
|
||||||
resetEnv func()
|
resetEnv func()
|
||||||
|
@ -234,6 +293,7 @@ func TestCloud_setConfigurationFields(t *testing.T) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.StringVal("prod"),
|
"name": cty.StringVal("prod"),
|
||||||
"prefix": cty.NullVal(cty.String),
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
expectedHostname: "hashicorp.com",
|
expectedHostname: "hashicorp.com",
|
||||||
|
@ -246,6 +306,7 @@ func TestCloud_setConfigurationFields(t *testing.T) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.StringVal("prod"),
|
"name": cty.StringVal("prod"),
|
||||||
"prefix": cty.NullVal(cty.String),
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
expectedHostname: defaultHostname,
|
expectedHostname: defaultHostname,
|
||||||
|
@ -258,6 +319,7 @@ func TestCloud_setConfigurationFields(t *testing.T) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.StringVal("prod"),
|
"name": cty.StringVal("prod"),
|
||||||
"prefix": cty.NullVal(cty.String),
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
expectedHostname: "hashicorp.com",
|
expectedHostname: "hashicorp.com",
|
||||||
|
@ -271,12 +333,31 @@ func TestCloud_setConfigurationFields(t *testing.T) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.NullVal(cty.String),
|
"name": cty.NullVal(cty.String),
|
||||||
"prefix": cty.StringVal("prod"),
|
"prefix": cty.StringVal("prod"),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
expectedHostname: "hashicorp.com",
|
expectedHostname: "hashicorp.com",
|
||||||
expectedOrganziation: "hashicorp",
|
expectedOrganziation: "hashicorp",
|
||||||
expectedWorkspacePrefix: "prod",
|
expectedWorkspacePrefix: "prod",
|
||||||
},
|
},
|
||||||
|
"with workspace tags set": {
|
||||||
|
obj: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"organization": cty.StringVal("hashicorp"),
|
||||||
|
"hostname": cty.StringVal("hashicorp.com"),
|
||||||
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"name": cty.NullVal(cty.String),
|
||||||
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.SetVal(
|
||||||
|
[]cty.Value{
|
||||||
|
cty.StringVal("billing"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
expectedHostname: "hashicorp.com",
|
||||||
|
expectedOrganziation: "hashicorp",
|
||||||
|
expectedWorkspaceTags: []string{"billing"},
|
||||||
|
},
|
||||||
"with force local set": {
|
"with force local set": {
|
||||||
obj: cty.ObjectVal(map[string]cty.Value{
|
obj: cty.ObjectVal(map[string]cty.Value{
|
||||||
"organization": cty.StringVal("hashicorp"),
|
"organization": cty.StringVal("hashicorp"),
|
||||||
|
@ -284,6 +365,7 @@ func TestCloud_setConfigurationFields(t *testing.T) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.NullVal(cty.String),
|
"name": cty.NullVal(cty.String),
|
||||||
"prefix": cty.StringVal("prod"),
|
"prefix": cty.StringVal("prod"),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
expectedHostname: "hashicorp.com",
|
expectedHostname: "hashicorp.com",
|
||||||
|
@ -317,16 +399,51 @@ func TestCloud_setConfigurationFields(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if tc.expectedHostname != "" && b.hostname != tc.expectedHostname {
|
if tc.expectedHostname != "" && b.hostname != tc.expectedHostname {
|
||||||
t.Fatalf("%s: expected hostname %s to match actual hostname %s", name, tc.expectedHostname, b.hostname)
|
t.Fatalf("%s: expected hostname %s to match configured hostname %s", name, b.hostname, tc.expectedHostname)
|
||||||
}
|
}
|
||||||
if tc.expectedOrganziation != "" && b.organization != tc.expectedOrganziation {
|
if tc.expectedOrganziation != "" && b.organization != tc.expectedOrganziation {
|
||||||
t.Fatalf("%s: expected organization %s to match actual organization %s", name, tc.expectedOrganziation, b.organization)
|
t.Fatalf("%s: expected organization (%s) to match configured organization (%s)", name, b.organization, tc.expectedOrganziation)
|
||||||
}
|
}
|
||||||
if tc.expectedWorkspacePrefix != "" && b.workspaceMapping.prefix != tc.expectedWorkspacePrefix {
|
if tc.expectedWorkspacePrefix != "" && b.workspaceMapping.prefix != tc.expectedWorkspacePrefix {
|
||||||
t.Fatalf("%s: expected workspace prefix %s to match actual workspace prefix %s", name, tc.expectedWorkspacePrefix, b.workspaceMapping.prefix)
|
t.Fatalf("%s: expected workspace prefix mapping (%s) to match configured workspace prefix (%s)", name, b.workspaceMapping.prefix, tc.expectedWorkspacePrefix)
|
||||||
}
|
}
|
||||||
if tc.expectedWorkspaceName != "" && b.workspaceMapping.name != tc.expectedWorkspaceName {
|
if tc.expectedWorkspaceName != "" && b.workspaceMapping.name != tc.expectedWorkspaceName {
|
||||||
t.Fatalf("%s: expected workspace name %s to match actual workspace name %s", name, tc.expectedWorkspaceName, b.workspaceMapping.name)
|
t.Fatalf("%s: expected workspace name mapping (%s) to match configured workspace name (%s)", name, b.workspaceMapping.name, tc.expectedWorkspaceName)
|
||||||
|
}
|
||||||
|
if len(tc.expectedWorkspaceTags) > 0 {
|
||||||
|
presentSet := make(map[string]struct{})
|
||||||
|
for _, tag := range b.workspaceMapping.tags {
|
||||||
|
presentSet[tag] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedSet := make(map[string]struct{})
|
||||||
|
for _, tag := range tc.expectedWorkspaceTags {
|
||||||
|
expectedSet[tag] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var missing []string
|
||||||
|
var unexpected []string
|
||||||
|
|
||||||
|
for _, expected := range tc.expectedWorkspaceTags {
|
||||||
|
if _, ok := presentSet[expected]; !ok {
|
||||||
|
missing = append(missing, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, actual := range b.workspaceMapping.tags {
|
||||||
|
if _, ok := expectedSet[actual]; !ok {
|
||||||
|
unexpected = append(missing, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missing) > 0 {
|
||||||
|
t.Fatalf("%s: expected workspace tag mapping (%s) to contain the following tags: %s", name, b.workspaceMapping.tags, missing)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(unexpected) > 0 {
|
||||||
|
t.Fatalf("%s: expected workspace tag mapping (%s) to NOT contain the following tags: %s", name, b.workspaceMapping.tags, unexpected)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if tc.expectedForceLocal != false && b.forceLocal != tc.expectedForceLocal {
|
if tc.expectedForceLocal != false && b.forceLocal != tc.expectedForceLocal {
|
||||||
t.Fatalf("%s: expected force local backend to be set ", name)
|
t.Fatalf("%s: expected force local backend to be set ", name)
|
||||||
|
@ -349,6 +466,7 @@ func TestCloud_versionConstraints(t *testing.T) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.StringVal("prod"),
|
"name": cty.StringVal("prod"),
|
||||||
"prefix": cty.NullVal(cty.String),
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
version: "0.11.1",
|
version: "0.11.1",
|
||||||
|
@ -361,6 +479,7 @@ func TestCloud_versionConstraints(t *testing.T) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.StringVal("prod"),
|
"name": cty.StringVal("prod"),
|
||||||
"prefix": cty.NullVal(cty.String),
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
version: "0.0.1",
|
version: "0.0.1",
|
||||||
|
@ -374,6 +493,7 @@ func TestCloud_versionConstraints(t *testing.T) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.StringVal("prod"),
|
"name": cty.StringVal("prod"),
|
||||||
"prefix": cty.NullVal(cty.String),
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
version: "10.0.1",
|
version: "10.0.1",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package cloud
|
package cloud
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
)
|
)
|
||||||
|
@ -9,21 +11,21 @@ var (
|
||||||
invalidOrganizationConfigMissingValue = tfdiags.AttributeValue(
|
invalidOrganizationConfigMissingValue = tfdiags.AttributeValue(
|
||||||
tfdiags.Error,
|
tfdiags.Error,
|
||||||
"Invalid organization value",
|
"Invalid organization value",
|
||||||
`The "organization" attribute value must not be empty.`,
|
`The "organization" attribute value must not be empty.\n\n%s`,
|
||||||
cty.Path{cty.GetAttrStep{Name: "organization"}},
|
cty.Path{cty.GetAttrStep{Name: "organization"}},
|
||||||
)
|
)
|
||||||
|
|
||||||
invalidWorkspaceConfigMissingValues = tfdiags.AttributeValue(
|
invalidWorkspaceConfigMissingValues = tfdiags.AttributeValue(
|
||||||
tfdiags.Error,
|
tfdiags.Error,
|
||||||
"Invalid workspaces configuration",
|
"Invalid workspaces configuration",
|
||||||
`Either workspace "name" or "prefix" is required.`,
|
fmt.Sprintf("Missing workspace mapping strategy. Either workspace \"tags\", \"name\", or \"prefix\" is required.\n\n%s", workspaceConfigurationHelp),
|
||||||
cty.Path{cty.GetAttrStep{Name: "workspaces"}},
|
cty.Path{cty.GetAttrStep{Name: "workspaces"}},
|
||||||
)
|
)
|
||||||
|
|
||||||
invalidWorkspaceConfigMisconfiguration = tfdiags.AttributeValue(
|
invalidWorkspaceConfigMisconfiguration = tfdiags.AttributeValue(
|
||||||
tfdiags.Error,
|
tfdiags.Error,
|
||||||
"Invalid workspaces configuration",
|
"Invalid workspaces configuration",
|
||||||
`Only one of workspace "name" or "prefix" is allowed.`,
|
fmt.Sprintf("Only one of workspace \"tags\", \"name\", or \"prefix\" is allowed.\n\n%s", workspaceConfigurationHelp),
|
||||||
cty.Path{cty.GetAttrStep{Name: "workspaces"}},
|
cty.Path{cty.GetAttrStep{Name: "workspaces"}},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -72,6 +72,7 @@ func testBackendDefault(t *testing.T) (*Cloud, func()) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.StringVal("prod"),
|
"name": cty.StringVal("prod"),
|
||||||
"prefix": cty.NullVal(cty.String),
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
return testBackend(t, obj)
|
return testBackend(t, obj)
|
||||||
|
@ -85,6 +86,7 @@ func testBackendNoDefault(t *testing.T) (*Cloud, func()) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.NullVal(cty.String),
|
"name": cty.NullVal(cty.String),
|
||||||
"prefix": cty.StringVal("my-app-"),
|
"prefix": cty.StringVal("my-app-"),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
return testBackend(t, obj)
|
return testBackend(t, obj)
|
||||||
|
@ -98,6 +100,7 @@ func testBackendNoOperations(t *testing.T) (*Cloud, func()) {
|
||||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||||
"name": cty.StringVal("prod"),
|
"name": cty.StringVal("prod"),
|
||||||
"prefix": cty.NullVal(cty.String),
|
"prefix": cty.NullVal(cty.String),
|
||||||
|
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
return testBackend(t, obj)
|
return testBackend(t, obj)
|
||||||
|
|
Loading…
Reference in New Issue