Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement querying for spaces using relative paths #576

Merged
merged 13 commits into from
Nov 8, 2024
7 changes: 7 additions & 0 deletions docs/data-sources/space_by_path.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ page_title: "spacelift_space_by_path Data Source - terraform-provider-spacelift"
subcategory: ""
description: |-
spacelift_space_by_path represents a Spacelift space - a collection of resources such as stacks, modules, policies, etc. Allows for more granular access control. Can have a parent space. In contrary to spacelift_space, this resource is identified by a path, not by an ID. For this data source to work, path must be unique. If there are multiple spaces with the same path, this datasource will fail.
This data source can be used either with absolute paths (starting with root) or relative paths. When using a relative path, the path is relative to the current run's space.
Disclaimer:
This datasource can only be used in a stack that resides in a space with inheritance enabled. In addition, the parent spaces (excluding root) must also have inheritance enabled.
---

# spacelift_space_by_path (Data Source)

`spacelift_space_by_path` represents a Spacelift **space** - a collection of resources such as stacks, modules, policies, etc. Allows for more granular access control. Can have a parent space. In contrary to `spacelift_space`, this resource is identified by a path, not by an ID. For this data source to work, path must be unique. If there are multiple spaces with the same path, this datasource will fail.
This data source can be used either with absolute paths (starting with root) or relative paths. When using a relative path, the path is relative to the current run's space.
**Disclaimer:**
This datasource can only be used in a stack that resides in a space with inheritance enabled. In addition, the parent spaces (excluding root) must also have inheritance enabled.

