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

feat: added support for Azure DevOps output #853

Merged
merged 1 commit into from
Aug 20, 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
16 changes: 16 additions & 0 deletions docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ As of today Conftest supports the following output types:
- Table `--output=table`
- JUnit `--output=junit`
- GitHub `--output=github`
- AzureDevOps `--output=azuredevops`

### Plaintext

Expand Down Expand Up @@ -306,6 +307,21 @@ jobs:
conftest test -o github -p examples/kubernetes/policy examples/kubernetes/deployment.yaml
```

### Azure DevOps

```console
$ conftest test -o azuredevops -p examples/kubernetes/policy examples/kubernetes/deployment.yaml
##[section]Testing 'examples/kubernetes/deployment.yaml' against 5 policies in namespace 'main'
##[group]See conftest results
##vso[task.logissue type=error] file=examples/kubernetes/deployment.yaml --> Containers must not run as root in Deployment hello-kubernetes
##vso[task.logissue type=error] file=examples/kubernetes/deployment.yaml --> Deployment hello-kubernetes must provide app/release labels for pod selectors
##vso[task.logissue type=error] file=examples/kubernetes/deployment.yaml --> hello-kubernetes must include Kubernetes recommended labels: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels
##vso[task.logissue type=error] file=examples/kubernetes/deployment.yaml --> Found deployment hello-kubernetes but deployments are not allowed
success file=examples/kubernetes/deployment.yaml 1
##[endgroup]
5 tests, 1 passed, 0 warnings, 4 failures, 0 exceptions
```

## `--parser`

Conftest normally detects which parser to used based on the file extension of the file, even when multiple input files are passed in. However, it is possible force a specific parser to be used with the `--parser` flag.
Expand Down
104 changes: 104 additions & 0 deletions output/azuredevops.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package output

import (
"fmt"
"io"

"github.com/open-policy-agent/opa/tester"
)

// AzureDevOps represents an Outputter that outputs
// results in AzureDevOps Pipelines format.
// https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands
type AzureDevOps struct {
writer io.Writer
}

// NewAzureDevOps creates a new AzureDevOps with the given writer.
func NewAzureDevOps(w io.Writer) *AzureDevOps {
azuredevops := AzureDevOps{
writer: w,
}

return &azuredevops
}

// Output outputs the results.
func (t *AzureDevOps) Output(checkResults []CheckResult) error {
var totalFailures int
var totalExceptions int
var totalWarnings int
var totalSuccesses int
var totalSkipped int

for _, result := range checkResults {
totalPolicies := result.Successes + len(result.Failures) + len(result.Warnings) + len(result.Exceptions) + len(result.Skipped)

fmt.Fprintf(t.writer, "##[section]Testing '%v' against %v policies in namespace '%v'\n", result.FileName, totalPolicies, result.Namespace)
fmt.Fprintf(t.writer, "##[group]See conftest results\n")
for _, failure := range result.Failures {
fmt.Fprintf(t.writer, "##vso[task.logissue type=error] file=%v --> %v\n", result.FileName, failure.Message)
}

for _, warning := range result.Warnings {
fmt.Fprintf(t.writer, "##vso[task.logissue type=warning] file=%v --> %v\n", result.FileName, warning.Message)
}

for _, exception := range result.Exceptions {
fmt.Fprintf(t.writer, "##vso[task.logissuetype=warning] file=%v --> %v\n", result.FileName, exception.Message)
}

for _, skipped := range result.Skipped {
fmt.Fprintf(t.writer, "skipped file=%v %v\n", result.FileName, skipped.Message)
}

if result.Successes > 0 {
fmt.Fprintf(t.writer, "success file=%v %v\n", result.FileName, result.Successes)
}

totalFailures += len(result.Failures)
totalExceptions += len(result.Exceptions)
totalWarnings += len(result.Warnings)
totalSkipped += len(result.Skipped)
totalSuccesses += result.Successes

fmt.Fprintf(t.writer, "##[endgroup]\n")
}

totalTests := totalFailures + totalExceptions + totalWarnings + totalSuccesses + totalSkipped

var pluralSuffixTests string
if totalTests != 1 {
pluralSuffixTests = "s"
}

var pluralSuffixWarnings string
if totalWarnings != 1 {
pluralSuffixWarnings = "s"
}

var pluralSuffixFailures string
if totalFailures != 1 {
pluralSuffixFailures = "s"
}

var pluralSuffixExceptions string
if totalExceptions != 1 {
pluralSuffixExceptions = "s"
}

outputText := fmt.Sprintf("%v test%s, %v passed, %v warning%s, %v failure%s, %v exception%s",
totalTests, pluralSuffixTests,
totalSuccesses,
totalWarnings, pluralSuffixWarnings,
totalFailures, pluralSuffixFailures,
totalExceptions, pluralSuffixExceptions,
)
fmt.Fprintln(t.writer, outputText)

return nil
}

func (t *AzureDevOps) Report(_ []*tester.Result, _ string) error {
return fmt.Errorf("report is not supported in AzureDevOps output")
}
107 changes: 107 additions & 0 deletions output/azuredevops_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package output

import (
"bytes"
"strings"
"testing"
)

func TestAzureDevOps(t *testing.T) {
tests := []struct {
name string
input []CheckResult
expected []string
}{
{
name: "no warnings or errors",
input: []CheckResult{
{
FileName: "examples/kubernetes/service.yaml",
Namespace: "namespace",
},
},
expected: []string{
"##[section]Testing 'examples/kubernetes/service.yaml' against 0 policies in namespace 'namespace'",
"##[group]See conftest results",
"##[endgroup]",
"0 tests, 0 passed, 0 warnings, 0 failures, 0 exceptions",
"",
},
},
{
name: "records failure and warnings",
input: []CheckResult{
{
FileName: "examples/kubernetes/service.yaml",
Namespace: "namespace",
Warnings: []Result{{Message: "first warning"}},
Failures: []Result{{Message: "first failure"}},
},
},
expected: []string{
"##[section]Testing 'examples/kubernetes/service.yaml' against 2 policies in namespace 'namespace'",
"##[group]See conftest results",
"##vso[task.logissue type=error] file=examples/kubernetes/service.yaml --> first failure",
"##vso[task.logissue type=warning] file=examples/kubernetes/service.yaml --> first warning",
"##[endgroup]",
"2 tests, 0 passed, 1 warning, 1 failure, 0 exceptions",
"",
},
},
{
name: "mixed failure, warnings and skipped",
input: []CheckResult{
{
FileName: "examples/kubernetes/service.yaml",
Namespace: "namespace",
Failures: []Result{{Message: "first failure"}},
Skipped: []Result{{Message: "first skipped"}},
},
},
expected: []string{
"##[section]Testing 'examples/kubernetes/service.yaml' against 2 policies in namespace 'namespace'",
"##[group]See conftest results",
"##vso[task.logissue type=error] file=examples/kubernetes/service.yaml --> first failure",
"skipped file=examples/kubernetes/service.yaml first skipped",
"##[endgroup]",
"2 tests, 0 passed, 0 warnings, 1 failure, 0 exceptions",
"",
},
},
{
name: "handles stdin input",
input: []CheckResult{
{
FileName: "-",
Namespace: "namespace",
Failures: []Result{{Message: "first failure"}},
},
},
expected: []string{
"##[section]Testing '-' against 1 policies in namespace 'namespace'",
"##[group]See conftest results",
"##vso[task.logissue type=error] file=- --> first failure",
"##[endgroup]",
"1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions",
"",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
expected := strings.Join(tt.expected, "\n")

buf := new(bytes.Buffer)
if err := NewAzureDevOps(buf).Output(tt.input); err != nil {
t.Fatal("output Azure DevOps:", err)
}

actual := buf.String()

if expected != actual {
t.Errorf("unexpected output. expected %v actual %v", expected, actual)
}
})
}
}
15 changes: 9 additions & 6 deletions output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ type Options struct {
// The defined output formats represent all of the supported formats
// that can be used to format and render results.
const (
OutputStandard = "stdout"
OutputJSON = "json"
OutputTAP = "tap"
OutputTable = "table"
OutputJUnit = "junit"
OutputGitHub = "github"
OutputStandard = "stdout"
OutputJSON = "json"
OutputTAP = "tap"
OutputTable = "table"
OutputJUnit = "junit"
OutputGitHub = "github"
OutputAzureDevOps = "azuredevops"
)

// Get returns a type that can render output in the given format.
Expand All @@ -49,6 +50,8 @@ func Get(format string, options Options) Outputter {
return NewJUnit(os.Stdout, options.JUnitHideMessage)
case OutputGitHub:
return NewGitHub(os.Stdout)
case OutputAzureDevOps:
return NewAzureDevOps(os.Stdout)
default:
return NewStandard(os.Stdout)
}
Expand Down
Loading