Skip to content

Commit 8ee1f65

Browse files
authored
Add policy commands (#276)
1 parent 3270b0f commit 8ee1f65

File tree

8 files changed

+622
-0
lines changed

8 files changed

+622
-0
lines changed

internal/cmd/policy/flags.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package policy
2+
3+
import "github.com/urfave/cli/v2"
4+
5+
var flagRequiredPolicyID = &cli.StringFlag{
6+
Name: "id",
7+
Usage: "[Required] `ID` of the policy",
8+
Required: true,
9+
}
10+
11+
var flagRequiredSampleKey = &cli.StringFlag{
12+
Name: "key",
13+
Usage: "[Required] `Key` of the policy sample",
14+
Required: true,
15+
}
16+
17+
var flagSimulationInput = &cli.StringFlag{
18+
Name: "input",
19+
Usage: "[Required] JSON Input of the data provided for policy simlation. Will Attempt to detect if a file is provided",
20+
Required: true,
21+
}

internal/cmd/policy/list.go

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package policy
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"slices"
7+
"strings"
8+
9+
"github.com/pkg/errors"
10+
"github.com/shurcooL/graphql"
11+
"github.com/spacelift-io/spacectl/client/structs"
12+
"github.com/spacelift-io/spacectl/internal"
13+
"github.com/spacelift-io/spacectl/internal/cmd"
14+
"github.com/spacelift-io/spacectl/internal/cmd/authenticated"
15+
"github.com/urfave/cli/v2"
16+
)
17+
18+
type listCommand struct{}
19+
20+
func (c *listCommand) list(cliCtx *cli.Context) error {
21+
outputFormat, err := cmd.GetOutputFormat(cliCtx)
22+
if err != nil {
23+
return err
24+
}
25+
26+
var limit *uint
27+
if cliCtx.IsSet(cmd.FlagLimit.Name) {
28+
limit = internal.Ptr(cliCtx.Uint(cmd.FlagLimit.Name))
29+
}
30+
31+
var search *string
32+
if cliCtx.IsSet(cmd.FlagSearch.Name) {
33+
search = internal.Ptr(cliCtx.String(cmd.FlagSearch.Name))
34+
}
35+
36+
switch outputFormat {
37+
case cmd.OutputFormatTable:
38+
return c.listTable(cliCtx, search, limit)
39+
case cmd.OutputFormatJSON:
40+
return c.listJSON(cliCtx, search, limit)
41+
}
42+
43+
return fmt.Errorf("unknown output format: %v", outputFormat)
44+
}
45+
46+
func (c *listCommand) listTable(ctx *cli.Context, search *string, limit *uint) error {
47+
var first *graphql.Int
48+
if limit != nil {
49+
first = graphql.NewInt(graphql.Int(*limit)) //nolint: gosec
50+
}
51+
52+
var fullTextSearch *graphql.String
53+
if search != nil {
54+
fullTextSearch = graphql.NewString(graphql.String(*search))
55+
}
56+
57+
input := structs.SearchInput{
58+
First: first,
59+
FullTextSearch: fullTextSearch,
60+
OrderBy: &structs.QueryOrder{
61+
Field: "name",
62+
Direction: "DESC",
63+
},
64+
}
65+
66+
policies, err := c.searchAllPolicies(ctx.Context, input)
67+
if err != nil {
68+
return err
69+
}
70+
71+
columns := []string{"Name", "ID", "Description", "Type", "Space", "Updated At", "Labels"}
72+
tableData := [][]string{columns}
73+
74+
for _, b := range policies {
75+
row := []string{
76+
b.Name,
77+
b.ID,
78+
b.Description,
79+
b.Type,
80+
b.Space.ID,
81+
cmd.HumanizeUnixSeconds(b.UpdatedAt),
82+
strings.Join(b.Labels, ", "),
83+
}
84+
if ctx.Bool(cmd.FlagShowLabels.Name) {
85+
row = append(row, strings.Join(b.Labels, ", "))
86+
}
87+
88+
tableData = append(tableData, row)
89+
}
90+
91+
return cmd.OutputTable(tableData, true)
92+
}
93+
94+
func (c *listCommand) listJSON(ctx *cli.Context, search *string, limit *uint) error {
95+
var first *graphql.Int
96+
if limit != nil {
97+
first = graphql.NewInt(graphql.Int(*limit)) //nolint: gosec
98+
}
99+
100+
var fullTextSearch *graphql.String
101+
if search != nil {
102+
fullTextSearch = graphql.NewString(graphql.String(*search))
103+
}
104+
105+
policies, err := c.searchAllPolicies(ctx.Context, structs.SearchInput{
106+
First: first,
107+
FullTextSearch: fullTextSearch,
108+
})
109+
if err != nil {
110+
return err
111+
}
112+
113+
return cmd.OutputJSON(policies)
114+
}
115+
116+
func (c *listCommand) searchAllPolicies(ctx context.Context, input structs.SearchInput) ([]policyNode, error) {
117+
const maxPageSize = 50
118+
119+
var limit int
120+
if input.First != nil {
121+
limit = int(*input.First)
122+
}
123+
fetchAll := limit == 0
124+
125+
out := []policyNode{}
126+
pageInput := structs.SearchInput{
127+
First: graphql.NewInt(maxPageSize),
128+
FullTextSearch: input.FullTextSearch,
129+
}
130+
for {
131+
if !fetchAll {
132+
// Fetch exactly the number of items requested
133+
pageInput.First = graphql.NewInt(
134+
//nolint: gosec
135+
graphql.Int(
136+
slices.Min([]int{maxPageSize, limit - len(out)}),
137+
),
138+
)
139+
}
140+
141+
result, err := searchPolicies(ctx, pageInput)
142+
if err != nil {
143+
return nil, err
144+
}
145+
146+
out = append(out, result.Policies...)
147+
148+
if result.PageInfo.HasNextPage && (fetchAll || limit > len(out)) {
149+
pageInput.After = graphql.NewString(graphql.String(result.PageInfo.EndCursor))
150+
} else {
151+
break
152+
}
153+
}
154+
155+
return out, nil
156+
}
157+
158+
type policyNode struct {
159+
ID string `graphql:"id" json:"id"`
160+
Name string `graphql:"name" json:"name"`
161+
Description string `graphql:"description" json:"description"`
162+
Body string `graphql:"body" json:"body"`
163+
Space struct {
164+
ID string `graphql:"id" json:"id"`
165+
Name string `graphql:"name" json:"name"`
166+
AccessLevel string `graphql:"accessLevel" json:"accessLevel"`
167+
} `graphql:"spaceDetails" json:"spaceDetails"`
168+
CreatedAt int `graphql:"createdAt" json:"createdAt"`
169+
UpdatedAt int `graphql:"updatedAt" json:"updatedAt"`
170+
Type string `graphql:"type" json:"type"`
171+
Labels []string `graphql:"labels" json:"labels"`
172+
}
173+
174+
type searchPoliciesResult struct {
175+
Policies []policyNode
176+
PageInfo structs.PageInfo
177+
}
178+
179+
func searchPolicies(ctx context.Context, input structs.SearchInput) (searchPoliciesResult, error) {
180+
var query struct {
181+
SearchPoliciesOutput struct {
182+
Edges []struct {
183+
Node policyNode `graphql:"node"`
184+
} `graphql:"edges"`
185+
PageInfo structs.PageInfo `graphql:"pageInfo"`
186+
} `graphql:"searchPolicies(input: $input)"`
187+
}
188+
189+
if err := authenticated.Client.Query(
190+
ctx,
191+
&query,
192+
map[string]interface{}{"input": input},
193+
); err != nil {
194+
return searchPoliciesResult{}, errors.Wrap(err, "failed search for policies")
195+
}
196+
197+
nodes := make([]policyNode, 0)
198+
for _, q := range query.SearchPoliciesOutput.Edges {
199+
nodes = append(nodes, q.Node)
200+
}
201+
202+
return searchPoliciesResult{
203+
Policies: nodes,
204+
PageInfo: query.SearchPoliciesOutput.PageInfo,
205+
}, nil
206+
}