Expand All @@ -24,6 +26,11 @@ data "spacelift_space_by_path" "space" {
output "space_description" {
value = data.spacelift_space_by_path.space.description
}

// The following example shows how to use a relative path. If this is used in a stack in the root space, this is identical to using a path of `root/second space/my space`.
TheMacies marked this conversation as resolved.
Show resolved Hide resolved
data "spacelift_space_by_relative_path" "space" {
space_path = "second space/my space"
}
```

<!-- schema generated by tfplugindocs -->
Expand Down
7 changes: 6 additions & 1 deletion examples/data-sources/spacelift_space_by_path/data-source.tf
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ data "spacelift_space_by_path" "space" {

output "space_description" {
value = data.spacelift_space_by_path.space.description
}
}

// The following example shows how to use a relative path. If this is used in a stack in the root space, this is identical to using a path of `root/second space/my space`.
data "spacelift_space_by_relative_path" "space" {
space_path = "second space/my space"
}
31 changes: 24 additions & 7 deletions spacelift/data_current_space.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package spacelift

import (
"context"
"fmt"
"path"
"strings"

Expand Down Expand Up @@ -54,25 +55,28 @@ func dataCurrentSpace() *schema.Resource {
}
}

func dataCurrentSpaceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
func getStackIDFromToken(token string) (string, error) {
var claims jwt.StandardClaims

_, _, err := (&jwt.Parser{}).ParseUnverified(meta.(*internal.Client).Token, &claims)
_, _, err := (&jwt.Parser{}).ParseUnverified(token, &claims)
if err != nil {
// Don't care about validation errors, we don't actually validate those
// tokens, we only parse them.
var unverifiable *jwt.UnverfiableTokenError
if !errors.As(err, &unverifiable) {
return diag.Errorf("could not parse client token: %v", err)
return "", fmt.Errorf("could not parse client token: %v", err)
}
}

if issuer := claims.Issuer; issuer != "spacelift" {
return diag.Errorf("unexpected token issuer %s, is this a Spacelift run?", issuer)
return "", fmt.Errorf("unexpected token issuer %s, is this a Spacelift run?", issuer)
}

stackID, _ := path.Split(claims.Subject)
return stackID, nil
}

func getSpaceForStack(ctx context.Context, stackID string, meta interface{}) (structs.Space, error) {
var query struct {
Stack *structs.Stack `graphql:"stack(id: $id)"`
Module *structs.Module `graphql:"module(id: $id)"`
Expand All @@ -81,9 +85,9 @@ func dataCurrentSpaceRead(ctx context.Context, d *schema.ResourceData, meta inte
variables := map[string]interface{}{"id": toID(strings.TrimRight(stackID, "/"))}
if err := meta.(*internal.Client).Query(ctx, "StackRead", &query, variables); err != nil {
if strings.Contains(err.Error(), "denied") {
return diag.Errorf("could not query for stack: %v, is this stack administrative?", err)
return structs.Space{}, fmt.Errorf("could not query for stack: %v, is this stack administrative?", err)
}
return diag.Errorf("could not query for stack: %v", err)
return structs.Space{}, fmt.Errorf("could not query for stack: %v", err)
}

var space structs.Space
Expand All @@ -94,7 +98,20 @@ func dataCurrentSpaceRead(ctx context.Context, d *schema.ResourceData, meta inte
case query.Module != nil:
space = query.Module.SpaceDetails
default:
return diag.Errorf("could not find stack or module with ID %s", stackID)
return structs.Space{}, fmt.Errorf("could not find stack or module with ID %s", stackID)
}
return space, nil
}

func dataCurrentSpaceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
stackID, err := getStackIDFromToken(meta.(*internal.Client).Token)
if err != nil {
return diag.Errorf("%v", err)
}

space, err := getSpaceForStack(ctx, stackID, meta)
if err != nil {
return diag.Errorf("%v", err)
}

d.SetId(space.ID)
Expand Down
34 changes: 27 additions & 7 deletions spacelift/data_space_by_path.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func dataSpaceByPath() *schema.Resource {
Description: "`spacelift_space_by_path` represents a Spacelift **space** - " +
"a collection of resources such as stacks, modules, policies, etc. Allows for more granular access control. Can have a parent space. In contrary to `spacelift_space`, this resource is identified by a path, not by an ID. " +
"For this data source to work, path must be unique. If there are multiple spaces with the same path, this datasource will fail. \n" +
"This data source can be used either with absolute paths (starting with root) or relative paths. When using a relative path, the path is relative to the current run's space. \n" +
"**Disclaimer:** \n" +
"This datasource can only be used in a stack that resides in a space with inheritance enabled. In addition, the parent spaces (excluding root) must also have inheritance enabled.",

Expand Down Expand Up @@ -62,8 +63,9 @@ func dataSpaceByPath() *schema.Resource {

func dataSpaceByPathRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
path := d.Get("space_path").(string)
if !strings.HasPrefix(path, "root/") && path != "root" {
return diag.Errorf("space path must start with `root`")

if strings.HasPrefix(path, "/") {
return diag.Errorf("path must not start with a slash")
}

var query struct {
Expand All @@ -74,7 +76,25 @@ func dataSpaceByPathRead(ctx context.Context, d *schema.ResourceData, meta inter
return diag.Errorf("could not query for spaces: %v", err)
}

space, err := findSpaceByPath(query.Spaces, path)
startingSpace := "root"
if !strings.HasPrefix(path, "root/") && path != "root" {
// if path does not start with root, we think it's a relative path. In this case it's relative to the current space the spacelift run is in

stackID, err := getStackIDFromToken(meta.(*internal.Client).Token)
if err != nil {
return diag.Errorf("couldn't identify the run: %v", err)
}

space, err := getSpaceForStack(ctx, stackID, meta)
if err != nil {
return diag.Errorf("couldn't determine current space: %v", err)
}

startingSpace = space.ID
path = space.Name + "/" + path // to be consistent with full path search where root is always included in the path
TheMacies marked this conversation as resolved.
Show resolved Hide resolved
}

space, err := findSpaceByPath(query.Spaces, path, startingSpace)
if err != nil {
return diag.Errorf("error while traversing space path: %v", err)
}
Expand All @@ -97,12 +117,12 @@ func dataSpaceByPathRead(ctx context.Context, d *schema.ResourceData, meta inter
return nil
}

func findSpaceByPath(spaces []*structs.Space, path string) (*structs.Space, error) {
func findSpaceByPath(spaces []*structs.Space, path, startingSpace string) (*structs.Space, error) {
childrenMap := make(map[string][]*structs.Space, len(spaces))
var currentSpace *structs.Space

for _, space := range spaces {
if space.ID == "root" {
if space.ID == startingSpace {
currentSpace = space
}
if space.ParentSpace != nil {
Expand All @@ -111,7 +131,7 @@ func findSpaceByPath(spaces []*structs.Space, path string) (*structs.Space, erro
}

if currentSpace == nil {
return nil, fmt.Errorf("root space not found")
return nil, fmt.Errorf("%v space not found", startingSpace)
}

pathSplit := strings.Split(path, "/")
Expand All @@ -131,7 +151,7 @@ func findSpaceByPath(spaces []*structs.Space, path string) (*structs.Space, erro
}
}
if !found {
return nil, fmt.Errorf("could not find a space identified by path: %s", strings.Join(pathSplit[:i+1], "/"))
return nil, fmt.Errorf("space does not exist")
}
}

Expand Down
68 changes: 58 additions & 10 deletions spacelift/data_space_by_path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,24 +50,33 @@ func TestSpaceByPathData(t *testing.T) {
space_path = "root123"
}
`,
ExpectError: regexp.MustCompile("space path must start with `root`"),
ExpectError: regexp.MustCompile("couldn't identify the run: unexpected token issuer api-key, is this a Spacelift run?"),
},
{
Config: `
data "spacelift_space_by_path" "test" {
space_path = "test123/test"
}
`,
ExpectError: regexp.MustCompile("space path must start with `root`"),
ExpectError: regexp.MustCompile("couldn't identify the run: unexpected token issuer api-key, is this a Spacelift run?"),
},
{
Config: `
data "spacelift_space_by_path" "test" {
space_path = "/my-space"
}
`,
ExpectError: regexp.MustCompile("path must not start with a slash"),
},
})
})
}

