Skip to content

Commit

Permalink
[SNC-392] compile support for policy test command (#973)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmdm authored Jul 31, 2023
1 parent 458f94e commit 0fd0133
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 38 deletions.
25 changes: 17 additions & 8 deletions api/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,36 +326,45 @@ func (c Client) MakeDecision(ownerID string, context string, req DecisionRequest
// NewClient returns a new policy client that will use the provided settings.Config to automatically inject appropriate
// Circle-Token authentication and other relevant CLI headers.
func NewClient(baseURL string, config *settings.Config) *Client {
transport := config.HTTPClient.Transport
client := config.HTTPClient
if client == nil {
client = http.DefaultClient
}

// Make sure to create a copy of the client so that any modifications we make to the transport
// doesn't affect the http.DefaultClient
client = func(c http.Client) *http.Client { return &c }(*client)

transport := client.Transport
if transport == nil {
transport = http.DefaultTransport
}

// Throttling the client so that it cannot make more than 10 concurrent requests at time
sem := make(chan struct{}, 10)

config.HTTPClient.Transport = transportFunc(func(r *http.Request) (*http.Response, error) {
client.Transport = transportFunc(func(r *http.Request) (*http.Response, error) {
// Acquiring semaphore to respect throttling
sem <- struct{}{}

// releasing the semaphore after a second ensuring client doesn't make more than cap(sem)/second
time.AfterFunc(time.Second, func() { <-sem })

if config.Token != "" {
r.Header.Add("circle-token", config.Token)
r.Header.Set("circle-token", config.Token)
}
r.Header.Add("Accept", "application/json")
r.Header.Add("Content-Type", "application/json")
r.Header.Add("User-Agent", version.UserAgent())
r.Header.Set("Accept", "application/json")
r.Header.Set("Content-Type", "application/json")
r.Header.Set("User-Agent", version.UserAgent())
if commandStr := header.GetCommandStr(); commandStr != "" {
r.Header.Add("Circleci-Cli-Command", commandStr)
r.Header.Set("Circleci-Cli-Command", commandStr)
}
return transport.RoundTrip(r)
})

return &Client{
serverUrl: strings.TrimSuffix(baseURL, "/"),
client: config.HTTPClient,
client: client,
}
}

Expand Down
50 changes: 47 additions & 3 deletions cmd/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"io/fs"
"net/url"
"os"
"path/filepath"
"regexp"
Expand All @@ -22,6 +23,7 @@ import (
"gopkg.in/yaml.v3"

"github.com/CircleCI-Public/circleci-cli/api/policy"
"github.com/CircleCI-Public/circleci-cli/api/rest"
"github.com/CircleCI-Public/circleci-cli/cmd/validator"
"github.com/CircleCI-Public/circleci-cli/config"
"github.com/CircleCI-Public/circleci-cli/settings"
Expand Down Expand Up @@ -473,11 +475,13 @@ This group of commands allows the management of polices to be verified against b
debug bool
useJSON bool
format string
ownerID string
)

cmd := &cobra.Command{
Use: "test [path]",
Short: "runs policy tests",
Use: "test [path]",
Short: "runs policy tests",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) (err error) {
var include *regexp.Regexp
if run != "" {
Expand All @@ -490,6 +494,44 @@ This group of commands allows the management of polices to be verified against b
runnerOpts := tester.RunnerOptions{
Path: args[0],
Include: include,
Compile: func(data []byte, pipelineValues map[string]any) ([]byte, error) {
parameters, _ := pipelineValues["parameters"].(map[string]any)
delete(pipelineValues, "parameters")

host := config.GetCompileHost(globalConfig.Host)
client := rest.NewFromConfig(host, globalConfig)

req, err := client.NewRequest(
"POST",
&url.URL{Path: "compile-config-with-defaults"},
config.CompileConfigRequest{
ConfigYaml: string(data),
Options: config.Options{
OwnerID: ownerID,
PipelineValues: pipelineValues,
PipelineParameters: parameters,
},
},
)
if err != nil {
return nil, fmt.Errorf("an error occurred creating the request: %w", err)
}

var resp config.ConfigResponse
if _, err := client.DoRequest(req, &resp); err != nil {
return nil, fmt.Errorf("failed to get compilation response: %w", err)
}

if len(resp.Errors) > 0 {
messages := make([]error, len(resp.Errors))
for i := range resp.Errors {
messages[i] = errors.New(resp.Errors[i].Message)
}
return nil, errors.Join(messages...)
}

return []byte(resp.OutputYaml), nil
},
}

runner, err := tester.NewRunner(runnerOpts)
Expand Down Expand Up @@ -531,8 +573,10 @@ This group of commands allows the management of polices to be verified against b
cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "print all tests instead of only failed tests")
cmd.Flags().BoolVar(&debug, "debug", false, "print test debug context. Sets verbose to true")
cmd.Flags().BoolVar(&useJSON, "json", false, "sprints json test results instead of standard output format")
_ = cmd.Flags().MarkDeprecated("json", "use --format=json to print json test results")
cmd.Flags().StringVar(&format, "format", "", "select desired format between json or junit")
cmd.Flags().StringVar(&ownerID, "owner-id", "", "the id of the policy's owner")

_ = cmd.Flags().MarkDeprecated("json", "use --format=json to print json test results")
return cmd
}()

Expand Down
49 changes: 37 additions & 12 deletions cmd/policy/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func TestPushPolicyBundleNoPrompt(t *testing.T) {
svr := httptest.NewServer(tc.ServerHandler)
defer svr.Close()

cmd, stdout, stderr := makeCMD("")
cmd, stdout, stderr := makeCMD("", "testtoken")

cmd.SetArgs(append(tc.Args, "--policy-base-url", svr.URL, "--no-prompt"))

Expand Down Expand Up @@ -266,7 +266,7 @@ func TestDiffPolicyBundle(t *testing.T) {
svr := httptest.NewServer(tc.ServerHandler)
defer svr.Close()

cmd, stdout, stderr := makeCMD("")
cmd, stdout, stderr := makeCMD("", "testtoken")

cmd.SetArgs(append(tc.Args, "--policy-base-url", svr.URL))

Expand Down Expand Up @@ -382,7 +382,7 @@ func TestFetchPolicyBundle(t *testing.T) {
svr := httptest.NewServer(tc.ServerHandler)
defer svr.Close()

cmd, stdout, _ := makeCMD("")
cmd, stdout, _ := makeCMD("", "testtoken")

cmd.SetArgs(append(tc.Args, "--policy-base-url", svr.URL))

Expand Down Expand Up @@ -567,7 +567,7 @@ func TestGetDecisionLogs(t *testing.T) {
svr := httptest.NewServer(tc.ServerHandler)
defer svr.Close()

cmd, stdout, _ := makeCMD("")
cmd, stdout, _ := makeCMD("", "testtoken")

cmd.SetArgs(append(tc.Args, "--policy-base-url", svr.URL))

Expand Down Expand Up @@ -637,7 +637,7 @@ test: config
err := json.NewDecoder(r.Body).Decode(&req)
require.NoError(t, err)

//dummy compilation here (remove the _compiled_ key in compiled config, as compiled config can't have that at top-level key).
// dummy compilation here (remove the _compiled_ key in compiled config, as compiled config can't have that at top-level key).
var yamlResp map[string]any
err = yaml.Unmarshal([]byte(req.ConfigYaml), &yamlResp)
require.NoError(t, err)
Expand Down Expand Up @@ -927,7 +927,7 @@ test: config
compilerServer := httptest.NewServer(tc.CompilerServerHandler)
defer compilerServer.Close()

cmd, stdout, _ := makeCMD(compilerServer.URL)
cmd, stdout, _ := makeCMD(compilerServer.URL, "testtoken")

cmd.SetArgs(append(tc.Args, "--policy-base-url", svr.URL))

Expand Down Expand Up @@ -1118,7 +1118,7 @@ test: config
compilerServer := httptest.NewServer(tc.CompilerServerHandler)
defer compilerServer.Close()

cmd, stdout, _ := makeCMD(compilerServer.URL)
cmd, stdout, _ := makeCMD(compilerServer.URL, "testtoken")

args := append(tc.Args, "--policy-base-url", svr.URL)

Expand Down Expand Up @@ -1234,7 +1234,7 @@ func TestGetSetSettings(t *testing.T) {
svr := httptest.NewServer(tc.ServerHandler)
defer svr.Close()

cmd, stdout, _ := makeCMD("")
cmd, stdout, _ := makeCMD("", "testtoken")

cmd.SetArgs(append(tc.Args, "--policy-base-url", svr.URL))

Expand All @@ -1256,6 +1256,7 @@ const jsonDeprecationMessage = "Flag --json has been deprecated, use --format=js
func TestTestRunner(t *testing.T) {
cases := []struct {
Name string
Path string
Verbose bool
Debug bool
Run string
Expand Down Expand Up @@ -1330,13 +1331,31 @@ func TestTestRunner(t *testing.T) {
assert.Contains(t, s, "<?xml")
},
},
{
Name: "compile",
Path: "./testdata/compile_policies",
Verbose: false,
Debug: false,
Run: "",
Json: false,
Format: "json",
Expected: func(t *testing.T, s string) {
require.Contains(t, s, `"Passed": true`)
require.Contains(t, s, `"Name": "test_compile_policy"`)
},
},
}

for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
cmd, stdout, _ := makeCMD("")
cmd, stdout, _ := makeCMD("", "")

args := []string{"test", "./testdata/test_policies"}
path := tc.Path
if path == "" {
path = "./testdata/test_policies"
}

args := []string{"test", path}
if tc.Verbose {
args = append(args, "-v")
}
Expand All @@ -1361,8 +1380,14 @@ func TestTestRunner(t *testing.T) {
}
}

func makeCMD(circleHost string) (*cobra.Command, *bytes.Buffer, *bytes.Buffer) {
config := &settings.Config{Host: circleHost, Token: "testtoken", HTTPClient: http.DefaultClient}
func makeCMD(circleHost string, token string) (*cobra.Command, *bytes.Buffer, *bytes.Buffer) {
config := &settings.Config{
Host: circleHost,
Token: token,
RestEndpoint: "/api/v2",
HTTPClient: http.DefaultClient,
}

cmd := NewCommand(config, nil)

stdout := new(bytes.Buffer)
Expand Down
13 changes: 13 additions & 0 deletions cmd/policy/testdata/compile_policies/compile.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org

import future.keywords

policy_name["example_compiled"]

enable_hard["enforce_small_jobs"]

enforce_small_jobs[reason] {
some job_name, job in input._compiled_.jobs
job.resource_class != "small"
reason = sprintf("job %s: resource_class must be small", [job_name])
}
25 changes: 25 additions & 0 deletions cmd/policy/testdata/compile_policies/compile_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
test_compile_policy:
compile: true
pipeline_parameters:
parameters:
size: small
input:
version: 2.1
parameters:
size:
type: string
default: medium
jobs:
test:
docker:
- image: go
resource_class: << pipeline.parameters.size >>
steps:
- run: it
workflows:
main:
jobs:
- test
decision:
status: PASS
enabled_rules: [enforce_small_jobs]
9 changes: 5 additions & 4 deletions cmd/policy/testdata/policy/test-expected-usage.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ Examples:
circleci policy test ./policies/...

Flags:
--debug print test debug context. Sets verbose to true
--format string select desired format between json or junit
--run string select which tests to run based on regular expression
-v, --verbose print all tests instead of only failed tests
--debug print test debug context. Sets verbose to true
--format string select desired format between json or junit
--owner-id string the id of the policy's owner
--run string select which tests to run based on regular expression
-v, --verbose print all tests instead of only failed tests

Global Flags:
--policy-base-url string base url for policy api (default "https://internal.circleci.com")
7 changes: 3 additions & 4 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ type ConfigCompiler struct {
}

func New(cfg *settings.Config) *ConfigCompiler {
hostValue := getCompileHost(cfg.Host)
hostValue := GetCompileHost(cfg.Host)
collaboratorsClient, err := collaborators.NewCollaboratorsRestClient(*cfg)

if err != nil {
panic(err)
}
Expand All @@ -49,8 +48,8 @@ func New(cfg *settings.Config) *ConfigCompiler {
return configCompiler
}

func getCompileHost(cfgHost string) string {
if cfgHost != defaultHost {
func GetCompileHost(cfgHost string) string {
if cfgHost != defaultHost && cfgHost != "" {
return cfgHost
} else {
return defaultAPIHost
Expand Down
6 changes: 2 additions & 4 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ func TestCompiler(t *testing.T) {
t.Run("tests that we correctly get the config api host when the host is not the default one", func(t *testing.T) {
// if the host isn't equal to `https://circleci.com` then this is likely a server instance and
// wont have the api.X.com subdomain so we should instead just respect the host for config commands
host := getCompileHost("test")
host := GetCompileHost("test")
assert.Equal(t, host, "test")

// If the host passed in is the same as the defaultHost 'https://circleci.com' - then we know this is cloud
// and as such should use the `api.circleci.com` subdomain
host = getCompileHost("https://circleci.com")
host = GetCompileHost("https://circleci.com")
assert.Equal(t, host, "https://api.circleci.com")
})
})
Expand Down Expand Up @@ -119,9 +119,7 @@ func TestCompiler(t *testing.T) {
assert.Equal(t, "output", resp.OutputYaml)
assert.Equal(t, "source", resp.SourceYaml)
})

})

}

func TestLoadYaml(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module github.com/CircleCI-Public/circleci-cli

require (
github.com/AlecAivazis/survey/v2 v2.1.1
github.com/CircleCI-Public/circle-policy-agent v0.0.663
github.com/CircleCI-Public/circle-policy-agent v0.0.683
github.com/Masterminds/semver v1.4.2
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/blang/semver v3.5.1+incompatible
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
github.com/AlecAivazis/survey/v2 v2.1.1 h1:LEMbHE0pLj75faaVEKClEX1TM4AJmmnOh9eimREzLWI=
github.com/AlecAivazis/survey/v2 v2.1.1/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk=
github.com/CircleCI-Public/circle-policy-agent v0.0.663 h1:v2DbMYrzcoO6x5KN8y7hxByXMdUjntm8eM5DGnXhKeg=
github.com/CircleCI-Public/circle-policy-agent v0.0.663/go.mod h1:72U4Q4OtvAGRGGo/GqlCCO0tARg1cSG9xwxWyz3ktQI=
github.com/CircleCI-Public/circle-policy-agent v0.0.683 h1:EzZaLy9mUGl4dwDNWceBHeDb3X0KAAjV4eFOk3C7lts=
github.com/CircleCI-Public/circle-policy-agent v0.0.683/go.mod h1:72U4Q4OtvAGRGGo/GqlCCO0tARg1cSG9xwxWyz3ktQI=
github.com/CircleCI-Public/circleci-config v0.0.0-20230609135034-182164ce950a h1:RqA4H9p77FsqV++HNNDBq8dJftYuJ+r+KdD9HAX28t4=
github.com/CircleCI-Public/circleci-config v0.0.0-20230609135034-182164ce950a/go.mod h1:XZaQPj2ylXZaz5vW31dRdkUY/Ey8MdpbgrUHbHyzICY=
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
Expand Down

0 comments on commit 0fd0133

Please sign in to comment.