diff --git a/cmd/cvp-trigger/main.go b/cmd/cvp-trigger/main.go index 60c41eb7929..c333de5d4b9 100644 --- a/cmd/cvp-trigger/main.go +++ b/cmd/cvp-trigger/main.go @@ -213,7 +213,7 @@ func main() { steps.OOChannel: o.channel, } if o.releaseImageRef != "" { - envVars[utils.ReleaseImageEnv(api.LatestStableName)] = o.releaseImageRef + envVars[utils.ReleaseImageEnv(api.LatestReleaseName)] = o.releaseImageRef } if o.installNamespace != "" { envVars[steps.OOInstallNamespace] = o.installNamespace diff --git a/pkg/api/config.go b/pkg/api/config.go index e58a410ae4c..0a0b0b0983e 100644 --- a/pkg/api/config.go +++ b/pkg/api/config.go @@ -71,6 +71,10 @@ func (config *ReleaseBuildConfiguration) validate(org, repo string, resolved boo validationErrors = append(validationErrors, validateBuildRootImageConfiguration("build_root", config.InputConfiguration.BuildRootImage, len(config.Images) > 0)...) validationErrors = append(validationErrors, validateTestStepConfiguration("tests", config.Tests, config.ReleaseTagConfiguration, resolved)...) + // this validation brings together a large amount of data from separate + // parts of the configuration, so it's written as a standalone method + validationErrors = append(validationErrors, config.validateTestStepDependencies()...) + if config.InputConfiguration.BaseImages != nil { validationErrors = append(validationErrors, validateImageStreamTagReferenceMap("base_images", config.InputConfiguration.BaseImages)...) } @@ -108,6 +112,146 @@ func (config *ReleaseBuildConfiguration) validate(org, repo string, resolved boo } } +// validateTestStepDependencies ensures that users have referenced valid dependencies +func (config *ReleaseBuildConfiguration) validateTestStepDependencies() []error { + dependencyErrors := func(step LiteralTestStep, testIdx int, stageField, stepField string, stepIdx int) []error { + var errs []error + for dependencyIdx, dependency := range step.Dependencies { + validationError := func(message string) error { + return fmt.Errorf("tests[%d].%s.%s[%d].dependencies[%d]: cannot determine source for dependency %q - %s", testIdx, stageField, stepField, stepIdx, dependencyIdx, dependency.Name, message) + } + stream, name, explicit := config.DependencyParts(dependency) + if link := LinkForImage(stream, name); link == nil { + errs = append(errs, validationError("ensure the correct ImageStream name was provided")) + } + if explicit { + // the user has asked us for something specific, and we can + // do some best-effort analysis of that input to see if it's + // possible that it will resolve at run-time. We could just + // let the step graph fail when this input is used to run a + // job, but this validation will catch things faster and be + // overall more useful, so we do both. + var releaseName string + switch { + case IsReleaseStream(stream): + releaseName = ReleaseNameFrom(stream) + case IsReleasePayloadStream(stream): + releaseName = name + } + + if releaseName != "" { + implictlyConfigured := (releaseName == InitialReleaseName || releaseName == LatestReleaseName) && config.InputConfiguration.ReleaseTagConfiguration != nil + _, explicitlyConfigured := config.InputConfiguration.Releases[releaseName] + if !(implictlyConfigured || explicitlyConfigured) { + errs = append(errs, validationError(fmt.Sprintf("this dependency requires a %q release, which is not configured", releaseName))) + } + } + + if stream == PipelineImageStream { + switch name { + case string(PipelineImageStreamTagReferenceRoot): + if config.InputConfiguration.BuildRootImage == nil { + errs = append(errs, validationError("this dependency requires a build root, which is not configured")) + } + case string(PipelineImageStreamTagReferenceSource): + // always present + case string(PipelineImageStreamTagReferenceBinaries): + if config.BinaryBuildCommands == "" { + errs = append(errs, validationError("this dependency requires built binaries, which are not configured")) + } + case string(PipelineImageStreamTagReferenceTestBinaries): + if config.TestBinaryBuildCommands == "" { + errs = append(errs, validationError("this dependency requires built test binaries, which are not configured")) + } + case string(PipelineImageStreamTagReferenceRPMs): + if config.RpmBuildCommands == "" { + errs = append(errs, validationError("this dependency requires built RPMs, which are not configured")) + } + default: + // this could be a base image, or a project image + if !config.IsBaseImage(name) && !config.BuildsImage(name) { + errs = append(errs, validationError("no base image import or project image build is configured to provide this dependency")) + } + } + } + } + } + return errs + } + processSteps := func(steps []TestStep, testIdx int, stageField, stepField string) []error { + var errs []error + for stepIdx, test := range steps { + if test.LiteralTestStep != nil { + errs = append(errs, dependencyErrors(*test.LiteralTestStep, testIdx, stageField, stepField, stepIdx)...) + } + } + return errs + } + processLiteralSteps := func(steps []LiteralTestStep, testIdx int, stageField, stepField string) []error { + var errs []error + for stepIdx, test := range steps { + errs = append(errs, dependencyErrors(test, testIdx, stageField, stepField, stepIdx)...) + } + return errs + } + var errs []error + for testIdx, test := range config.Tests { + if test.MultiStageTestConfiguration != nil { + for _, item := range []struct { + field string + list []TestStep + }{ + {field: "pre", list: test.MultiStageTestConfiguration.Pre}, + {field: "test", list: test.MultiStageTestConfiguration.Test}, + {field: "post", list: test.MultiStageTestConfiguration.Post}, + } { + errs = append(errs, processSteps(item.list, testIdx, "steps", item.field)...) + } + } + if test.MultiStageTestConfigurationLiteral != nil { + for _, item := range []struct { + field string + list []LiteralTestStep + }{ + {field: "pre", list: test.MultiStageTestConfigurationLiteral.Pre}, + {field: "test", list: test.MultiStageTestConfigurationLiteral.Test}, + {field: "post", list: test.MultiStageTestConfigurationLiteral.Post}, + } { + errs = append(errs, processLiteralSteps(item.list, testIdx, "literal_steps", item.field)...) + } + } + } + return errs +} + +// ImageStreamFor guesses at the ImageStream that will hold a tag. +// We use this to decipher the user's intent when they provide a +// naked tag in configuration; we support such behavior in order to +// allow users a simpler workflow for the most common cases, like +// referring to `pipeline:src`. If they refer to an ambiguous image, +// however, they will get bad behavior and will need to specify an +// ImageStream as well, for instance release-initial:installer. +// We also return whether the stream is explicit or inferred. +func (config *ReleaseBuildConfiguration) ImageStreamFor(image string) (string, bool) { + if config.IsPipelineImage(image) || config.BuildsImage(image) { + return PipelineImageStream, true + } else { + return StableImageStream, false + } +} + +// DependencyParts returns the imageStream and tag name from a user-provided +// reference to an image in the test namespace +func (config *ReleaseBuildConfiguration) DependencyParts(dependency StepDependency) (string, string, bool) { + if !strings.Contains(dependency.Name, ":") { + stream, explicit := config.ImageStreamFor(dependency.Name) + return stream, dependency.Name, explicit + } else { + parts := strings.Split(dependency.Name, ":") + return parts[0], parts[1], true + } +} + func validateBuildRootImageConfiguration(fieldRoot string, input *BuildRootImageConfiguration, hasImages bool) []error { if input == nil { if hasImages { @@ -469,6 +613,7 @@ func validateLiteralTestStepCommon(fieldRoot string, step LiteralTestStep, seen if err := validateParameters(fieldRoot, step.Environment, env); err != nil { ret = append(ret, err) } + ret = append(ret, validateDependencies(fieldRoot, step.Dependencies)...) return } @@ -551,6 +696,26 @@ func validateParameters(fieldRoot string, params []StepParameter, env TestEnviro return nil } +func validateDependencies(fieldRoot string, dependencies []StepDependency) []error { + var errs []error + env := sets.NewString() + for i, dependency := range dependencies { + if dependency.Name == "" { + errs = append(errs, fmt.Errorf("%s.dependencies[%d].name must be set", fieldRoot, i)) + } else if numColons := strings.Count(dependency.Name, ":"); !(numColons == 0 || numColons == 1) { + errs = append(errs, fmt.Errorf("%s.dependencies[%d].name must take the `tag` or `stream:tag` form, not %q", fieldRoot, i, dependency.Name)) + } + if dependency.Env == "" { + errs = append(errs, fmt.Errorf("%s.dependencies[%d].env must be set", fieldRoot, i)) + } else if env.Has(dependency.Env) { + errs = append(errs, fmt.Errorf("%s.dependencies[%d].env targets an environment variable that is already set by another dependency", fieldRoot, i)) + } else { + env.Insert(dependency.Env) + } + } + return errs +} + func validateReleaseBuildConfiguration(input *ReleaseBuildConfiguration, org, repo string) []error { var validationErrors []error diff --git a/pkg/api/config_test.go b/pkg/api/config_test.go index 239295c53aa..9ddc617f5a7 100644 --- a/pkg/api/config_test.go +++ b/pkg/api/config_test.go @@ -1487,3 +1487,258 @@ func errListMessagesEqual(a, b []error) bool { } return true } + +func TestValidateDependencies(t *testing.T) { + var testCases = []struct { + name string + input []StepDependency + output []error + }{ + { + name: "no dependencies", + input: nil, + }, + { + name: "valid dependencies", + input: []StepDependency{ + {Name: "src", Env: "SOURCE"}, + {Name: "stable:installer", Env: "INSTALLER"}, + }, + }, + { + name: "invalid dependencies", + input: []StepDependency{ + {Name: "", Env: ""}, + {Name: "src", Env: "SOURCE"}, + {Name: "src", Env: "SOURCE"}, + {Name: "src:lol:oops", Env: "WHOA"}, + }, + output: []error{ + errors.New("root.dependencies[0].name must be set"), + errors.New("root.dependencies[0].env must be set"), + errors.New("root.dependencies[2].env targets an environment variable that is already set by another dependency"), + errors.New("root.dependencies[3].name must take the `tag` or `stream:tag` form, not \"src:lol:oops\""), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + if actual, expected := validateDependencies("root", testCase.input), testCase.output; !reflect.DeepEqual(actual, expected) { + t.Errorf("%s: got incorrect errors: %s", testCase.name, cmp.Diff(actual, expected, cmp.Comparer(func(x, y error) bool { + return x.Error() == y.Error() + }))) + } + }) + } +} + +func TestReleaseBuildConfiguration_validateTestStepDependencies(t *testing.T) { + var testCases = []struct { + name string + config ReleaseBuildConfiguration + expected []error + }{ + { + name: "no tests", + }, + { + name: "valid dependencies", + config: ReleaseBuildConfiguration{ + InputConfiguration: InputConfiguration{ + // tag_spec provides stable, initial + ReleaseTagConfiguration: &ReleaseTagConfiguration{Namespace: "ocp", Name: "4.5"}, + // releases provides custom + Releases: map[string]UnresolvedRelease{ + "custom": {Release: &Release{Version: "4.7", Channel: ReleaseChannelStable}}, + }, + }, + BinaryBuildCommands: "whoa", + Images: []ProjectDirectoryImageBuildStepConfiguration{{To: "image"}}, + Tests: []TestStepConfiguration{ + {MultiStageTestConfiguration: &MultiStageTestConfiguration{ + Pre: []TestStep{ + {LiteralTestStep: &LiteralTestStep{Dependencies: []StepDependency{{Name: "src"}, {Name: "bin"}, {Name: "installer"}}}}, + {LiteralTestStep: &LiteralTestStep{Dependencies: []StepDependency{{Name: "stable:installer"}, {Name: "stable-initial:installer"}}}}, + }, + Test: []TestStep{{LiteralTestStep: &LiteralTestStep{Dependencies: []StepDependency{{Name: "pipeline:bin"}}}}}, + Post: []TestStep{{LiteralTestStep: &LiteralTestStep{Dependencies: []StepDependency{{Name: "image"}}}}}, + }}, + {MultiStageTestConfigurationLiteral: &MultiStageTestConfigurationLiteral{ + Pre: []LiteralTestStep{{Dependencies: []StepDependency{{Name: "stable-custom:cli"}}}}, + Test: []LiteralTestStep{{Dependencies: []StepDependency{{Name: "release:custom"}, {Name: "release:initial"}}}}, + Post: []LiteralTestStep{{Dependencies: []StepDependency{{Name: "pipeline:image"}}}}, + }}, + }, + }, + }, + { + name: "invalid dependencies", + config: ReleaseBuildConfiguration{ + Tests: []TestStepConfiguration{ + {MultiStageTestConfiguration: &MultiStageTestConfiguration{ + Pre: []TestStep{ + {LiteralTestStep: &LiteralTestStep{Dependencies: []StepDependency{{Name: "stable:installer"}, {Name: "stable:grafana"}}}}, + {LiteralTestStep: &LiteralTestStep{Dependencies: []StepDependency{{Name: "stable-custom:cli"}, {Name: "totally-invalid:cli"}}}}, + }, + Test: []TestStep{ + {LiteralTestStep: &LiteralTestStep{Dependencies: []StepDependency{{Name: "pipeline:bin"}}}}, + {LiteralTestStep: &LiteralTestStep{Dependencies: []StepDependency{{Name: "pipeline:test-bin"}}}}, + }, + Post: []TestStep{{LiteralTestStep: &LiteralTestStep{Dependencies: []StepDependency{{Name: "pipeline:image"}}}}}, + }}, + {MultiStageTestConfigurationLiteral: &MultiStageTestConfigurationLiteral{ + Pre: []LiteralTestStep{{Dependencies: []StepDependency{{Name: "release:custom"}}}}, + Test: []LiteralTestStep{{Dependencies: []StepDependency{{Name: "pipeline:root"}}}}, + Post: []LiteralTestStep{{Dependencies: []StepDependency{{Name: "pipeline:rpms"}}}}, + }}, + }, + }, + expected: []error{ + errors.New(`tests[0].steps.pre[0].dependencies[0]: cannot determine source for dependency "stable:installer" - this dependency requires a "latest" release, which is not configured`), + errors.New(`tests[0].steps.pre[0].dependencies[1]: cannot determine source for dependency "stable:grafana" - this dependency requires a "latest" release, which is not configured`), + errors.New(`tests[0].steps.pre[1].dependencies[0]: cannot determine source for dependency "stable-custom:cli" - this dependency requires a "custom" release, which is not configured`), + errors.New(`tests[0].steps.pre[1].dependencies[1]: cannot determine source for dependency "totally-invalid:cli" - ensure the correct ImageStream name was provided`), + errors.New(`tests[0].steps.test[0].dependencies[0]: cannot determine source for dependency "pipeline:bin" - this dependency requires built binaries, which are not configured`), + errors.New(`tests[0].steps.test[1].dependencies[0]: cannot determine source for dependency "pipeline:test-bin" - this dependency requires built test binaries, which are not configured`), + errors.New(`tests[0].steps.post[0].dependencies[0]: cannot determine source for dependency "pipeline:image" - no base image import or project image build is configured to provide this dependency`), + errors.New(`tests[1].literal_steps.pre[0].dependencies[0]: cannot determine source for dependency "release:custom" - this dependency requires a "custom" release, which is not configured`), + errors.New(`tests[1].literal_steps.test[0].dependencies[0]: cannot determine source for dependency "pipeline:root" - this dependency requires a build root, which is not configured`), + errors.New(`tests[1].literal_steps.post[0].dependencies[0]: cannot determine source for dependency "pipeline:rpms" - this dependency requires built RPMs, which are not configured`), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + if actual, expected := testCase.config.validateTestStepDependencies(), testCase.expected; !reflect.DeepEqual(actual, expected) { + t.Errorf("%s: got incorrect errors: %s", testCase.name, cmp.Diff(actual, expected, cmp.Comparer(func(x, y error) bool { + return x.Error() == y.Error() + }))) + } + }) + } +} + +func TestReleaseBuildConfiguration_ImageStreamFor(t *testing.T) { + var testCases = []struct { + name string + config *ReleaseBuildConfiguration + image string + expected string + explicit bool + }{ + { + name: "explicit, is a base image", + config: &ReleaseBuildConfiguration{InputConfiguration: InputConfiguration{ + BaseImages: map[string]ImageStreamTagReference{"thebase": {}}, + }}, + image: "thebase", + expected: PipelineImageStream, + explicit: true, + }, + { + name: "explicit, is an RPM base image", + config: &ReleaseBuildConfiguration{InputConfiguration: InputConfiguration{ + BaseRPMImages: map[string]ImageStreamTagReference{"thebase": {}}, + }}, + image: "thebase", + expected: PipelineImageStream, + explicit: true, + }, + { + name: "explicit, is a known pipeline image", + config: &ReleaseBuildConfiguration{}, + image: "src", + expected: PipelineImageStream, + explicit: true, + }, + { + name: "explicit, is a known built image", + config: &ReleaseBuildConfiguration{Images: []ProjectDirectoryImageBuildStepConfiguration{{To: "myimage"}}}, + image: "myimage", + expected: PipelineImageStream, + explicit: true, + }, + { + name: "implicit, is random", + config: &ReleaseBuildConfiguration{}, + image: "something", + expected: StableImageStream, + explicit: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + actual, explicit := testCase.config.ImageStreamFor(testCase.image) + if explicit != testCase.explicit { + t.Errorf("%s: did not correctly determine if ImageStream was explicit (should be %v)", testCase.name, testCase.explicit) + } + if actual != testCase.expected { + t.Errorf("%s: did not correctly determine ImageStream wanted %s, got %s", testCase.name, testCase.expected, actual) + } + }) + } +} + +func TestReleaseBuildConfiguration_DependencyParts(t *testing.T) { + var testCases = []struct { + name string + config *ReleaseBuildConfiguration + dependency StepDependency + expectedStream string + expectedTag string + explicit bool + }{ + { + name: "explicit, short-hand for base image", + config: &ReleaseBuildConfiguration{InputConfiguration: InputConfiguration{ + BaseImages: map[string]ImageStreamTagReference{"thebase": {}}, + }}, + dependency: StepDependency{Name: "thebase"}, + expectedStream: PipelineImageStream, + expectedTag: "thebase", + explicit: true, + }, + { + name: "implicit, short-hand for random", + config: &ReleaseBuildConfiguration{}, + dependency: StepDependency{Name: "whatever"}, + expectedStream: StableImageStream, + expectedTag: "whatever", + explicit: false, + }, + { + name: "explicit, long-form for stable", + config: &ReleaseBuildConfiguration{}, + dependency: StepDependency{Name: "stable:installer"}, + expectedStream: StableImageStream, + expectedTag: "installer", + explicit: true, + }, + { + name: "explicit, long-form for something crazy", + config: &ReleaseBuildConfiguration{}, + dependency: StepDependency{Name: "whoa:really"}, + expectedStream: "whoa", + expectedTag: "really", + explicit: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + actualStream, actualTag, explicit := testCase.config.DependencyParts(testCase.dependency) + if explicit != testCase.explicit { + t.Errorf("%s: did not correctly determine if ImageStream was explicit (should be %v)", testCase.name, testCase.explicit) + } + if actualStream != testCase.expectedStream { + t.Errorf("%s: did not correctly determine ImageStream wanted %s, got %s", testCase.name, testCase.expectedStream, actualStream) + } + if actualTag != testCase.expectedTag { + t.Errorf("%s: did not correctly determine ImageTag wanted %s, got %s", testCase.name, testCase.expectedTag, actualTag) + } + }) + } +} diff --git a/pkg/api/graph.go b/pkg/api/graph.go index cb66dde7e37..fbd4acaa5ff 100644 --- a/pkg/api/graph.go +++ b/pkg/api/graph.go @@ -151,19 +151,19 @@ func (l *rpmRepoLink) SatisfiedBy(other StepLink) bool { } } -// StableImagesLink describes the content of a stable(-foo)? +// ReleaseImagesLink describes the content of a stable(-foo)? // ImageStream in the test namespace. -func StableImagesLink(name string) StepLink { +func ReleaseImagesLink(name string) StepLink { return &internalImageStreamLink{ - name: StableStreamFor(name), + name: ReleaseStreamFor(name), } } -// StableImageTagLink describes a specific tag in a stable(-foo)? +// ReleaseImageTagLink describes a specific tag in a stable(-foo)? // ImageStream in the test namespace. -func StableImageTagLink(name, tag string) StepLink { +func ReleaseImageTagLink(name, tag string) StepLink { return &internalImageStreamTagLink{ - name: StableStreamFor(name), + name: ReleaseStreamFor(name), tag: tag, } } @@ -176,14 +176,38 @@ func Comparer() cmp.Option { ) } -func StableStreamFor(name string) string { - if name == LatestStableName { +// ReleaseStreamFor determines the ImageStream into which a named +// release will be imported or assembled. +func ReleaseStreamFor(name string) string { + if name == LatestReleaseName { return StableImageStream } return fmt.Sprintf("%s-%s", StableImageStream, name) } +// ReleaseNameFrom determines the named release that was imported +// or assembled into an ImageStream. +func ReleaseNameFrom(stream string) string { + if stream == StableImageStream { + return LatestReleaseName + } + + return strings.TrimPrefix(stream, fmt.Sprintf("%s-", StableImageStream)) +} + +// IsReleaseStream determines if the ImageStream was created from +// an import or assembly of a release. +func IsReleaseStream(stream string) bool { + return strings.HasPrefix(stream, StableImageStream) +} + +// IsReleasePayloadStream determines if the ImageStream holds +// release paylaod images. +func IsReleasePayloadStream(stream string) bool { + return stream == ReleaseImageStream +} + type StepNode struct { Step Step Children []*StepNode @@ -322,3 +346,25 @@ const CIOperatorStepGraphJSONFilename = "ci-operator-step-graph.json" func StepGraphJSONURL(baseJobURL string) string { return strings.Join([]string{baseJobURL, "artifacts", CIOperatorStepGraphJSONFilename}, "/") } + +// LinkForImage determines what dependent link is required +// for the user's image dependency +func LinkForImage(imageStream, tag string) StepLink { + switch { + case imageStream == PipelineImageStream: + // the user needs an image we're building + return InternalImageLink(PipelineImageStreamTagReference(tag)) + case IsReleaseStream(imageStream): + // the user needs a tag that's a component of some release; + // we cant' rely on a specific tag, as they are implicit in + // the import process and won't be present in the build graph, + // so we wait for the whole import to succeed + return ReleaseImagesLink(ReleaseNameFrom(imageStream)) + case IsReleasePayloadStream(imageStream): + // the user needs a release payload + return ReleasePayloadImageLink(tag) + default: + // we have no idea what the user's configured + return nil + } +} diff --git a/pkg/api/graph_test.go b/pkg/api/graph_test.go index 142389687a2..b13f900eb40 100644 --- a/pkg/api/graph_test.go +++ b/pkg/api/graph_test.go @@ -4,6 +4,8 @@ import ( "context" "reflect" "testing" + + "github.com/google/go-cmp/cmp" ) func TestMatches(t *testing.T) { @@ -33,8 +35,8 @@ func TestMatches(t *testing.T) { }, { name: "release images matches itself", - first: StableImagesLink(LatestStableName), - second: StableImagesLink(LatestStableName), + first: ReleaseImagesLink(LatestReleaseName), + second: ReleaseImagesLink(LatestReleaseName), matches: true, }, { @@ -64,7 +66,7 @@ func TestMatches(t *testing.T) { { name: "internal does not match release images", first: InternalImageLink(PipelineImageStreamTagReferenceRPMs), - second: StableImagesLink(LatestStableName), + second: ReleaseImagesLink(LatestReleaseName), matches: false, }, { @@ -76,13 +78,13 @@ func TestMatches(t *testing.T) { { name: "external does not match release images", first: ExternalImageLink(ImageStreamTagReference{Namespace: "ns", Name: "name", Tag: "latest"}), - second: StableImagesLink(LatestStableName), + second: ReleaseImagesLink(LatestReleaseName), matches: false, }, { name: "RPM does not match release images", first: RPMRepoLink(), - second: StableImagesLink(LatestStableName), + second: ReleaseImagesLink(LatestReleaseName), matches: false, }, } @@ -220,3 +222,68 @@ func TestBuildGraph(t *testing.T) { } } } + +func TestReleaseNames(t *testing.T) { + var testCases = []string{ + LatestReleaseName, + InitialReleaseName, + "foo", + } + for _, name := range testCases { + stream := ReleaseStreamFor(name) + if !IsReleaseStream(stream) { + t.Errorf("stream %s for name %s was not identified as a release stream", stream, name) + } + if actual, expected := ReleaseNameFrom(stream), name; actual != expected { + t.Errorf("parsed name %s from stream %s, but it was created for name %s", actual, stream, expected) + } + } + +} + +func TestLinkForImage(t *testing.T) { + var testCases = []struct { + stream, tag string + expected StepLink + }{ + { + stream: "pipeline", + tag: "src", + expected: InternalImageLink(PipelineImageStreamTagReferenceSource), + }, + { + stream: "pipeline", + tag: "rpms", + expected: InternalImageLink(PipelineImageStreamTagReferenceRPMs), + }, + { + stream: "stable", + tag: "installer", + expected: ReleaseImagesLink(LatestReleaseName), + }, + { + stream: "stable-initial", + tag: "cli", + expected: ReleaseImagesLink(InitialReleaseName), + }, + { + stream: "stable-whatever", + tag: "hyperconverged-cluster-operator", + expected: ReleaseImagesLink("whatever"), + }, + { + stream: "release", + tag: "latest", + expected: ReleasePayloadImageLink(LatestReleaseName), + }, + { + stream: "crazy", + tag: "tag", + }, + } + for _, testCase := range testCases { + if diff := cmp.Diff(LinkForImage(testCase.stream, testCase.tag), testCase.expected, Comparer()); diff != "" { + t.Errorf("got incorrect link for %s:%s: %v", testCase.stream, testCase.tag, diff) + } + } +} diff --git a/pkg/api/types.go b/pkg/api/types.go index 76dabbe8b3f..8552fb7be8c 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -104,8 +104,9 @@ func (c ReleaseBuildConfiguration) BuildsImage(name string) bool { return false } -// IsPipelineImage checks if `name` will be a tag in the pipeline image stream. -func (c ReleaseBuildConfiguration) IsPipelineImage(name string) bool { +// IsBaseImage checks if `name` will be a tag in the pipeline image stream +// by virtue of being imported as a base image +func (c ReleaseBuildConfiguration) IsBaseImage(name string) bool { for i := range c.BaseImages { if i == name { return true @@ -116,6 +117,14 @@ func (c ReleaseBuildConfiguration) IsPipelineImage(name string) bool { return true } } + return false +} + +// IsPipelineImage checks if `name` will be a tag in the pipeline image stream. +func (c ReleaseBuildConfiguration) IsPipelineImage(name string) bool { + if c.IsBaseImage(name) { + return true + } switch name { case string(PipelineImageStreamTagReferenceRoot), string(PipelineImageStreamTagReferenceSource), @@ -563,6 +572,9 @@ type LiteralTestStep struct { Credentials []CredentialReference `json:"credentials,omitempty"` // Environment lists parameters that should be set by the test. Environment []StepParameter `json:"env,omitempty"` + // Dependencies lists images which must be available before the test runs + // and the environment variables which are used to expose their pull specs. + Dependencies []StepDependency `json:"dependencies,omitempty"` // OptionalOnSuccess defines if this step should be skipped as long // as all `pre` and `test` steps were successful and AllowSkipOnSuccess // flag is set to true in MultiStageTestConfiguration. This option is @@ -590,6 +602,15 @@ type CredentialReference struct { MountPath string `json:"mount_path"` } +// StepDependency defines a dependency on an image and the environment variable +// used to expose the image's pull spec to the step. +type StepDependency struct { + // Name is the tag or stream:tag that this dependency references + Name string `json:"name"` + // Env is the environment variable that the image's pull spec is exposed with + Env string `json:"env"` +} + // FromImageTag returns the internal name for the image tag that will be used // for this step, if one is configured. func (s *LiteralTestStep) FromImageTag() (PipelineImageStreamTagReference, bool) { @@ -1108,15 +1129,15 @@ const ( // build outputs from the repository under test and // the associated images imported from integration streams StableImageStream = "stable" - // LatestStableName is the name of the special latest + // LatestReleaseName is the name of the special latest // stable stream, images in this stream are held in // the StableImageStream. Images for other versions of // the stream are held in similarly-named streams. - LatestStableName = "latest" - // InitialImageStream is the name of the special stable + LatestReleaseName = "latest" + // LatestReleaseName is the name of the special stable // stream we copy at import to keep for upgrade tests. // TODO(skuznets): remove these when they're not implicit - InitialImageStream = "initial" + InitialReleaseName = "initial" // ReleaseImageStream is the name of the ImageStream // used to hold built or imported release payload images diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go index 6dfbab3afe0..6cdc217e43b 100644 --- a/pkg/defaults/defaults.go +++ b/pkg/defaults/defaults.go @@ -174,7 +174,7 @@ func FromConfig( // as well. For backwards compatibility, we explicitly support // 'initial' and 'latest': if not provided, we will build them. // If a pull spec was provided, however, it will be used. - for _, name := range []string{api.InitialImageStream, api.LatestStableName} { + for _, name := range []string{api.InitialReleaseName, api.LatestReleaseName} { var releaseStep api.Step envVar := utils.ReleaseImageEnv(name) if params.HasInput(envVar) { @@ -224,7 +224,7 @@ func FromConfig( step = release.ImportReleaseStep(resolveConfig.Name, value, false, config.Resources, podClient, imageClient, saGetter, rbacClient, artifactDir, jobSpec) } else if testStep := rawStep.TestStepConfiguration; testStep != nil { if test := testStep.MultiStageTestConfigurationLiteral; test != nil { - step = steps.MultiStageTestStep(*testStep, config, params, podClient, secretGetter, saGetter, rbacClient, artifactDir, jobSpec) + step = steps.MultiStageTestStep(*testStep, config, params, podClient, secretGetter, saGetter, rbacClient, imageClient, artifactDir, jobSpec) if test.ClusterProfile != "" { step = steps.LeaseStep(leaseClient, test.ClusterProfile.LeaseType(), step, jobSpec.Namespace, namespaceClient) } diff --git a/pkg/steps/clusterinstall/clusterinstall.go b/pkg/steps/clusterinstall/clusterinstall.go index 61605ed4962..ef002c1565e 100644 --- a/pkg/steps/clusterinstall/clusterinstall.go +++ b/pkg/steps/clusterinstall/clusterinstall.go @@ -70,7 +70,7 @@ func E2ETestStep( } // ensure we depend on the release image - name := utils.ReleaseImageEnv(api.InitialImageStream) + name := utils.ReleaseImageEnv(api.InitialReleaseName) template.Parameters = append(template.Parameters, templateapi.Parameter{ Required: true, Name: name, diff --git a/pkg/steps/lease_test.go b/pkg/steps/lease_test.go index bb7d6e68972..5837fdfbaa8 100644 --- a/pkg/steps/lease_test.go +++ b/pkg/steps/lease_test.go @@ -32,7 +32,7 @@ func (s *stepNeedsLease) Run(ctx context.Context) error { func (stepNeedsLease) Name() string { return "needs_lease" } func (stepNeedsLease) Description() string { return "this step needs a lease" } func (stepNeedsLease) Requires() []api.StepLink { - return []api.StepLink{api.StableImagesLink(api.LatestStableName)} + return []api.StepLink{api.ReleaseImagesLink(api.LatestReleaseName)} } func (stepNeedsLease) Creates() []api.StepLink { return []api.StepLink{api.ImagesReadyLink()} } diff --git a/pkg/steps/multi_stage.go b/pkg/steps/multi_stage.go index 930db5b2962..76fb62f08c9 100644 --- a/pkg/steps/multi_stage.go +++ b/pkg/steps/multi_stage.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" + imageclientset "github.com/openshift/client-go/image/clientset/versioned/typed/image/v1" coreapi "k8s.io/api/core/v1" rbacapi "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -36,7 +37,7 @@ const ( ) var envForProfile = []string{ - utils.ReleaseImageEnv(api.LatestStableName), + utils.ReleaseImageEnv(api.LatestReleaseName), leaseEnv, utils.ImageFormatEnv, } @@ -53,6 +54,7 @@ type multiStageTestStep struct { secretClient coreclientset.SecretsGetter saClient coreclientset.ServiceAccountsGetter rbacClient rbacclientset.RbacV1Interface + isClient imageclientset.ImageStreamsGetter artifactDir string jobSpec *api.JobSpec pre, test, post []api.LiteralTestStep @@ -68,10 +70,11 @@ func MultiStageTestStep( secretClient coreclientset.SecretsGetter, saClient coreclientset.ServiceAccountsGetter, rbacClient rbacclientset.RbacV1Interface, + isClient imageclientset.ImageStreamsGetter, artifactDir string, jobSpec *api.JobSpec, ) api.Step { - return newMultiStageTestStep(testConfig, config, params, podClient, secretClient, saClient, rbacClient, artifactDir, jobSpec) + return newMultiStageTestStep(testConfig, config, params, podClient, secretClient, saClient, rbacClient, isClient, artifactDir, jobSpec) } func newMultiStageTestStep( @@ -82,6 +85,7 @@ func newMultiStageTestStep( secretClient coreclientset.SecretsGetter, saClient coreclientset.ServiceAccountsGetter, rbacClient rbacclientset.RbacV1Interface, + isClient imageclientset.ImageStreamsGetter, artifactDir string, jobSpec *api.JobSpec, ) *multiStageTestStep { @@ -99,6 +103,7 @@ func newMultiStageTestStep( secretClient: secretClient, saClient: saClient, rbacClient: rbacClient, + isClient: isClient, artifactDir: artifactDir, jobSpec: jobSpec, pre: ms.Pre, @@ -179,6 +184,13 @@ func (s *multiStageTestStep) Requires() (ret []api.StepLink) { if link, ok := step.FromImageTag(); ok { internalLinks[link] = struct{}{} } + + for _, dependency := range step.Dependencies { + // we validate that the link will exist at config load time + // so we can safely ignore the case where !ok + imageStream, name, _ := s.config.DependencyParts(dependency) + ret = append(ret, api.LinkForImage(imageStream, name)) + } } for link := range internalLinks { ret = append(ret, api.InternalImageLink(link)) @@ -192,7 +204,7 @@ func (s *multiStageTestStep) Requires() (ret []api.StepLink) { } } if needsReleaseImage && !needsReleasePayload { - ret = append(ret, api.StableImagesLink(api.LatestStableName)) + ret = append(ret, api.ReleaseImagesLink(api.LatestReleaseName)) } return } @@ -335,11 +347,8 @@ func (s *multiStageTestStep) generatePods(steps []api.LiteralTestStep, env []cor if link, ok := step.FromImageTag(); ok { image = fmt.Sprintf("%s:%s", api.PipelineImageStream, link) } else { - if s.config.IsPipelineImage(image) || s.config.BuildsImage(image) { - image = fmt.Sprintf("%s:%s", api.PipelineImageStream, image) - } else { - image = fmt.Sprintf("%s:%s", api.StableImageStream, image) - } + stream, _ := s.config.ImageStreamFor(image) + image = fmt.Sprintf("%s:%s", stream, image) } resources, err := resourcesFor(step.Resources) if err != nil { @@ -365,6 +374,12 @@ func (s *multiStageTestStep) generatePods(steps []api.LiteralTestStep, env []cor }...) container.Env = append(container.Env, env...) container.Env = append(container.Env, s.generateParams(step.Environment)...) + depEnv, depErrs := s.envForDependencies(step) + if len(depErrs) != 0 { + errs = append(errs, depErrs...) + continue + } + container.Env = append(container.Env, depEnv...) if owner := s.jobSpec.Owner(); owner != nil { pod.OwnerReferences = append(pod.OwnerReferences, *owner) } @@ -381,6 +396,23 @@ func (s *multiStageTestStep) generatePods(steps []api.LiteralTestStep, env []cor return ret, utilerrors.NewAggregate(errs) } +func (s *multiStageTestStep) envForDependencies(step api.LiteralTestStep) ([]coreapi.EnvVar, []error) { + var env []coreapi.EnvVar + var errs []error + for _, dependency := range step.Dependencies { + imageStream, name, _ := s.config.DependencyParts(dependency) + ref, err := utils.ImageDigestFor(s.isClient, s.jobSpec.Namespace, imageStream, name)() + if err != nil { + errs = append(errs, fmt.Errorf("could not determine image pull spec for image %s on step %s", dependency.Name, step.As)) + continue + } + env = append(env, coreapi.EnvVar{ + Name: dependency.Env, Value: ref, + }) + } + return env, errs +} + func addSecretWrapper(pod *coreapi.Pod) { volume := "secret-wrapper" dir := "/tmp/secret-wrapper" diff --git a/pkg/steps/multi_stage_test.go b/pkg/steps/multi_stage_test.go index 1b8e79e0a65..4d547d85516 100644 --- a/pkg/steps/multi_stage_test.go +++ b/pkg/steps/multi_stage_test.go @@ -2,7 +2,6 @@ package steps import ( "context" - "github.com/google/go-cmp/cmp" "io/ioutil" "os" "path/filepath" @@ -10,6 +9,8 @@ import ( "testing" "time" + "github.com/google/go-cmp/cmp" + coreapi "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" meta "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -34,22 +35,22 @@ func TestRequires(t *testing.T) { steps api.MultiStageTestConfigurationLiteral req []api.StepLink }{{ - name: "step has a cluster profile and requires a release image, should not have StableImagesLink", + name: "step has a cluster profile and requires a release image, should not have ReleaseImagesLink", steps: api.MultiStageTestConfigurationLiteral{ ClusterProfile: api.ClusterProfileAWS, Test: []api.LiteralTestStep{{From: "from-release"}}, }, req: []api.StepLink{ - api.ReleasePayloadImageLink(api.LatestStableName), + api.ReleasePayloadImageLink(api.LatestReleaseName), api.ImagesReadyLink(), }, }, { - name: "step needs release images, should have StableImagesLink", + name: "step needs release images, should have ReleaseImagesLink", steps: api.MultiStageTestConfigurationLiteral{ Test: []api.LiteralTestStep{{From: "from-release"}}, }, req: []api.StepLink{ - api.StableImagesLink(api.LatestStableName), + api.ReleaseImagesLink(api.LatestReleaseName), }, }, { name: "step needs images, should have InternalImageLink", @@ -75,7 +76,7 @@ func TestRequires(t *testing.T) { t.Run(tc.name, func(t *testing.T) { step := MultiStageTestStep(api.TestStepConfiguration{ MultiStageTestConfigurationLiteral: &tc.steps, - }, &tc.config, api.NewDeferredParameters(), nil, nil, nil, nil, "", nil) + }, &tc.config, api.NewDeferredParameters(), nil, nil, nil, nil, nil, "", nil) ret := step.Requires() if len(ret) == len(tc.req) { matches := true @@ -127,7 +128,7 @@ func TestGeneratePods(t *testing.T) { }, } jobSpec.SetNamespace("namespace") - step := newMultiStageTestStep(config.Tests[0], &config, nil, nil, nil, nil, nil, "artifact_dir", &jobSpec) + step := newMultiStageTestStep(config.Tests[0], &config, nil, nil, nil, nil, nil, nil, "artifact_dir", &jobSpec) env := []coreapi.EnvVar{ {Name: "RELEASE_IMAGE_INITIAL", Value: "release:initial"}, {Name: "RELEASE_IMAGE_LATEST", Value: "release:latest"}, @@ -197,7 +198,7 @@ func TestGeneratePodsEnvironment(t *testing.T) { Test: test, Environment: tc.env, }, - }, &api.ReleaseBuildConfiguration{}, nil, nil, nil, nil, nil, "", &jobSpec) + }, &api.ReleaseBuildConfiguration{}, nil, nil, nil, nil, nil, nil, "", &jobSpec) pods, err := step.(*multiStageTestStep).generatePods(test, nil, false) if err != nil { t.Fatal(err) @@ -322,7 +323,7 @@ func TestRun(t *testing.T) { Post: []api.LiteralTestStep{{As: "post0"}, {As: "post1", OptionalOnSuccess: &yes}}, AllowSkipOnSuccess: &yes, }, - }, &api.ReleaseBuildConfiguration{}, nil, &fakePodClient{NewPodClient(client, nil, nil)}, client, client, fakecs.RbacV1(), "", &jobSpec) + }, &api.ReleaseBuildConfiguration{}, nil, &fakePodClient{NewPodClient(client, nil, nil)}, client, client, fakecs.RbacV1(), nil, "", &jobSpec) if err := step.Run(context.Background()); tc.failures == nil && err != nil { t.Error(err) return @@ -378,7 +379,7 @@ func TestArtifacts(t *testing.T) { {As: "test1", ArtifactDir: "/path/to/artifacts"}, }, }, - }, &api.ReleaseBuildConfiguration{}, nil, &fakePodClient{NewPodClient(client, nil, nil)}, client, client, fakecs.RbacV1(), tmp, &jobSpec) + }, &api.ReleaseBuildConfiguration{}, nil, &fakePodClient{NewPodClient(client, nil, nil)}, client, client, fakecs.RbacV1(), nil, tmp, &jobSpec) if err := step.Run(context.Background()); err != nil { t.Fatal(err) } @@ -455,7 +456,7 @@ func TestJUnit(t *testing.T) { Test: []api.LiteralTestStep{{As: "test0"}, {As: "test1"}}, Post: []api.LiteralTestStep{{As: "post0"}, {As: "post1"}}, }, - }, &api.ReleaseBuildConfiguration{}, nil, &fakePodClient{NewPodClient(client, nil, nil)}, client, client, fakecs.RbacV1(), "/dev/null", &jobSpec) + }, &api.ReleaseBuildConfiguration{}, nil, &fakePodClient{NewPodClient(client, nil, nil)}, client, client, fakecs.RbacV1(), nil, "/dev/null", &jobSpec) if err := step.Run(context.Background()); tc.failures == nil && err != nil { t.Error(err) return diff --git a/pkg/steps/output_image_tag.go b/pkg/steps/output_image_tag.go index 615bc06db8b..6285d123db9 100644 --- a/pkg/steps/output_image_tag.go +++ b/pkg/steps/output_image_tag.go @@ -71,7 +71,7 @@ func (s *outputImageTagStep) Requires() []api.StepLink { // latter only once images are built. However, in // specific configurations, authors may create an // execution graph where we race. - api.StableImagesLink(api.LatestStableName), + api.ReleaseImagesLink(api.LatestReleaseName), } } diff --git a/pkg/steps/output_image_tag_test.go b/pkg/steps/output_image_tag_test.go index 2489000b624..42b945b1f23 100644 --- a/pkg/steps/output_image_tag_test.go +++ b/pkg/steps/output_image_tag_test.go @@ -32,7 +32,7 @@ func TestOutputImageStep(t *testing.T) { name: "configToAs", requires: []api.StepLink{ api.InternalImageLink(config.From), - api.StableImagesLink(api.LatestStableName), + api.ReleaseImagesLink(api.LatestReleaseName), }, creates: []api.StepLink{ api.ExternalImageLink(config.To), diff --git a/pkg/steps/release/create_release.go b/pkg/steps/release/create_release.go index a4f8d8cebfd..2454d933208 100644 --- a/pkg/steps/release/create_release.go +++ b/pkg/steps/release/create_release.go @@ -139,7 +139,7 @@ func (s *assembleReleaseStep) run(ctx context.Context) error { return err } - streamName := api.StableStreamFor(s.name) + streamName := api.ReleaseStreamFor(s.name) var stable *imageapi.ImageStream var cvo string cvoExists := false @@ -229,10 +229,10 @@ oc adm release extract --from=%q --to=/tmp/artifacts/release-payload-%s } func (s *assembleReleaseStep) Requires() []api.StepLink { - if s.name == api.LatestStableName { + if s.name == api.LatestReleaseName { return []api.StepLink{api.ImagesReadyLink()} } - return []api.StepLink{api.StableImagesLink(s.name)} + return []api.StepLink{api.ReleaseImagesLink(s.name)} } func (s *assembleReleaseStep) Creates() []api.StepLink { diff --git a/pkg/steps/release/import_release.go b/pkg/steps/release/import_release.go index 7e98a4dfb80..18f46df7acc 100644 --- a/pkg/steps/release/import_release.go +++ b/pkg/steps/release/import_release.go @@ -77,7 +77,7 @@ func (s *importReleaseStep) run(ctx context.Context) error { return err } - streamName := api.StableStreamFor(s.name) + streamName := api.ReleaseStreamFor(s.name) log.Printf("Importing release image %s", s.name) @@ -370,10 +370,10 @@ func (s *importReleaseStep) Requires() []api.StepLink { // users to import images they care about rather than // having two steps overwrite each other on import if s.append { - if s.name == api.LatestStableName { + if s.name == api.LatestReleaseName { return []api.StepLink{api.ImagesReadyLink()} } - return []api.StepLink{api.StableImagesLink(api.LatestStableName)} + return []api.StepLink{api.ReleaseImagesLink(api.LatestReleaseName)} } // we don't depend on anything as we will populate // the stable streams with our images. diff --git a/pkg/steps/release/release_images.go b/pkg/steps/release/release_images.go index 422c67b1a6e..37b9cd9a684 100644 --- a/pkg/steps/release/release_images.go +++ b/pkg/steps/release/release_images.go @@ -71,7 +71,7 @@ func (s *stableImagesTagStep) Requires() []api.StepLink { return []api.StepLink{ func (s *stableImagesTagStep) Creates() []api.StepLink { // we can only ever create the latest stable image stream with this step - return []api.StepLink{api.StableImagesLink(api.LatestStableName)} + return []api.StepLink{api.ReleaseImagesLink(api.LatestReleaseName)} } func (s *stableImagesTagStep) Provides() api.ParameterMap { return nil } @@ -122,7 +122,7 @@ func (s *releaseImagesTagStep) run(ctx context.Context) error { is.UID = "" newIS := &imageapi.ImageStream{ ObjectMeta: meta.ObjectMeta{ - Name: api.StableStreamFor(api.LatestStableName), + Name: api.ReleaseStreamFor(api.LatestReleaseName), Annotations: map[string]string{}, }, Spec: imageapi.ImageStreamSpec{ @@ -144,7 +144,7 @@ func (s *releaseImagesTagStep) run(ctx context.Context) error { } initialIS := newIS.DeepCopy() - initialIS.Name = api.StableStreamFor(api.InitialImageStream) + initialIS.Name = api.ReleaseStreamFor(api.InitialReleaseName) _, err = s.client.ImageStreams(s.jobSpec.Namespace()).Create(ctx, newIS, meta.CreateOptions{}) if err != nil && !errors.IsAlreadyExists(err) { @@ -173,8 +173,8 @@ func (s *releaseImagesTagStep) Requires() []api.StepLink { func (s *releaseImagesTagStep) Creates() []api.StepLink { return []api.StepLink{ - api.StableImagesLink(api.InitialImageStream), - api.StableImagesLink(api.LatestStableName), + api.ReleaseImagesLink(api.InitialReleaseName), + api.ReleaseImagesLink(api.LatestReleaseName), } } diff --git a/pkg/steps/utils/env.go b/pkg/steps/utils/env.go index fdfac8a5a61..5736fb809ba 100644 --- a/pkg/steps/utils/env.go +++ b/pkg/steps/utils/env.go @@ -18,10 +18,10 @@ const ( ) var knownPrefixes = map[string]string{ - api.PipelineImageStream: pipelineEnvPrefix + imageEnvPrefix, - api.InitialImageStream: initialEnvPrefix + imageEnvPrefix, - api.StableImageStream: imageEnvPrefix, - api.ReleaseImageStream: releaseEnvPrefix + imageEnvPrefix, + api.PipelineImageStream: pipelineEnvPrefix + imageEnvPrefix, + api.ReleaseStreamFor(api.InitialReleaseName): initialEnvPrefix + imageEnvPrefix, + api.ReleaseStreamFor(api.LatestReleaseName): imageEnvPrefix, + api.ReleaseImageStream: releaseEnvPrefix + imageEnvPrefix, } func escapedImageName(name string) string { @@ -62,9 +62,9 @@ func LinkForEnv(envVar string) (api.StepLink, bool) { case IsStableImageEnv(envVar): // we don't know what will produce this parameter, // so we assume it will come from the release import - return api.StableImagesLink(api.LatestStableName), true + return api.ReleaseImagesLink(api.LatestReleaseName), true case IsInitialImageEnv(envVar): - return api.StableImagesLink(api.InitialImageStream), true + return api.ReleaseImagesLink(api.InitialReleaseName), true case IsReleaseImageEnv(envVar): return api.ReleasePayloadImageLink(ReleaseNameFrom(envVar)), true default: @@ -105,19 +105,19 @@ func IsPipelineImageEnv(envVar string) bool { // used to expose a pull spec for a stable ImageStreamTag // in the test namespace to test workloads. func StableImageEnv(name string) string { - return validatedEnvVarFor(api.StableImageStream, name) + return validatedEnvVarFor(api.ReleaseStreamFor(api.LatestReleaseName), name) } // IsStableImageEnv determines if an env var holds a pull // spec for a tag under the stable image stream func IsStableImageEnv(envVar string) bool { - return strings.HasPrefix(envVar, knownPrefixes[api.StableImageStream]) + return strings.HasPrefix(envVar, knownPrefixes[api.ReleaseStreamFor(api.LatestReleaseName)]) } // StableImageNameFrom gets an image name from an env name func StableImageNameFrom(envVar string) string { // we know that we will be able to unfurl - name, _ := imageFromEnv(api.StableImageStream, envVar) + name, _ := imageFromEnv(api.ReleaseStreamFor(api.LatestReleaseName), envVar) return name } @@ -125,13 +125,13 @@ func StableImageNameFrom(envVar string) string { // used to expose a pull spec for a initial ImageStreamTag // in the test namespace to test workloads. func InitialImageEnv(name string) string { - return validatedEnvVarFor(api.InitialImageStream, name) + return validatedEnvVarFor(api.ReleaseStreamFor(api.InitialReleaseName), name) } // IsInitialImageEnv determines if an env var holds a pull // spec for a tag under the initial image stream func IsInitialImageEnv(envVar string) bool { - return strings.HasPrefix(envVar, knownPrefixes[api.InitialImageStream]) + return strings.HasPrefix(envVar, knownPrefixes[api.ReleaseStreamFor(api.InitialReleaseName)]) } // ReleaseImageEnv determines the environment variable diff --git a/pkg/steps/utils/env_test.go b/pkg/steps/utils/env_test.go index 3387de22006..2e91db3a8a9 100644 --- a/pkg/steps/utils/env_test.go +++ b/pkg/steps/utils/env_test.go @@ -81,12 +81,12 @@ func TestLinkForEnv(t *testing.T) { }, { input: "IMAGE_COMPONENT", - output: api.StableImagesLink(api.LatestStableName), + output: api.ReleaseImagesLink(api.LatestReleaseName), valid: true, }, { input: "INITIAL_IMAGE_COMPONENT", - output: api.StableImagesLink(api.InitialImageStream), + output: api.ReleaseImagesLink(api.InitialReleaseName), valid: true, }, { diff --git a/pkg/webreg/webreg.go b/pkg/webreg/webreg.go index 66a63467d0f..028ea013b8e 100644 --- a/pkg/webreg/webreg.go +++ b/pkg/webreg/webreg.go @@ -729,6 +729,84 @@ to configure that test to run on a schedule, instead of as a pre-submit: Note that the build farms used to execute jobs run on UTC time, so time-of-day based cron schedules must be set with that in mind.

+ +

Referencing Images

+

+As ci-operator is OpenShift-native, all images used in a test workflow +are stored as ImageStreamTags. The following ImageStreams +will exist in the Namespace executing a test workflow: +

+ + + + + + + + + + + + + + + + + + + + + + +
ImageStreamDescription
pipelineInput images described with base_images and build_root as well as images holding built artifacts (such as src or bin) and output images as defined in images.
releaseTags of this ImageStreams hold OpenShift release payload images for installing and upgrading ephemeral OpenShift clusters for testing; a tag will be present for every named release configured in releases. If a tag_specification is provided, two tags will be present, :initial and :latest.
stable-<name>Images composing the release:name release payload, present when <name> is configured in releases.
stableSame as above, but for the release:latest release payload. Appropriate tags are overridden using the container images built during the test.
+ +

Referring to Images in ci-operator Configuration

+

+Inside of any ci-operator configuration file all images must be +referenced as an ImageStreamTag (stream:tag), but +may be referenced simply with the tag name. When an image is referenced with +a tag name, the tag will be resolved on the pipeline ImageStream, +if possible, falling back to the stable ImageStream +if not. For example, an image referenced as installer will use +pipeline:installer if that tag is present, falling back to +stable:installer if not. The following configuration fields +use this defaulting mechanism: +

+ + + +

Referring to Images in Tests

+

+ci-operator will run every part of a test as soon as possible, including +imports of external releases, builds of container images and test workflow steps. If a +workflow step runs in a container image that's imported or built in an earlier part of +a test, ci-operator will wait to schedule that test step until the image is +present. In some cases, however, it is necessary for a test command to refer to an image +that was built during the test workflow but not run inside of that container image itself. +In this case, the default scheduling algorithm needs to know that the step requires a +valid reference to exist before running. Test workloads can declare that they require +fully resolved pull specification as a digest for any image from the pipeline, +stable-<name> or release ImageStreams. +Tests may opt into having these environment variables present by declaring +dependencies in the ci-operator configuration for the test. +For instance, the example container test will be able to access the following +environment variables: +

+ + + +ci-operator configuration: +{{ yamlSyntax (index . "ciOperatorContainerTestWithDependenciesConfig") }} ` const ciOperatorInputConfig = `base_images: @@ -831,6 +909,19 @@ const ciOperatorContainerTestConfig = `tests: container: from: "src" # runs the commands in "pipeline:src" ` +const ciOperatorContainerTestWithDependenciesConfig = `tests: +- as: "vet" + commands: "test-script.sh ${BINARIES} ${MACHINE_CONFIG_OPERATOR} ${LATEST_RELEASE}" + container: + from: "src" + dependencies: + - name: "machine-config-operator" + env: "MACHINE_CONFIG_OPERATOR" + - name: "bin" + env: "BINARIES" + - name: "release:latest" + env: "LATEST_RELEASE" +` const ciOperatorPeriodicTestConfig = `tests: - as: "sanity" # names this test "sanity" @@ -2086,6 +2177,7 @@ func helpHandler(subPath string, w http.ResponseWriter, _ *http.Request) { data["ciOperatorReleaseConfig"] = ciOperatorReleaseConfig data["ciOperatorContainerTestConfig"] = ciOperatorContainerTestConfig data["ciOperatorPeriodicTestConfig"] = ciOperatorPeriodicTestConfig + data["ciOperatorContainerTestWithDependenciesConfig"] = ciOperatorContainerTestWithDependenciesConfig case "/leases": helpTemplate, err = helpFuncs.Parse(quotasAndLeasesPage) data["dynamicBoskosConfig"] = dynamicBoskosConfig diff --git a/test/e2e/multi-stage.sh b/test/e2e/multi-stage.sh index c7796e6b9e2..1839096c5a0 100755 --- a/test/e2e/multi-stage.sh +++ b/test/e2e/multi-stage.sh @@ -25,3 +25,11 @@ unset UNRESOLVED_CONFIG os::integration::configresolver::check_log os::test::junit::declare_suite_end + +os::test::junit::declare_suite_start "e2e/multi-stage/dependencies" +# This test validates the ci-operator can amend the graph with user input + +export JOB_SPEC='{"type":"postsubmit","job":"branch-ci-openshift-ci-tools-master-ci-operator-e2e","buildid":"0","prowjobid":"uuid","refs":{"org":"openshift","repo":"ci-tools","base_ref":"master","base_sha":"6d231cc37652e85e0f0e25c21088b73d644d89ad","pulls":[]}}' +os::cmd::expect_success "ci-operator --artifact-dir ${BASETMPDIR} --resolver-address http://127.0.0.1:8080 --target with-dependencies --unresolved-config ${suite_dir}/dependencies.yaml" +os::integration::configresolver::check_log +os::test::junit::declare_suite_end diff --git a/test/e2e/multi-stage/dependencies.yaml b/test/e2e/multi-stage/dependencies.yaml new file mode 100644 index 00000000000..3056dece428 --- /dev/null +++ b/test/e2e/multi-stage/dependencies.yaml @@ -0,0 +1,76 @@ +base_images: + os: + name: centos + namespace: openshift + tag: '7' +build_root: + image_stream_tag: + name: release + namespace: openshift + tag: golang-1.14 +resources: + '*': + limits: + cpu: 500m + requests: + cpu: 10m +tag_specification: + namespace: ocp + name: "4.5" +releases: + custom: + candidate: + product: okd + version: "4.3" +tests: + - as: with-dependencies + steps: + test: + - as: depend-on-stuff + commands: | + if [[ -z $SOURCE ]]; then + echo "ERROR: $SOURCE unset!" + exit 1 + elif [[ ! $SOURCE =~ .*ci-op-[a-z0-9]+/pipeline@sha256:.* ]]; then + echo "ERROR: SOURCE set to something unexpected: $SOURCE!" + exit 1 + fi + if [[ -z $INSTALLER ]]; then + echo "ERROR: INSTALLER unset!" + exit 1 + elif [[ ! $INSTALLER =~ .*ci-op-[a-z0-9]+/stable@sha256:.* ]]; then + echo "ERROR: INSTALLER set to something unexpected: $INSTALLER!" + exit 1 + fi + if [[ -z $COMMAND ]]; then + echo "ERROR: COMMAND unset!" + exit 1 + elif [[ ! $COMMAND =~ .*ci-op-[a-z0-9]+/stable-initial@sha256:.* ]]; then + echo "ERROR: COMMAND set to something unexpected: $COMMAND!" + exit 1 + fi + if [[ -z $RELEASE ]]; then + echo "ERROR: RELEASE unset!" + exit 1 + elif [[ ! $RELEASE =~ .*ci-op-[a-z0-9]+/release@sha256:.* ]]; then + echo "ERROR: RELEASE set to something unexpected: $RELEASE!" + exit 1 + fi + from: os + resources: + requests: + cpu: 100m + memory: 200Mi + dependencies: + - name: "src" + env: "SOURCE" + - name: "stable:installer" + env: "INSTALLER" + - name: "stable-initial:cli" + env: "COMMAND" + - name: "release:custom" + env: "RELEASE" +zz_generated_metadata: + branch: master + org: test + repo: test