Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/resources/environment_variable.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ resource "spacelift_environment_variable" "core-kubeconfig" {
- `module_id` (String) ID of the module on which the environment variable is defined
- `stack_id` (String) ID of the stack on which the environment variable is defined
- `value` (String, Sensitive) Value of the environment variable. Defaults to an empty string.
- `value_nonsensitive` (String) Value of the environment variable. Defaults to an empty string.
- `write_only` (Boolean) Indicates whether the value is secret or not. Defaults to `true`.

### Read-Only
Expand Down
34 changes: 31 additions & 3 deletions spacelift/resource_environment_variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ func resourceEnvironmentVariable() *schema.Resource {
Optional: true,
Default: "",
ForceNew: true,
ConflictsWith: []string{"value_nonsensitive"},
},
"value_nonsensitive": {
Copy link
Member

@eliecharra eliecharra Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we introduce that, we should IMO fully get rid of write_only since it will bring confusion.
The validation we perform works, but will only trigger during apply I think nope? That's a bit sad that we can't catch such misconfiguration earlier in the plan.

Removing write_only will introduce a BC break, so I can understand we want to avoid that. But maybe let's mark it as deprecated?

Can we also use ConflictsWith write_only ? Or find a way to validate that the logic is correct earlier than during apply? Not sure if we can do cross field validation 🤔

Type: schema.TypeString,
Description: "Value of the environment variable. Defaults to an empty string.",
Sensitive: false,
Optional: true,
Default: "",
ForceNew: true,
ConflictsWith: []string{"value"},
},
"write_only": {
Type: schema.TypeBool,
Expand All @@ -93,12 +103,19 @@ func resourceEnvironmentVariable() *schema.Resource {
}

func resourceEnvironmentVariableCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
writeOnly := d.Get("write_only").(bool)

value, sensitive := resourceEnvironmentVariableValue(d)
if !sensitive && writeOnly {
return diag.Errorf("a non-sensitive environment variable cannot be write-only")
}

variables := map[string]interface{}{
"config": structs.ConfigInput{
ID: toID(d.Get("name")),
Type: structs.ConfigType("ENVIRONMENT_VARIABLE"),
Value: toString(d.Get("value")),
WriteOnly: graphql.Boolean(d.Get("write_only").(bool)),
Value: value,
WriteOnly: graphql.Boolean(writeOnly),
Description: toOptionalString(d.Get("description")),
},
}
Expand Down Expand Up @@ -127,6 +144,13 @@ func resourceEnvironmentVariableCreate(ctx context.Context, d *schema.ResourceDa
return resourceEnvironmentVariableCreateModule(ctx, d, meta.(*internal.Client), variables)
}

func resourceEnvironmentVariableValue(d *schema.ResourceData) (graphql.String, bool) {
if v, ok := d.GetOk("value_nonsensitive"); ok {
return toString(v), false
}
return toString(d.Get("value")), true
}

func resourceEnvironmentVariableCreateContext(ctx context.Context, d *schema.ResourceData, client *internal.Client, variables map[string]interface{}) diag.Diagnostics {
var mutation struct {
AddContextConfig structs.ConfigElement `graphql:"contextConfigAdd(context: $context, config: $config)"`
Expand Down Expand Up @@ -206,7 +230,11 @@ func resourceEnvironmentVariableRead(ctx context.Context, d *schema.ResourceData
d.Set("write_only", element.WriteOnly)

if value := element.Value; value != nil {
d.Set("value", *value)
if _, ok := d.GetOk("value_nonsensitive"); ok {
d.Set("value_nonsensitive", *value)
} else {
d.Set("value", *value)
}
} else {
d.Set("value", element.Checksum)
}
Expand Down
137 changes: 137 additions & 0 deletions spacelift/resource_environment_variable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package spacelift

import (
"fmt"
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
Expand Down Expand Up @@ -141,3 +142,139 @@ func TestEnvironmentVariableResource(t *testing.T) {
})
})
}

func TestEnvironmentVariableResourceNonsensitiveValue(t *testing.T) {
const resourceName = "spacelift_environment_variable.test"

t.Run("with a context", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)

config := func(description string) string {
return fmt.Sprintf(`
resource "spacelift_context" "test" {
name = "My first context %s"
}

resource "spacelift_environment_variable" "test" {
context_id = spacelift_context.test.id
name = "BACON"
value_nonsensitive = "is tasty"
write_only = false
description = %s
}
`, randomID, description)
}

testSteps(t, []resource.TestStep{
{
Config: config(`"Bacon is tasty"`),
Check: Resource(
resourceName,
Attribute("id", IsNotEmpty()),
Attribute("checksum", Equals("4d5d01ea427b10dd483e8fce5b5149fb5a9814e9ee614176b756ca4a65c8f154")),
Attribute("context_id", Contains(randomID)),
Attribute("name", Equals("BACON")),
Attribute("value_nonsensitive", Equals("is tasty")),
Attribute("write_only", Equals("false")),
Attribute("description", Equals("Bacon is tasty")),
AttributeNotPresent("value"),
AttributeNotPresent("module_id"),
AttributeNotPresent("stack_id"),
),
},
})
})

t.Run("with a module", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)

testSteps(t, []resource.TestStep{
{
Config: fmt.Sprintf(`
resource "spacelift_module" "test" {
name = "test-module-%s"
branch = "master"
repository = "terraform-bacon-tasty"
}

resource "spacelift_environment_variable" "test" {
module_id = spacelift_module.test.id
name = "BACON"
value_nonsensitive = "is tasty"
write_only = false
description = "Bacon is tasty"
}
`, randomID),
Check: Resource(
resourceName,
Attribute("module_id", Equals(fmt.Sprintf("terraform-default-test-module-%s", randomID))),
Attribute("value_nonsensitive", Equals("is tasty")),
Attribute("write_only", Equals("false")),
Attribute("description", Equals("Bacon is tasty")),
AttributeNotPresent("value"),
AttributeNotPresent("context_id"),
AttributeNotPresent("stack_id"),
),
},
})
})

t.Run("with a stack", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)

testSteps(t, []resource.TestStep{
{
Config: fmt.Sprintf(`
resource "spacelift_stack" "test" {
branch = "master"
repository = "demo"
name = "Test stack %s"
}

resource "spacelift_environment_variable" "test" {
stack_id = spacelift_stack.test.id
value_nonsensitive = "is tasty"
write_only = false
name = "BACON"
description = "Bacon is tasty"
}
`, randomID),
Check: Resource(
resourceName,
Attribute("stack_id", StartsWith("test-stack-")),
Attribute("stack_id", Contains(randomID)),
Attribute("value_nonsensitive", Equals("is tasty")),
Attribute("description", Equals("Bacon is tasty")),
AttributeNotPresent("value"),
AttributeNotPresent("context_id"),
AttributeNotPresent("module_id"),
),
},
})
})

t.Run("write only is not allowed", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)

testSteps(t, []resource.TestStep{
{
Config: fmt.Sprintf(`
resource "spacelift_stack" "test" {
branch = "master"
repository = "demo"
name = "Test stack %s"
}

resource "spacelift_environment_variable" "test" {
stack_id = spacelift_stack.test.id
value_nonsensitive = "is tasty"
write_only = true
name = "BACON"
description = "Bacon is tasty"
}
`, randomID),
ExpectError: regexp.MustCompile("a non-sensitive environment variable cannot be write-only"),
},
})
})
}
Loading