internal/cmd/policy/policy.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package policy
2+
3+
import (
4+
"github.com/spacelift-io/spacectl/internal/cmd"
5+
"github.com/spacelift-io/spacectl/internal/cmd/authenticated"
6+
"github.com/urfave/cli/v2"
7+
)
8+
9+
// Command encapsulates the policyNode command subtree.
10+
func Command() *cli.Command {
11+
return &cli.Command{
12+
Name: "policy",
13+
Usage: "Manage Spacelift policies",
14+
Subcommands: []*cli.Command{
15+
{
16+
Name: "list",
17+
Usage: "List the policies you have access to",
18+
Flags: []cli.Flag{
19+
cmd.FlagOutputFormat,
20+
cmd.FlagLimit,
21+
cmd.FlagSearch,
22+
},
23+
Action: (&listCommand{}).list,
24+
Before: cmd.PerformAllBefore(
25+
cmd.HandleNoColor,
26+
authenticated.Ensure,
27+
),
28+
ArgsUsage: cmd.EmptyArgsUsage,
29+
},
30+
{
31+
Name: "show",
32+
Usage: "Shows detailed information about a specific policy",
33+
Flags: []cli.Flag{
34+
cmd.FlagOutputFormat,
35+
flagRequiredPolicyID,
36+
},
37+
Action: (&showCommand{}).show,
38+
Before: cmd.PerformAllBefore(cmd.HandleNoColor, authenticated.Ensure),
39+
ArgsUsage: cmd.EmptyArgsUsage,
40+
},
41+
{
42+
Name: "samples",
43+
Usage: "List all policy samples",
44+
Flags: []cli.Flag{
45+
cmd.FlagOutputFormat,
46+
cmd.FlagNoColor,
47+
flagRequiredPolicyID,
48+
},
49+
Action: (&samplesCommand{}).list,
50+
Before: cmd.PerformAllBefore(cmd.HandleNoColor, authenticated.Ensure),
51+
ArgsUsage: cmd.EmptyArgsUsage,
52+
},
53+
{
54+
Name: "sample",
55+
Usage: "Inspect one policy sample",
56+
Flags: []cli.Flag{
57+
cmd.FlagNoColor,
58+
flagRequiredPolicyID,
59+
flagRequiredSampleKey,
60+
},
61+
Action: (&sampleCommand{}).show,
62+
Before: cmd.PerformAllBefore(cmd.HandleNoColor, authenticated.Ensure),
63+
ArgsUsage: cmd.EmptyArgsUsage,
64+
},
65+
{
66+
Name: "simulate",
67+
Usage: "Simulate a policy using a sample",
68+
Flags: []cli.Flag{
69+
cmd.FlagNoColor,
70+
flagRequiredPolicyID,
71+
flagSimulationInput,
72+
},
73+
Action: (&simulateCommand{}).simulate,
74+
Before: cmd.PerformAllBefore(cmd.HandleNoColor, authenticated.Ensure),
75+
ArgsUsage: cmd.EmptyArgsUsage,
76+
},
77+
},
78+
}
79+
}

