diff --git a/api/types/pipeline.go b/api/types/pipeline.go index dd0ff617d..6fcb2744c 100644 --- a/api/types/pipeline.go +++ b/api/types/pipeline.go @@ -24,6 +24,7 @@ type Pipeline struct { Stages *bool `json:"stages,omitempty"` Steps *bool `json:"steps,omitempty"` Templates *bool `json:"templates,omitempty"` + TestReport *bool `json:"test_report,omitempty"` Warnings *[]string `json:"warnings,omitempty"` // swagger:strfmt base64 Data *[]byte `json:"data,omitempty"` @@ -237,6 +238,19 @@ func (p *Pipeline) GetData() []byte { return *p.Data } +// GetTestReport returns the TestReport results field. +// +// When the provided Pipeline type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (p *Pipeline) GetTestReport() bool { + // return zero value if Pipeline type or TestReport field is nil + if p == nil || p.TestReport == nil { + return false + } + + return *p.TestReport +} + // SetID sets the ID field. // // When the provided Pipeline type is nil, it @@ -419,6 +433,19 @@ func (p *Pipeline) SetTemplates(v bool) { p.Templates = &v } +// SetTestReport sets the TestReport field. +// +// When the provided Pipeline type is nil, it +// will set nothing and immediately return. +func (p *Pipeline) SetTestReport(v bool) { + // return if Pipeline type is nil + if p == nil { + return + } + + p.TestReport = &v +} + // SetWarnings sets the Warnings field. // // When the provided Pipeline type is nil, it @@ -461,6 +488,7 @@ func (p *Pipeline) String() string { Stages: %t, Steps: %t, Templates: %t, + TestReport: %t, Type: %s, Version: %s, Warnings: %v, @@ -478,6 +506,7 @@ func (p *Pipeline) String() string { p.GetStages(), p.GetSteps(), p.GetTemplates(), + p.GetTestReport(), p.GetType(), p.GetVersion(), p.GetWarnings(), diff --git a/api/types/pipeline_test.go b/api/types/pipeline_test.go index 8c14c5af2..6d73f29bc 100644 --- a/api/types/pipeline_test.go +++ b/api/types/pipeline_test.go @@ -214,6 +214,7 @@ func TestAPI_Pipeline_String(t *testing.T) { Stages: %t, Steps: %t, Templates: %t, + TestReport: %t, Type: %s, Version: %s, Warnings: %v, @@ -231,6 +232,7 @@ func TestAPI_Pipeline_String(t *testing.T) { p.GetStages(), p.GetSteps(), p.GetTemplates(), + p.GetTestReport(), p.GetType(), p.GetVersion(), p.GetWarnings(), @@ -263,6 +265,7 @@ func testPipeline() *Pipeline { p.SetStages(false) p.SetSteps(true) p.SetTemplates(false) + p.SetTestReport(true) p.SetData(testPipelineData()) p.SetWarnings([]string{"42:this is a warning"}) diff --git a/api/types/test_report.go b/api/types/test_report.go new file mode 100644 index 000000000..a6cb7c7da --- /dev/null +++ b/api/types/test_report.go @@ -0,0 +1,62 @@ +package types + +import "fmt" + +// TestReport is the API representation of a test report for a pipeline. +// +// swagger:model TestReport +type TestReport struct { + Results *string `json:"results,omitempty"` + Attachments *string `json:"attachments,omitempty"` +} + +// GetResults returns the Results field. +// +// When the provided TestReport type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestReport) GetResults() string { + // return zero value if TestReport type or Results field is nil + if t == nil || t.Results == nil { + return "" + } + + return *t.Results +} + +// GetAttachments returns the Attachments field. +// +// When the provided TestReport type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestReport) GetAttachments() string { + // return zero value if TestReport type or Attachments field is nil + if t == nil || t.Attachments == nil { + return "" + } + + return *t.Attachments +} + +// SetResults sets the Results field. +func (t *TestReport) SetResults(v string) { + // return if TestReport type is nil + if t == nil { + return + } + // set the Results field + t.Results = &v +} + +// SetAttachments sets the Attachments field. +func (t *TestReport) SetAttachments(v string) { + // return if TestReport type is nil + if t == nil { + return + } + // set the Attachments field + t.Attachments = &v +} + +// String implements the Stringer interface for the TestReport type. +func (t *TestReport) String() string { + return fmt.Sprintf("Results: %s, Attachments: %s", t.GetResults(), t.GetAttachments()) +} diff --git a/compiler/native/validate.go b/compiler/native/validate.go index e6850f372..3d74144f5 100644 --- a/compiler/native/validate.go +++ b/compiler/native/validate.go @@ -108,7 +108,9 @@ func validateYAMLSteps(s yaml.StepSlice) error { if len(step.Commands) == 0 && len(step.Environment) == 0 && len(step.Parameters) == 0 && len(step.Secrets) == 0 && - len(step.Template.Name) == 0 && !step.Detach { + len(step.Template.Name) == 0 && len(step.TestReport.Results) == 0 && + len(step.TestReport.Attachments) == 0 && !step.Detach { + return fmt.Errorf("no commands, environment, parameters, secrets or template provided for step %s", step.Name) } } diff --git a/compiler/native/validate_test.go b/compiler/native/validate_test.go index 56c557491..078d90795 100644 --- a/compiler/native/validate_test.go +++ b/compiler/native/validate_test.go @@ -700,3 +700,34 @@ func TestNative_Validate_Steps_StepNameConflict(t *testing.T) { t.Errorf("Validate should have returned err") } } + +func TestNative_Validate_TestReport(t *testing.T) { + // setup types + str := "foo" + p := &yaml.Build{ + Version: "v1", + Steps: yaml.StepSlice{ + &yaml.Step{ + Commands: raw.StringSlice{"echo hello"}, + Image: "alpine", + Name: str, + Pull: "always", + TestReport: yaml.TestReport{ + Results: []string{"results.xml"}, + Attachments: []string{"attachments"}, + }, + }, + }, + } + + // run test + compiler, err := FromCLICommand(context.Background(), testCommand(t, "http://foo.example.com")) + if err != nil { + t.Errorf("Unable to create new compiler: %v", err) + } + + err = compiler.ValidateYAML(p) + if err != nil { + t.Errorf("Validate returned err: %v", err) + } +} diff --git a/compiler/types/pipeline/container.go b/compiler/types/pipeline/container.go index e3684da90..ed671667e 100644 --- a/compiler/types/pipeline/container.go +++ b/compiler/types/pipeline/container.go @@ -48,6 +48,7 @@ type ( Pull string `json:"pull,omitempty" yaml:"pull,omitempty"` Ruleset Ruleset `json:"ruleset,omitempty" yaml:"ruleset,omitempty"` Secrets StepSecretSlice `json:"secrets,omitempty" yaml:"secrets,omitempty"` + TestReport TestReport `json:"test_report,omitempty" yaml:"test_report,omitempty"` Ulimits UlimitSlice `json:"ulimits,omitempty" yaml:"ulimits,omitempty"` Volumes VolumeSlice `json:"volumes,omitempty" yaml:"volumes,omitempty"` User string `json:"user,omitempty" yaml:"user,omitempty"` @@ -137,7 +138,8 @@ func (c *Container) Empty() bool { len(c.Volumes) == 0 && len(c.User) == 0 && len(c.ReportAs) == 0 && - len(c.IDRequest) == 0 { + len(c.IDRequest) == 0 && + reflect.DeepEqual(c.TestReport, TestReport{}) { return true } diff --git a/compiler/types/pipeline/test_report.go b/compiler/types/pipeline/test_report.go new file mode 100644 index 000000000..bd4f81fd4 --- /dev/null +++ b/compiler/types/pipeline/test_report.go @@ -0,0 +1,54 @@ +package pipeline + +// TestReport represents the structure for test report configuration. +type ( + // TestReportSlice is the pipleine representation + //of a slice of TestReport. + // + // swagger:model PipelineTestReportSlice + TestReportSlice []*TestReport + + // TestReport is the pipeline representation + // of a test report for a pipeline. + // + // swagger:model PipelineTestReport + TestReport struct { + Results []string `yaml:"results,omitempty" json:"results,omitempty"` + Attachments []string `yaml:"attachments,omitempty" json:"attachments,omitempty"` + } +) + +// Purge removes the test report configuration from the pipeline +// if it does not match the provided ruledata. If both results +// and attachments are provided, then an empty test report is returned. +//func (t *TestReport) Purge(r *RuleData) (*TestReport, error) { +// // return an empty test report if both results and attachments are provided +// if len(t.Results) > 0 && len(t.Attachments) > 0 { +// return nil, fmt.Errorf("cannot have both results and attachments in the test report") +// } +// +// // purge results if provided +// if len(t.Results) > 0 { +// t.Results = "" +// } +// +// // purge attachments if provided +// if len(t.Attachments) > 0 { +// t.Attachments = "" +// } +// +// // return the purged test report +// return t, nil +//} + +// Empty returns true if the provided test report is empty. +func (t *TestReport) Empty() bool { + // return true if every test report field is empty + if len(t.Results) == 0 && + len(t.Attachments) == 0 { + return true + } + + // return false if any of the test report fields are provided + return false +} diff --git a/compiler/types/pipeline/test_report_test.go b/compiler/types/pipeline/test_report_test.go new file mode 100644 index 000000000..0e3db067c --- /dev/null +++ b/compiler/types/pipeline/test_report_test.go @@ -0,0 +1,29 @@ +package pipeline + +import "testing" + +func TestPipeline_TestReport_Empty(t *testing.T) { + // setup tests + tests := []struct { + report *TestReport + want bool + }{ + { + report: &TestReport{Results: []string{"foo"}}, + want: false, + }, + { + report: new(TestReport), + want: true, + }, + } + + // run tests + for _, test := range tests { + got := test.report.Empty() + + if got != test.want { + t.Errorf("Empty is %v, want %t", got, test.want) + } + } +} diff --git a/compiler/types/yaml/buildkite/step.go b/compiler/types/yaml/buildkite/step.go index 4b36b93fd..88cde5fa7 100644 --- a/compiler/types/yaml/buildkite/step.go +++ b/compiler/types/yaml/buildkite/step.go @@ -24,6 +24,7 @@ type ( Commands raw.StringSlice `yaml:"commands,omitempty" json:"commands,omitempty" jsonschema:"description=Execution instructions to run inside the container.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-commands-key"` Entrypoint raw.StringSlice `yaml:"entrypoint,omitempty" json:"entrypoint,omitempty" jsonschema:"description=Command to execute inside the container.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-entrypoint-key"` Secrets StepSecretSlice `yaml:"secrets,omitempty" json:"secrets,omitempty" jsonschema:"description=Sensitive variables injected into the container environment.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-secrets-key"` + TestReport TestReport `yaml:"test_report,omitempty" json:"test_report,omitempty" jsonschema:"description=Test report configuration for the step.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-test_report-key"` Template StepTemplate `yaml:"template,omitempty" json:"template,omitempty" jsonschema:"oneof_required=template,description=Name of template to expand in the pipeline.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-template-key"` Ulimits UlimitSlice `yaml:"ulimits,omitempty" json:"ulimits,omitempty" jsonschema:"description=Set the user limits for the container.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-ulimits-key"` Volumes VolumeSlice `yaml:"volumes,omitempty" json:"volumes,omitempty" jsonschema:"description=Mount volumes for the container.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-volume-key"` @@ -60,6 +61,7 @@ func (s *StepSlice) ToPipeline() *pipeline.ContainerSlice { Pull: step.Pull, Ruleset: *step.Ruleset.ToPipeline(), Secrets: *step.Secrets.ToPipeline(), + TestReport: *step.TestReport.ToPipeline(), Ulimits: *step.Ulimits.ToPipeline(), Volumes: *step.Volumes.ToPipeline(), User: step.User, @@ -165,6 +167,7 @@ func (s *Step) ToYAML() *yaml.Step { Ruleset: *s.Ruleset.ToYAML(), Secrets: *s.Secrets.ToYAML(), Template: s.Template.ToYAML(), + TestReport: s.TestReport.ToYAML(), Ulimits: *s.Ulimits.ToYAML(), Volumes: *s.Volumes.ToYAML(), Parameters: s.Parameters, diff --git a/compiler/types/yaml/buildkite/step_test.go b/compiler/types/yaml/buildkite/step_test.go index da4df26fe..18479c1b1 100644 --- a/compiler/types/yaml/buildkite/step_test.go +++ b/compiler/types/yaml/buildkite/step_test.go @@ -76,6 +76,10 @@ func TestYaml_StepSlice_ToPipeline(t *testing.T) { AccessMode: "ro", }, }, + TestReport: TestReport{ + Results: []string{"test-results/*.xml"}, + Attachments: []string{"screenshots/**/*.png", " video/*.mp4"}, + }, }, }, want: &pipeline.ContainerSlice{ @@ -134,6 +138,10 @@ func TestYaml_StepSlice_ToPipeline(t *testing.T) { AccessMode: "ro", }, }, + TestReport: pipeline.TestReport{ + Results: []string{"test-results/*.xml"}, + Attachments: []string{"screenshots/**/*.png", " video/*.mp4"}, + }, }, }, }, @@ -213,6 +221,15 @@ func TestYaml_StepSlice_UnmarshalYAML(t *testing.T) { }, }, }, + { + Name: "test-reports", + Pull: "always", + Image: "golang:1.20", + TestReport: TestReport{ + Results: []string{"test-results/*.xml"}, + Attachments: []string{"screenshots/**/*.png", " video/*.mp4"}, + }, + }, }, }, { diff --git a/compiler/types/yaml/buildkite/test_report.go b/compiler/types/yaml/buildkite/test_report.go new file mode 100644 index 000000000..0de529da0 --- /dev/null +++ b/compiler/types/yaml/buildkite/test_report.go @@ -0,0 +1,50 @@ +package buildkite + +import ( + "github.com/go-vela/server/compiler/types/pipeline" + "github.com/go-vela/server/compiler/types/yaml/yaml" +) + +// TestReport represents the structure for test report configuration. +type TestReport struct { + Results []string `yaml:"results,omitempty" json:"results,omitempty"` + Attachments []string `yaml:"attachments,omitempty" json:"attachments,omitempty"` +} + +// ToPipeline converts the TestReport type +// to a pipeline TestReport type. +func (t *TestReport) ToPipeline() *pipeline.TestReport { + return &pipeline.TestReport{ + Results: t.Results, + Attachments: t.Attachments, + } +} + +// UnmarshalYAML implements the Unmarshaler interface for the TestReport type. +func (t *TestReport) UnmarshalYAML(unmarshal func(interface{}) error) error { + // test report we try unmarshalling to + testReport := new(struct { + Results []string `yaml:"results,omitempty" json:"results,omitempty"` + Attachments []string `yaml:"attachments,omitempty" json:"attachments,omitempty"` + }) + + // attempt to unmarshal test report type + err := unmarshal(testReport) + if err != nil { + return err + } + + // set the results field + t.Results = testReport.Results + // set the attachments field + t.Attachments = testReport.Attachments + + return nil +} + +func (t *TestReport) ToYAML() yaml.TestReport { + return yaml.TestReport{ + Results: t.Results, + Attachments: t.Attachments, + } +} diff --git a/compiler/types/yaml/buildkite/testdata/step.yml b/compiler/types/yaml/buildkite/testdata/step.yml index 1d6d9cc93..79c0ff08f 100644 --- a/compiler/types/yaml/buildkite/testdata/step.yml +++ b/compiler/types/yaml/buildkite/testdata/step.yml @@ -43,4 +43,11 @@ vars: registry: index.docker.io repo: github/octocat - tags: [ latest, dev ] + tags: [latest, dev] + +- name: test-reports + image: golang:1.20 + pull: true + test_report: + results: ["test-results/*.xml"] + attachments: ["screenshots/**/*.png", " video/*.mp4"] diff --git a/compiler/types/yaml/yaml/secret.go b/compiler/types/yaml/yaml/secret.go index 97b836a09..efb0b86a9 100644 --- a/compiler/types/yaml/yaml/secret.go +++ b/compiler/types/yaml/yaml/secret.go @@ -157,7 +157,7 @@ func (o *Origin) Empty() bool { // MergeEnv takes a list of environment variables and attempts // to set them in the secret environment. If the environment -// variable already exists in the secret, than this will +// variable already exists in the secret, then this will // overwrite the existing environment variable. func (o *Origin) MergeEnv(environment map[string]string) error { // check if the secret container is empty diff --git a/compiler/types/yaml/yaml/step.go b/compiler/types/yaml/yaml/step.go index 405e6d598..8289520b3 100644 --- a/compiler/types/yaml/yaml/step.go +++ b/compiler/types/yaml/yaml/step.go @@ -24,6 +24,7 @@ type ( Entrypoint raw.StringSlice `yaml:"entrypoint,omitempty" json:"entrypoint,omitempty" jsonschema:"description=Command to execute inside the container.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-entrypoint-key"` Secrets StepSecretSlice `yaml:"secrets,omitempty" json:"secrets,omitempty" jsonschema:"description=Sensitive variables injected into the container environment.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-secrets-key"` Template StepTemplate `yaml:"template,omitempty" json:"template,omitempty" jsonschema:"oneof_required=template,description=Name of template to expand in the pipeline.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-template-key"` + TestReport TestReport `yaml:"test_report,omitempty" json:"test_report,omitempty" jsonschema:"description=Test report configuration for the step.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-test_report-key"` Ulimits UlimitSlice `yaml:"ulimits,omitempty" json:"ulimits,omitempty" jsonschema:"description=Set the user limits for the container.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-ulimits-key"` Volumes VolumeSlice `yaml:"volumes,omitempty" json:"volumes,omitempty" jsonschema:"description=Mount volumes for the container.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-volume-key"` Image string `yaml:"image,omitempty" json:"image,omitempty" jsonschema:"oneof_required=image,minLength=1,description=Docker image to use to create the ephemeral container.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-image-key"` @@ -59,6 +60,7 @@ func (s *StepSlice) ToPipeline() *pipeline.ContainerSlice { Pull: step.Pull, Ruleset: *step.Ruleset.ToPipeline(), Secrets: *step.Secrets.ToPipeline(), + TestReport: *step.TestReport.ToPipeline(), Ulimits: *step.Ulimits.ToPipeline(), Volumes: *step.Volumes.ToPipeline(), User: step.User, diff --git a/compiler/types/yaml/yaml/step_test.go b/compiler/types/yaml/yaml/step_test.go index 343daf230..dabbc7c3f 100644 --- a/compiler/types/yaml/yaml/step_test.go +++ b/compiler/types/yaml/yaml/step_test.go @@ -76,6 +76,10 @@ func TestYaml_StepSlice_ToPipeline(t *testing.T) { AccessMode: "ro", }, }, + TestReport: TestReport{ + Results: []string{"test-results/*.xml"}, + Attachments: []string{"screenshots/**/*.png", " video/*.mp4"}, + }, }, }, want: &pipeline.ContainerSlice{ @@ -134,6 +138,10 @@ func TestYaml_StepSlice_ToPipeline(t *testing.T) { AccessMode: "ro", }, }, + TestReport: pipeline.TestReport{ + Results: []string{"test-results/*.xml"}, + Attachments: []string{"screenshots/**/*.png", " video/*.mp4"}, + }, }, }, }, @@ -213,6 +221,15 @@ func TestYaml_StepSlice_UnmarshalYAML(t *testing.T) { }, }, }, + { + Name: "test-reports", + Image: "golang:1.20", + Pull: "always", + TestReport: TestReport{ + Results: []string{"test-results/*.xml"}, + Attachments: []string{"screenshots/**/*.png", " video/*.mp4"}, + }, + }, }, }, { diff --git a/compiler/types/yaml/yaml/test_report.go b/compiler/types/yaml/yaml/test_report.go new file mode 100644 index 000000000..038db367c --- /dev/null +++ b/compiler/types/yaml/yaml/test_report.go @@ -0,0 +1,40 @@ +package yaml + +import "github.com/go-vela/server/compiler/types/pipeline" + +// TestReport represents the structure for test report configuration. +type TestReport struct { + Results []string `yaml:"results,omitempty" json:"results,omitempty"` + Attachments []string `yaml:"attachments,omitempty" json:"attachments,omitempty"` +} + +// ToPipeline converts the TestReport type +// to a pipeline TestReport type. +func (t *TestReport) ToPipeline() *pipeline.TestReport { + return &pipeline.TestReport{ + Results: t.Results, + Attachments: t.Attachments, + } +} + +// UnmarshalYAML implements the Unmarshaler interface for the TestReport type. +func (t *TestReport) UnmarshalYAML(unmarshal func(interface{}) error) error { + // test report we try unmarshalling to + testReport := new(struct { + Results []string `yaml:"results,omitempty" json:"results,omitempty"` + Attachments []string `yaml:"attachments,omitempty" json:"attachments,omitempty"` + }) + + // attempt to unmarshal test report type + err := unmarshal(testReport) + if err != nil { + return err + } + + // set the results field + t.Results = testReport.Results + // set the attachments field + t.Attachments = testReport.Attachments + + return nil +} diff --git a/compiler/types/yaml/yaml/testdata/step.yml b/compiler/types/yaml/yaml/testdata/step.yml index 1d6d9cc93..79c0ff08f 100644 --- a/compiler/types/yaml/yaml/testdata/step.yml +++ b/compiler/types/yaml/yaml/testdata/step.yml @@ -43,4 +43,11 @@ vars: registry: index.docker.io repo: github/octocat - tags: [ latest, dev ] + tags: [latest, dev] + +- name: test-reports + image: golang:1.20 + pull: true + test_report: + results: ["test-results/*.xml"] + attachments: ["screenshots/**/*.png", " video/*.mp4"] diff --git a/mock/server/pipeline.go b/mock/server/pipeline.go index a55fc1e93..0b5662fae 100644 --- a/mock/server/pipeline.go +++ b/mock/server/pipeline.go @@ -169,6 +169,7 @@ templates: "stages": false, "steps": true, "templates": false, + "test_report": false, "warnings": [ "42:this is a warning" ], @@ -244,6 +245,7 @@ templates: "stages": false, "steps": true, "templates": false, + "test_report": false, "data": "LS0tCnZlcnNpb246ICIxIgoKc3RlcHM6CiAgLSBuYW1lOiBlY2hvCiAgICBpbWFnZTogYWxwaW5lOmxhdGVzdAogICAgY29tbWFuZHM6IFtlY2hvIGZvb10=" }, { @@ -313,6 +315,7 @@ templates: "stages": false, "steps": true, "templates": false, + "test_report": false, "data": "LS0tCnZlcnNpb246ICIxIgoKc3RlcHM6CiAgLSBuYW1lOiBlY2hvCiAgICBpbWFnZTogYWxwaW5lOmxhdGVzdAogICAgY29tbWFuZHM6IFtlY2hvIGZvb10=" } ]`