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.
+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:
+
ImageStream |
+ Description | +
|---|---|
pipeline |
+ Input 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. |
+
release |
+ Tags 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> |
+
stable |
+ Same as above, but for the release:latest release payload. Appropriate tags are overridden using the container images built during the test. |
+
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:
+
images[*].from: configuring the base FROM which an image buildspromotion.additional_images: configuring which images are publishedpromotion.excluded_images: configuring which images are not publishedtests[*].container.from: configuring the container image in which a single-stage test runstests[*].steps.{pre,test,post}[*].from: configuring the container image which some part of a multi-stage test runs
+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:
+
${MACHINE_CONFIG_OPERATOR}: exposing the pull specification of the stable:machine-config-operator ImageStreamTag${BINARIES}: exposing the pull specification of the pipeline:bin ImageStreamTag${LATEST_RELEASE}: exposing the pull specification of the release:latest payload ImageStreamTagci-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