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

Merge main #976

Merged
merged 3 commits into from
Aug 1, 2023
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
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
Loading