Skip to content

Commit

Permalink
return non-zero status codes for some cases (#778)
Browse files Browse the repository at this point in the history
  • Loading branch information
sagar-connect authored Sep 12, 2022
1 parent 56e791b commit 45afa93
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 16 deletions.
8 changes: 5 additions & 3 deletions api/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"strings"
"time"

"github.com/CircleCI-Public/circle-policy-agent/cpa"

"github.com/CircleCI-Public/circleci-cli/api/header"
"github.com/CircleCI-Public/circleci-cli/settings"
"github.com/CircleCI-Public/circleci-cli/version"
Expand Down Expand Up @@ -215,7 +217,7 @@ type DecisionRequest struct {
}

// MakeDecision sends a requests to Policy-Service public decision endpoint and returns the decision response
func (c Client) MakeDecision(ownerID string, context string, req DecisionRequest) (interface{}, error) {
func (c Client) MakeDecision(ownerID string, context string, req DecisionRequest) (*cpa.Decision, error) {
payload, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
Expand Down Expand Up @@ -244,12 +246,12 @@ func (c Client) MakeDecision(ownerID string, context string, req DecisionRequest
return nil, fmt.Errorf("unexpected status-code: %d - %s", resp.StatusCode, payload.Error)
}

var body interface{}
var body cpa.Decision
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
return nil, fmt.Errorf("failed to decode response body: %w", err)
}

return body, nil
return &body, nil
}

// NewClient returns a new policy client that will use the provided settings.Config to automatically inject appropriate
Expand Down
3 changes: 2 additions & 1 deletion api/policy/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"
"time"

"github.com/CircleCI-Public/circle-policy-agent/cpa"
"gotest.tools/v3/assert"

"github.com/CircleCI-Public/circleci-cli/settings"
Expand Down Expand Up @@ -579,7 +580,7 @@ func TestMakeDecision(t *testing.T) {

_ = json.NewEncoder(w).Encode(map[string]string{"status": "PASS"})
},
ExpectedDecision: map[string]interface{}{"status": "PASS"},
ExpectedDecision: &cpa.Decision{Status: cpa.StatusPass},
},
{
Name: "unexpected status code",
Expand Down
36 changes: 24 additions & 12 deletions cmd/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ func NewCommand(config *settings.Config, preRunE validator.Validator) *cobra.Com
_ = prettyJSONEncoder(cmd.ErrOrStderr()).Encode(diff)
_, _ = io.WriteString(cmd.ErrOrStderr(), "\n")

if !Confirm(cmd.ErrOrStderr(), cmd.InOrStdin(), "Do you wish to continue? (y/N)") {
proceed, err := Confirm(cmd.ErrOrStderr(), cmd.InOrStdin(), "Do you wish to continue? (y/N)")
if err != nil {
return err
}
if !proceed {
return nil
}
_, _ = io.WriteString(cmd.ErrOrStderr(), "\n")
Expand Down Expand Up @@ -210,14 +214,14 @@ func NewCommand(config *settings.Config, preRunE validator.Validator) *cobra.Com
}()
}

var output interface{}
client := policy.NewClient(*policyBaseURL, config)

if decisionID != "" {
output, err = client.GetDecisionLog(ownerID, context, decisionID, policyBundle)
} else {
output, err = getAllDecisionLogs(client, ownerID, context, request, cmd.ErrOrStderr())
}
output, err := func() (interface{}, error) {
if decisionID != "" {
return client.GetDecisionLog(ownerID, context, decisionID, policyBundle)
}
return getAllDecisionLogs(client, ownerID, context, request, cmd.ErrOrStderr())
}()

if err != nil {
return fmt.Errorf("failed to get policy decision logs: %v", err)
Expand Down Expand Up @@ -256,6 +260,7 @@ func NewCommand(config *settings.Config, preRunE validator.Validator) *cobra.Com
metaFile string
ownerID string
context string
strict bool
request policy.DecisionRequest
)

Expand Down Expand Up @@ -286,7 +291,7 @@ func NewCommand(config *settings.Config, preRunE validator.Validator) *cobra.Com
}
}