func Test_findSpaceByPath(t *testing.T) {
type args struct {
spaces []*structs.Space
path string
spaces []*structs.Space
path string
startingSpace string
}

var root = &structs.Space{
Expand Down Expand Up @@ -95,6 +104,11 @@ func Test_findSpaceByPath(t *testing.T) {
Name: "rootChild",
ParentSpace: &root.ID,
}
var rootGrandchildAmbiguous = &structs.Space{
ID: "rootGrandchild-randomsuffix5",
Name: "rootGrandchild",
ParentSpace: &rootChild.ID,
}

tests := []struct {
name string
Expand All @@ -108,7 +122,8 @@ func Test_findSpaceByPath(t *testing.T) {
spaces: []*structs.Space{
root,
},
path: "root",
startingSpace: "root",
path: "root",
},
want: root,
wantErr: false,
Expand All @@ -121,7 +136,8 @@ func Test_findSpaceByPath(t *testing.T) {
rootChild,
rootChild2,
},
path: "root/rootChild",
startingSpace: "root",
path: "root/rootChild",
},
want: rootChild,
wantErr: false,
Expand All @@ -135,7 +151,8 @@ func Test_findSpaceByPath(t *testing.T) {
rootChild2,
rootChildSameName,
},
path: "root/rootChild",
startingSpace: "root",
path: "root/rootChild",
},
want: nil,
wantErr: true,
Expand All @@ -149,7 +166,8 @@ func Test_findSpaceByPath(t *testing.T) {
rootChild2,
rootGrandchild,
},
path: "root/rootChild/rootGrandchild",
startingSpace: "root",
path: "root/rootChild/rootGrandchild",
},
want: rootGrandchild,
wantErr: false,
Expand All @@ -163,15 +181,45 @@ func Test_findSpaceByPath(t *testing.T) {
rootChild2,
rootGrandchild,
},
path: "root/rootGrandchild",
startingSpace: "root",
path: "root/rootGrandchild",
},
want: nil,
wantErr: true,
},
{
name: "grandchild should be found if starting from child",
args: args{
spaces: []*structs.Space{
root,
rootChild,
rootChild2,
rootGrandchild,
},
startingSpace: rootChild.ID,
path: "rootChild/rootGrandchild",
},
want: rootGrandchild,
wantErr: false,
},
{
name: "ambiguous path should return error",
args: args{
spaces: []*structs.Space{
root,
rootChild,
rootGrandchild,
rootGrandchildAmbiguous,
},
startingSpace: rootChild.ID,
path: "rootChild/rootGrandchild",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := findSpaceByPath(tt.args.spaces, tt.args.path)
got, err := findSpaceByPath(tt.args.spaces, tt.args.path, tt.args.startingSpace)
if (err != nil) != tt.wantErr {
t.Errorf("findSpaceByPath() error = %v, wantErr %v", err, tt.wantErr)
return
Expand Down
Loading