Skip to content

Commit

Permalink
Add policy commands (#276)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasmik authored Dec 11, 2024
1 parent 3270b0f commit 8ee1f65
Show file tree
Hide file tree
Showing 8 changed files with 622 additions and 0 deletions.
21 changes: 21 additions & 0 deletions internal/cmd/policy/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package policy

import "github.com/urfave/cli/v2"

var flagRequiredPolicyID = &cli.StringFlag{
Name: "id",
Usage: "[Required] `ID` of the policy",
Required: true,
}

var flagRequiredSampleKey = &cli.StringFlag{
Name: "key",
Usage: "[Required] `Key` of the policy sample",
Required: true,
}

var flagSimulationInput = &cli.StringFlag{
Name: "input",
Usage: "[Required] JSON Input of the data provided for policy simlation. Will Attempt to detect if a file is provided",
Required: true,
}
206 changes: 206 additions & 0 deletions internal/cmd/policy/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package policy

import (
"context"
"fmt"
"slices"
"strings"

"github.com/pkg/errors"
"github.com/shurcooL/graphql"
"github.com/spacelift-io/spacectl/client/structs"
"github.com/spacelift-io/spacectl/internal"
"github.com/spacelift-io/spacectl/internal/cmd"
"github.com/spacelift-io/spacectl/internal/cmd/authenticated"
"github.com/urfave/cli/v2"
)

type listCommand struct{}

func (c *listCommand) list(cliCtx *cli.Context) error {
outputFormat, err := cmd.GetOutputFormat(cliCtx)
if err != nil {
return err
}

var limit *uint
if cliCtx.IsSet(cmd.FlagLimit.Name) {
limit = internal.Ptr(cliCtx.Uint(cmd.FlagLimit.Name))
}

var search *string
if cliCtx.IsSet(cmd.FlagSearch.Name) {
search = internal.Ptr(cliCtx.String(cmd.FlagSearch.Name))
}

switch outputFormat {
case cmd.OutputFormatTable:
return c.listTable(cliCtx, search, limit)
case cmd.OutputFormatJSON:
return c.listJSON(cliCtx, search, limit)
}

return fmt.Errorf("unknown output format: %v", outputFormat)
}

func (c *listCommand) listTable(ctx *cli.Context, search *string, limit *uint) error {
var first *graphql.Int
if limit != nil {
first = graphql.NewInt(graphql.Int(*limit)) //nolint: gosec
}

var fullTextSearch *graphql.String
if search != nil {
fullTextSearch = graphql.NewString(graphql.String(*search))
}

input := structs.SearchInput{
First: first,
FullTextSearch: fullTextSearch,
OrderBy: &structs.QueryOrder{
Field: "name",
Direction: "DESC",
},
}

policies, err := c.searchAllPolicies(ctx.Context, input)
if err != nil {
return err
}

columns := []string{"Name", "ID", "Description", "Type", "Space", "Updated At", "Labels"}
tableData := [][]string{columns}

for _, b := range policies {
row := []string{
b.Name,
b.ID,
b.Description,
b.Type,
b.Space.ID,
cmd.HumanizeUnixSeconds(b.UpdatedAt),
strings.Join(b.Labels, ", "),
}
if ctx.Bool(cmd.FlagShowLabels.Name) {
row = append(row, strings.Join(b.Labels, ", "))
}

tableData = append(tableData, row)
}

return cmd.OutputTable(tableData, true)
}

func (c *listCommand) listJSON(ctx *cli.Context, search *string, limit *uint) error {
var first *graphql.Int
if limit != nil {
first = graphql.NewInt(graphql.Int(*limit)) //nolint: gosec
}

var fullTextSearch *graphql.String
if search != nil {
fullTextSearch = graphql.NewString(graphql.String(*search))
}

policies, err := c.searchAllPolicies(ctx.Context, structs.SearchInput{
First: first,
FullTextSearch: fullTextSearch,
})
if err != nil {
return err
}

return cmd.OutputJSON(policies)
}

func (c *listCommand) searchAllPolicies(ctx context.Context, input structs.SearchInput) ([]policyNode, error) {
const maxPageSize = 50

var limit int
if input.First != nil {
limit = int(*input.First)
}
fetchAll := limit == 0

out := []policyNode{}
pageInput := structs.SearchInput{
First: graphql.NewInt(maxPageSize),
FullTextSearch: input.FullTextSearch,
}
for {
if !fetchAll {
// Fetch exactly the number of items requested
pageInput.First = graphql.NewInt(
//nolint: gosec
graphql.Int(
slices.Min([]int{maxPageSize, limit - len(out)}),
),
)
}

result, err := searchPolicies(ctx, pageInput)
if err != nil {
return nil, err
}

out = append(out, result.Policies...)

if result.PageInfo.HasNextPage && (fetchAll || limit > len(out)) {
pageInput.After = graphql.NewString(graphql.String(result.PageInfo.EndCursor))
} else {
break
}
}

return out, nil
}

type policyNode struct {
ID string `graphql:"id" json:"id"`
Name string `graphql:"name" json:"name"`
Description string `graphql:"description" json:"description"`
Body string `graphql:"body" json:"body"`
Space struct {
ID string `graphql:"id" json:"id"`
Name string `graphql:"name" json:"name"`
AccessLevel string `graphql:"accessLevel" json:"accessLevel"`
} `graphql:"spaceDetails" json:"spaceDetails"`
CreatedAt int `graphql:"createdAt" json:"createdAt"`
UpdatedAt int `graphql:"updatedAt" json:"updatedAt"`
Type string `graphql:"type" json:"type"`
Labels []string `graphql:"labels" json:"labels"`
}

type searchPoliciesResult struct {
Policies []policyNode
PageInfo structs.PageInfo
}

func searchPolicies(ctx context.Context, input structs.SearchInput) (searchPoliciesResult, error) {
var query struct {
SearchPoliciesOutput struct {
Edges []struct {
Node policyNode `graphql:"node"`
} `graphql:"edges"`
PageInfo structs.PageInfo `graphql:"pageInfo"`
} `graphql:"searchPolicies(input: $input)"`
}

if err := authenticated.Client.Query(
ctx,
&query,
map[string]interface{}{"input": input},
); err != nil {
return searchPoliciesResult{}, errors.Wrap(err, "failed search for policies")
}

nodes := make([]policyNode, 0)
for _, q := range query.SearchPoliciesOutput.Edges {
nodes = append(nodes, q.Node)
}

return searchPoliciesResult{
Policies: nodes,
PageInfo: query.SearchPoliciesOutput.PageInfo,
}, nil
}
79 changes: 79 additions & 0 deletions internal/cmd/policy/policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package policy

import (
"github.com/spacelift-io/spacectl/internal/cmd"
"github.com/spacelift-io/spacectl/internal/cmd/authenticated"
"github.com/urfave/cli/v2"
)

// Command encapsulates the policyNode command subtree.
func Command() *cli.Command {
return &cli.Command{
Name: "policy",
Usage: "Manage Spacelift policies",
Subcommands: []*cli.Command{
{
Name: "list",
Usage: "List the policies you have access to",
Flags: []cli.Flag{
cmd.FlagOutputFormat,
cmd.FlagLimit,
cmd.FlagSearch,
},
Action: (&listCommand{}).list,
Before: cmd.PerformAllBefore(
cmd.HandleNoColor,
authenticated.Ensure,
),
ArgsUsage: cmd.EmptyArgsUsage,
},
{
Name: "show",
Usage: "Shows detailed information about a specific policy",
Flags: []cli.Flag{
cmd.FlagOutputFormat,
flagRequiredPolicyID,
},
Action: (&showCommand{}).show,
Before: cmd.PerformAllBefore(cmd.HandleNoColor, authenticated.Ensure),
ArgsUsage: cmd.EmptyArgsUsage,
},
{
Name: "samples",
Usage: "List all policy samples",
Flags: []cli.Flag{
cmd.FlagOutputFormat,
cmd.FlagNoColor,
flagRequiredPolicyID,
},
Action: (&samplesCommand{}).list,
Before: cmd.PerformAllBefore(cmd.HandleNoColor, authenticated.Ensure),
ArgsUsage: cmd.EmptyArgsUsage,
},
{
Name: "sample",
Usage: "Inspect one policy sample",
Flags: []cli.Flag{
cmd.FlagNoColor,
flagRequiredPolicyID,
flagRequiredSampleKey,
},
Action: (&sampleCommand{}).show,
Before: cmd.PerformAllBefore(cmd.HandleNoColor, authenticated.Ensure),
ArgsUsage: cmd.EmptyArgsUsage,
},
{
Name: "simulate",
Usage: "Simulate a policy using a sample",
Flags: []cli.Flag{
cmd.FlagNoColor,
flagRequiredPolicyID,
flagSimulationInput,
},
Action: (&simulateCommand{}).simulate,
Before: cmd.PerformAllBefore(cmd.HandleNoColor, authenticated.Ensure),
ArgsUsage: cmd.EmptyArgsUsage,
},
},
}
}
49 changes: 49 additions & 0 deletions internal/cmd/policy/sample.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package policy

import (
"context"

"github.com/pkg/errors"
"github.com/shurcooL/graphql"
"github.com/spacelift-io/spacectl/internal/cmd"
"github.com/spacelift-io/spacectl/internal/cmd/authenticated"
"github.com/urfave/cli/v2"
)

type policyEvaluationSample struct {
Input string `graphql:"input" json:"input"`
Body string `graphql:"body" json:"body"`
}

type sampleCommand struct{}

func (c *sampleCommand) show(cliCtx *cli.Context) error {
policyID := cliCtx.String(flagRequiredPolicyID.Name)
key := cliCtx.String(flagRequiredSampleKey.Name)

b, err := c.getSamplesPolicyByID(cliCtx.Context, policyID, key)
if err != nil {
return errors.Wrapf(err, "failed to query for policyEvaluation ID %q", policyID)
}

return cmd.OutputJSON(b)
}

func (c *sampleCommand) getSamplesPolicyByID(ctx context.Context, policyID, key string) (policyEvaluationSample, error) {
var query struct {
Policy struct {
Sample policyEvaluationSample `graphql:"evaluationSample(key: $key)"`
} `graphql:"policy(id: $policyId)"`
}

variables := map[string]interface{}{
"policyId": graphql.ID(policyID),
"key": graphql.String(key),
}

if err := authenticated.Client.Query(ctx, &query, variables); err != nil {
return policyEvaluationSample{}, errors.Wrapf(err, "failed to query for policyEvaluation ID %q", policyID)
}

return query.Policy.Sample, nil
}
Loading

0 comments on commit 8ee1f65

Please sign in to comment.