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. In the latter case, the path is relative to the space the stack for which the current run is executing is in.
TheMacies marked this conversation as resolved.
Show resolved Hide resolved
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. In the latter case, the path is relative to the space the stack for which the current run is executing is in.
TheMacies marked this conversation as resolved.
Show resolved Hide resolved
**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
}

// Assuming this data source is created in a run that belongs to a stack in a space located at following path - "root", then the following data source shall be equal to the one above.
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
}
}

// Assuming this data source is created in a run that belongs to a stack in a space located at following path - "root", then the following data source shall be equal to the one above.
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
32 changes: 26 additions & 6 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. In the latter case, the path is relative to the space the stack for which the current run is executing is in. \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 Down
49 changes: 39 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 @@ -108,7 +117,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 +131,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 +146,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 +161,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 +176,31 @@ 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,
},
}
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