Skip to content

Commit 4a527f9

Browse files
authored
feat: human-readable stack dependency import format (#704)
Add support for importing stack dependencies and references using human-readable identifiers instead of obscure ULIDs: - Stack dependency: stack-id/depends-on-stack-id (new) vs stack-id/dependency-ulid (old, deprecated) - Stack dependency reference: stack-id/depends-on-stack-id/input-name (new) vs stack-id/dependency-ulid/reference-ulid (old, deprecated) The old ULID-based format remains supported for backward compatibility but is now deprecated in favor of the more user-friendly format.
1 parent e67ea52 commit 4a527f9

File tree

8 files changed

+305
-37
lines changed

8 files changed

+305
-37
lines changed

docs/resources/stack_dependency.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ page_title: "spacelift_stack_dependency Resource - terraform-provider-spacelift"
44
subcategory: ""
55
description: |-
66
spacelift_stack_dependency represents a Spacelift stack dependency - a dependency between two stacks. When one stack depends on another, the tracked runs of the stack will not start until the dependent stack is successfully finished. Additionally, changes to the dependency will trigger the dependent.
7+
~> Import format: Use terraform import spacelift_stack_dependency.example stack-id/depends-on-stack-id. The old format stack-id/dependency-ulid is deprecated but still supported for backward compatibility.
78
---
89

910
# spacelift_stack_dependency (Resource)
1011

1112
`spacelift_stack_dependency` represents a Spacelift **stack dependency** - a dependency between two stacks. When one stack depends on another, the tracked runs of the stack will not start until the dependent stack is successfully finished. Additionally, changes to the dependency will trigger the dependent.
1213

14+
~> **Import format**: Use `terraform import spacelift_stack_dependency.example stack-id/depends-on-stack-id`. The old format `stack-id/dependency-ulid` is deprecated but still supported for backward compatibility.
15+
1316
## Example Usage
1417

1518
```terraform

docs/resources/stack_dependency_reference.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ page_title: "spacelift_stack_dependency_reference Resource - terraform-provider-
44
subcategory: ""
55
description: |-
66
spacelift_stack_dependency_reference represents a Spacelift stack dependency reference - a reference matches a stack's output to another stack's input. It is similar to an environment variable (spacelift_environment_variable), except that value is provided by another stack's output.
7+
~> Import format: Use terraform import spacelift_stack_dependency_reference.example stack-id/depends-on-stack-id/input-name. The old format stack-id/dependency-ulid/reference-ulid is deprecated but still supported for backward compatibility.
78
---
89

910
# spacelift_stack_dependency_reference (Resource)
1011

1112
`spacelift_stack_dependency_reference` represents a Spacelift **stack dependency reference** - a reference matches a stack's output to another stack's input. It is similar to an environment variable (`spacelift_environment_variable`), except that value is provided by another stack's output.
1213

14+
~> **Import format**: Use `terraform import spacelift_stack_dependency_reference.example stack-id/depends-on-stack-id/input-name`. The old format `stack-id/dependency-ulid/reference-ulid` is deprecated but still supported for backward compatibility.
15+
1316
## Example Usage
1417

1518
```terraform

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/hashicorp/terraform-plugin-log v0.9.0
1010
github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1
1111
github.com/kelseyhightower/envconfig v1.4.0
12+
github.com/oklog/ulid/v2 v2.1.1
1213
github.com/pkg/errors v0.9.1
1314
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466
1415
golang.org/x/time v0.14.0

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
130130
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
131131
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
132132
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
133+
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
134+
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
135+
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
133136
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
134137
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
135138
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=

spacelift/resource_stack_dependency.go

Lines changed: 90 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package spacelift
22

33
import (
44
"context"
5+
"fmt"
56
"path"
67
"strings"
78

89
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
910
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
11+
"github.com/oklog/ulid/v2"
1012
"github.com/shurcooL/graphql"
1113

1214
"github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal"
@@ -19,7 +21,9 @@ func resourceStackDependency() *schema.Resource {
1921
"`spacelift_stack_dependency` represents a Spacelift **stack dependency** - " +
2022
"a dependency between two stacks. When one stack depends on another, the tracked runs " +
2123
"of the stack will not start until the dependent stack is successfully finished. Additionally, " +
22-
"changes to the dependency will trigger the dependent.",
24+
"changes to the dependency will trigger the dependent.\n\n" +
25+
"~> **Import format**: Use `terraform import spacelift_stack_dependency.example stack-id/depends-on-stack-id`. " +
26+
"The old format `stack-id/dependency-ulid` is deprecated but still supported for backward compatibility.",
2327

2428
CreateContext: resourceStackDependencyCreate,
2529
ReadContext: resourceStackDependencyRead,
@@ -68,7 +72,12 @@ func resourceStackDependencyDelete(ctx context.Context, d *schema.ResourceData,
6872
StackDependency *structs.StackDependency `graphql:"stackDependencyDelete(id: $id)"`
6973
}
7074

71-
variables := map[string]interface{}{"id": getStackDependencyID(d)}
75+
_, depID, err := parseStackDependencyID(d.Id())
76+
if err != nil {
77+
return diag.FromErr(err)
78+
}
79+
80+
variables := map[string]any{"id": graphql.ID(depID)}
7281

7382
if err := meta.(*internal.Client).Mutate(ctx, "StackDependencyDelete", &query, variables); err != nil {
7483
return diag.Errorf("could not delete stack dependency: %s", err)
@@ -79,46 +88,103 @@ func resourceStackDependencyDelete(ctx context.Context, d *schema.ResourceData,
7988
return nil
8089
}
8190

82-
func resourceStackDependencyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
83-
var query struct {
84-
Stack *struct {
85-
Dependency *structs.StackDependency `graphql:"dependency(id: $id)"`
86-
} `graphql:"stack(id: $stack)"`
87-
}
91+
func resourceStackDependencyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
92+
stackID := toID(d.Get("stack_id"))
93+
dependsOnStackID := toID(d.Get("depends_on_stack_id"))
8894

89-
variables := map[string]interface{}{
90-
"id": getStackDependencyID(d),
91-
"stack": toID(d.Get("stack_id")),
92-
}
93-
94-
if err := meta.(*internal.Client).Query(ctx, "StackDependencyRead", &query, variables); err != nil {
95+
dependency, err := resourceStackDependencyFetchByStackID(ctx, meta, stackID, dependsOnStackID)
96+
if err != nil {
9597
return diag.Errorf("could not query for stack dependency: %s", err)
9698
}
9799

98-
if query.Stack == nil || query.Stack.Dependency == nil {
100+
if dependency == nil {
99101
d.SetId("")
100102
return nil
101103
}
102104

103-
d.Set("stack_id", query.Stack.Dependency.Stack.ID)
104-
d.Set("depends_on_stack_id", query.Stack.Dependency.DependsOnStack.ID)
105+
d.Set("stack_id", dependency.Stack.ID)
106+
d.Set("depends_on_stack_id", dependency.DependsOnStack.ID)
107+
d.SetId(path.Join(dependency.Stack.ID, dependency.ID))
105108

106109
return nil
107110
}
108111

109-
func resourceStackDependencyImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
110-
idParts := strings.Split(d.Id(), "/")
111-
d.Set("stack_id", idParts[0])
112+
func resourceStackDependencyImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) {
113+
stackID, secondPart, err := parseStackDependencyID(d.Id())
114+
if err != nil {
115+
return nil, fmt.Errorf("invalid import ID format: %w", err)
116+
}
112117

113-
resourceStackDependencyRead(ctx, d, meta)
118+
var dependency *structs.StackDependency
119+
120+
// Try to parse as ULID to detect old format (stackID/dependencyULID)
121+
_, err = ulid.Parse(secondPart)
122+
if err == nil {
123+
dependency, err = resourceStackDependencyFetchByID(ctx, meta, toID(stackID), toID(secondPart))
124+
} else {
125+
dependency, err = resourceStackDependencyFetchByStackID(ctx, meta, toID(stackID), toID(secondPart))
126+
}
127+
128+
if err != nil {
129+
return nil, err
130+
}
131+
132+
if dependency == nil {
133+
return nil, fmt.Errorf("stack dependency not found for import ID: %s", path.Join(stackID, secondPart))
134+
}
135+
136+
d.Set("stack_id", dependency.Stack.ID)
137+
d.Set("depends_on_stack_id", dependency.DependsOnStack.ID)
138+
d.SetId(path.Join(dependency.Stack.ID, dependency.ID))
114139

115140
return []*schema.ResourceData{d}, nil
116141
}
117142

118-
func getStackDependencyID(d *schema.ResourceData) graphql.ID {
119-
idParts := strings.Split(d.Id(), "/")
143+
// resourceStackDependencyFetchByID fetches a dependency using ULID (old format)
144+
func resourceStackDependencyFetchByID(ctx context.Context, meta any, stackID, dependencyID graphql.ID) (*structs.StackDependency, error) {
145+
variables := map[string]any{
146+
"id": dependencyID,
147+
"stackId": stackID,
148+
"dependsOnStackId": toID(""),
149+
}
150+
return resourceStackDependencyFetch(ctx, meta, variables)
151+
}
152+
153+
// resourceStackDependencyFetchByStackID fetches a dependency using stack ID (new format)
154+
func resourceStackDependencyFetchByStackID(ctx context.Context, meta any, stackID, dependsOnStackID graphql.ID) (*structs.StackDependency, error) {
155+
variables := map[string]any{
156+
"id": toID(""),
157+
"stackId": stackID,
158+
"dependsOnStackId": dependsOnStackID,
159+
}
160+
return resourceStackDependencyFetch(ctx, meta, variables)
161+
}
162+
163+
func resourceStackDependencyFetch(ctx context.Context, meta any, variables map[string]any) (*structs.StackDependency, error) {
164+
var query struct {
165+
Stack *struct {
166+
Dependency *structs.StackDependency `graphql:"dependency(id: $id, dependsOnStackId: $dependsOnStackId)"`
167+
} `graphql:"stack(id: $stackId)"`
168+
}
169+
170+
if err := meta.(*internal.Client).Query(ctx, "StackDependencyRead", &query, variables); err != nil {
171+
return nil, err
172+
}
173+
174+
if query.Stack == nil {
175+
return nil, fmt.Errorf("stack not found")
176+
}
177+
178+
return query.Stack.Dependency, nil
179+
}
180+
181+
func parseStackDependencyID(id string) (string, string, error) {
182+
idParts := strings.SplitN(id, "/", 2)
183+
if len(idParts) != 2 {
184+
return "", "", fmt.Errorf("unexpected resource ID format: %s", id)
185+
}
120186

121-
return graphql.ID(idParts[1])
187+
return idParts[0], idParts[1], nil
122188
}
123189

124190
func stackDependencyCreateInput(d *schema.ResourceData) structs.StackDependencyInput {

0 commit comments

Comments
 (0)