decision, err := func() (interface{}, error) {
decision, err := func() (*cpa.Decision, error) {
if policyPath != "" {
return getPolicyDecisionLocally(policyPath, input, metadata)
}
Expand All @@ -298,6 +303,10 @@ func NewCommand(config *settings.Config, preRunE validator.Validator) *cobra.Com
return fmt.Errorf("failed to make decision: %w", err)
}

if strict && decision.Status == cpa.StatusHardFail {
return fmt.Errorf("policy decision status: HARD_FAIL")
}

if err := prettyJSONEncoder(cmd.OutOrStdout()).Encode(decision); err != nil {
return fmt.Errorf("failed to encode decision: %w", err)
}
Expand All @@ -312,6 +321,7 @@ func NewCommand(config *settings.Config, preRunE validator.Validator) *cobra.Com
cmd.Flags().StringVar(&context, "context", "config", "policy context for decision")
cmd.Flags().StringVar(&inputPath, "input", "", "path to input file")
cmd.Flags().StringVar(&metaFile, "metafile", "", "decision metadata file")
cmd.Flags().BoolVar(&strict, "strict", false, "return non-zero status code for decision resulting in HARD_FAIL")

if err := cmd.MarkFlagRequired("input"); err != nil {
panic(err)
Expand Down Expand Up @@ -489,12 +499,14 @@ func getAllDecisionLogs(client *policy.Client, ownerID string, context string, r
return allLogs, nil
}

func Confirm(w io.Writer, r io.Reader, question string) bool {
func Confirm(w io.Writer, r io.Reader, question string) (bool, error) {
fmt.Fprint(w, question+" ")
var answer string

_, _ = fmt.Fscanln(r, &answer)

n, err := fmt.Fscanln(r, &answer)
if err != nil || n == 0 {
return false, fmt.Errorf("error in input")
}
answer = strings.ToLower(answer)
return answer == "y" || answer == "yes"
return answer == "y" || answer == "yes", nil
}
58 changes: 58 additions & 0 deletions cmd/policy/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,42 @@ func TestMakeDecisionCommand(t *testing.T) {
},
ExpectedOutput: "{\n \"status\": \"PASS\"\n}\n",
},
{
Name: "passes when decision status = HARD_FAIL AND --strict is OFF",
Args: []string{"decide", "--owner-id", "test-owner", "--input", "./testdata/test1/test.yml"},
ServerHandler: func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, r.Method, "POST")
assert.Equal(t, r.URL.Path, "/api/v1/owner/test-owner/context/config/decision")

var payload map[string]interface{}
assert.NilError(t, json.NewDecoder(r.Body).Decode(&payload))

assert.DeepEqual(t, payload, map[string]interface{}{
"input": "test: config\n",
})

_, _ = io.WriteString(w, `{"status":"HARD_FAIL"}`)
},
ExpectedOutput: "{\n \"status\": \"HARD_FAIL\"\n}\n",
},
{
Name: "fails when decision status = HARD_FAIL AND --strict is ON",
Args: []string{"decide", "--owner-id", "test-owner", "--input", "./testdata/test1/test.yml", "--strict"},
ServerHandler: func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, r.Method, "POST")
assert.Equal(t, r.URL.Path, "/api/v1/owner/test-owner/context/config/decision")

var payload map[string]interface{}
assert.NilError(t, json.NewDecoder(r.Body).Decode(&payload))

assert.DeepEqual(t, payload, map[string]interface{}{
"input": "test: config\n",
})

_, _ = io.WriteString(w, `{"status":"HARD_FAIL"}`)
},
ExpectedErr: "policy decision status: HARD_FAIL",
},
{
Name: "sends expected request with context",
Args: []string{"decide", "--owner-id", "test-owner", "--input", "./testdata/test1/test.yml", "--context", "custom"},
Expand Down Expand Up @@ -687,6 +723,28 @@ func TestMakeDecisionCommand(t *testing.T) {
}
`,
},
{
Name: "successfully performs decision for policy FILE provided locally, passes when decision = HARD_FAIL and strict = OFF",
Args: []string{"decide", "./testdata/test2/hard_fail_policy.rego", "--input", "./testdata/test0/config.yml"},
ExpectedOutput: `{
"status": "HARD_FAIL",
"enabled_rules": [
"always_hard_fails"
],
"hard_failures": [
{
"rule": "always_hard_fails",
"reason": "0 is not equals 1"
}
]
}
`,
},
{
Name: "successfully performs decision for policy FILE provided locally, fails when decision = HARD_FAIL and strict = ON",
Args: []string{"decide", "./testdata/test2/hard_fail_policy.rego", "--input", "./testdata/test0/config.yml", "--strict"},
ExpectedErr: "policy decision status: HARD_FAIL",
},
{
Name: "successfully performs decision with metadata for policy FILE provided locally",
Args: []string{
Expand Down
1 change: 1 addition & 0 deletions cmd/policy/testdata/policy/decide-expected-usage.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Flags:
--input string path to input file
--metafile string decision metadata file
--owner-id string the id of the policy's owner
--strict return non-zero status code for decision resulting in HARD_FAIL

Global Flags:
--policy-base-url string base url for policy api (default "https://internal.circleci.com")
6 changes: 6 additions & 0 deletions cmd/policy/testdata/test2/hard_fail_policy.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org

policy_name["hard_fail_test"]
enable_rule["always_hard_fails"]
hard_fail["always_hard_fails"]
always_hard_fails = "0 is not equals 1" { 0 != 1 }

0 comments on commit 45afa93

Please sign in to comment.