internal/cmd/policy/sample.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package policy
2+
3+
import (
4+
"context"
5+
6+
"github.com/pkg/errors"
7+
"github.com/shurcooL/graphql"
8+
"github.com/spacelift-io/spacectl/internal/cmd"
9+
"github.com/spacelift-io/spacectl/internal/cmd/authenticated"
10+
"github.com/urfave/cli/v2"
11+
)
12+
13+
type policyEvaluationSample struct {
14+
Input string `graphql:"input" json:"input"`
15+
Body string `graphql:"body" json:"body"`
16+
}
17+
18+
type sampleCommand struct{}
19+
20+
func (c *sampleCommand) show(cliCtx *cli.Context) error {
21+
policyID := cliCtx.String(flagRequiredPolicyID.Name)
22+
key := cliCtx.String(flagRequiredSampleKey.Name)
23+
24+
b, err := c.getSamplesPolicyByID(cliCtx.Context, policyID, key)
25+
if err != nil {
26+
return errors.Wrapf(err, "failed to query for policyEvaluation ID %q", policyID)
27+
}
28+
29+
return cmd.OutputJSON(b)
30+
}
31+
32+
func (c *sampleCommand) getSamplesPolicyByID(ctx context.Context, policyID, key string) (policyEvaluationSample, error) {
33+
var query struct {
34+
Policy struct {
35+
Sample policyEvaluationSample `graphql:"evaluationSample(key: $key)"`
36+
} `graphql:"policy(id: $policyId)"`
37+
}
38+
39+
variables := map[string]interface{}{
40+
"policyId": graphql.ID(policyID),
41+
"key": graphql.String(key),
42+
}
43+
44+
if err := authenticated.Client.Query(ctx, &query, variables); err != nil {
45+
return policyEvaluationSample{}, errors.Wrapf(err, "failed to query for policyEvaluation ID %q", policyID)
46+
}
47+
48+
return query.Policy.Sample, nil
49+
}

0 commit comments

Comments
 (0)