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

Add policy commands #276

Merged
merged 1 commit into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading