Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions cmd/release-controller-api/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func (c *Controller) findReleaseStreamTags(includeStableTags bool, tags ...strin
// TODO: should be refactored to be unsortedSemanticReleaseTags
releaseTags := releasecontroller.SortedReleaseTags(r)
if includeStableTags {
if version, err := releasecontroller.SemverParseTolerant(r.Config.Name); err == nil || r.Config.As == releasecontroller.ReleaseConfigModeStable {
if version, err := releasecontroller.SemverParseTolerant(r.Config.Name); err == nil || r.Config.As == releasecontroller.ReleaseConfigModeStable || r.Config.As == releasecontroller.ReleaseConfigModeLayered {
stable.Releases = append(stable.Releases, releasecontroller.StableRelease{
Release: r,
Version: version,
Expand Down Expand Up @@ -1702,7 +1702,7 @@ func (c *Controller) tableLink(config *releasecontroller.ReleaseConfig, tag imag
}
if strings.Contains(tag.Name, "nightly") && c.doesInconsistencyExist(tag.Name) {
return fmt.Sprintf(`<td class="text-monospace"><a class="%s" href="/releasestream/%s/release/%s">%s</a> <a href="/releasestream/%s/inconsistency/%s"><i title="Inconsistency detected! Click for more details" class="bi bi-exclamation-circle"></i></a></td>`, alert, template.HTMLEscapeString(config.Name), template.HTMLEscapeString(tag.Name), template.HTMLEscapeString(tag.Name), template.HTMLEscapeString(config.Name), template.HTMLEscapeString(tag.Name))
} else if config.As == releasecontroller.ReleaseConfigModeStable {
} else if config.As == releasecontroller.ReleaseConfigModeStable || config.As == releasecontroller.ReleaseConfigModeLayered {
return fmt.Sprintf(`<td class="text-monospace"><a class="%s" style="padding-left:15px" href="/releasestream/%s/release/%s">%s</a></td>`, alert, template.HTMLEscapeString(config.Name), template.HTMLEscapeString(tag.Name), template.HTMLEscapeString(tag.Name))
} else {
return fmt.Sprintf(`<td class="text-monospace"><a class="%s" href="/releasestream/%s/release/%s">%s</a></td>`, alert, template.HTMLEscapeString(config.Name), template.HTMLEscapeString(tag.Name), template.HTMLEscapeString(tag.Name))
Expand Down Expand Up @@ -1747,7 +1747,7 @@ func (c *Controller) httpReleases(w http.ResponseWriter, req *http.Request) {
"publishDescription": func(r *ReleaseStream) string {
streamMessage := generateStreamMessage(r)
if len(streamMessage) > 0 {
if r.Release.Config.As == releasecontroller.ReleaseConfigModeStable {
if r.Release.Config.As == releasecontroller.ReleaseConfigModeStable || r.Release.Config.As == releasecontroller.ReleaseConfigModeLayered {
searchFunctionPrefix := removeSpecialCharacters(r.Release.Config.Name)
searchFunction := fmt.Sprintf("searchTable_%s('%s')", searchFunctionPrefix, searchFunctionPrefix)
return fmt.Sprintf("<div class=\"container\">\n<div class=\"row d-flex justify-content-between\">\n<div><p>%s</p></div>\n<div class=\"form-outline\"><input type=\"search\" class=\"form-control\" id=\"%s\" onkeyup=\"%s\" placeholder=\"Search\" aria-label=\"Search\"></div>\n</div>\n</div>", streamMessage, searchFunctionPrefix, searchFunction)
Expand All @@ -1760,6 +1760,10 @@ func (c *Controller) httpReleases(w http.ResponseWriter, req *http.Request) {
if len(streamMessage) == 0 {
out = append(out, `<span>stable tags</span>`)
}
case releasecontroller.ReleaseConfigModeLayered:
if len(streamMessage) == 0 {
out = append(out, `<span>layered releases</span>`)
}
default:
out = append(out, fmt.Sprintf(`<span>updated when <code>%s/%s</code> changes</span>`, r.Release.Source.Namespace, r.Release.Source.Name))
}
Expand Down Expand Up @@ -1846,7 +1850,7 @@ func (c *Controller) httpReleases(w http.ResponseWriter, req *http.Request) {
Tags: releasecontroller.SortedReleaseTags(r),
}
var delays []string
if r.Config.As != releasecontroller.ReleaseConfigModeStable && len(s.Tags) > 0 {
if r.Config.As != releasecontroller.ReleaseConfigModeStable && r.Config.As != releasecontroller.ReleaseConfigModeLayered && len(s.Tags) > 0 {
if ok, _, queueAfter := releasecontroller.IsReleaseDelayedForInterval(r, s.Tags[0]); ok {
delays = append(delays, fmt.Sprintf("waiting for %s", queueAfter.Truncate(time.Second)))
}
Expand All @@ -1857,7 +1861,7 @@ func (c *Controller) httpReleases(w http.ResponseWriter, req *http.Request) {
if len(delays) > 0 {
s.Delayed = &ReleaseDelay{Message: fmt.Sprintf("Next release may not start: %s", strings.Join(delays, ", "))}
}
if r.Config.As != releasecontroller.ReleaseConfigModeStable {
if r.Config.As != releasecontroller.ReleaseConfigModeStable && r.Config.As != releasecontroller.ReleaseConfigModeLayered {
s.Upgrades = calculateReleaseUpgrades(r, s.Tags, c.graph, false)
}
page.Streams = append(page.Streams, s)
Expand Down Expand Up @@ -2214,7 +2218,7 @@ func (c *Controller) httpReleaseStreamTable(w http.ResponseWriter, req *http.Req
"publishDescription": func(r *ReleaseStream) string {
streamMessage := generateStreamMessage(r)
if len(streamMessage) > 0 {
if r.Release.Config.As == releasecontroller.ReleaseConfigModeStable {
if r.Release.Config.As == releasecontroller.ReleaseConfigModeStable || r.Release.Config.As == releasecontroller.ReleaseConfigModeLayered {
searchFunctionPrefix := removeSpecialCharacters(r.Release.Config.Name)
searchFunction := fmt.Sprintf("searchTable_%s('%s')", searchFunctionPrefix, searchFunctionPrefix)
return fmt.Sprintf("<div class=\"container\">\n<div class=\"row d-flex justify-content-between\">\n<div><p>%s</p></div>\n<div class=\"form-outline\"><input type=\"search\" class=\"form-control\" id=\"%s\" onkeyup=\"%s\" placeholder=\"Search\" aria-label=\"Search\"></div>\n</div>\n</div>", streamMessage, searchFunctionPrefix, searchFunction)
Expand All @@ -2227,6 +2231,10 @@ func (c *Controller) httpReleaseStreamTable(w http.ResponseWriter, req *http.Req
if len(streamMessage) == 0 {
out = append(out, `<span>stable tags</span>`)
}
case releasecontroller.ReleaseConfigModeLayered:
if len(streamMessage) == 0 {
out = append(out, `<span>layered releases</span>`)
}
default:
out = append(out, fmt.Sprintf(`<span>updated when <code>%s/%s</code> changes</span>`, r.Release.Source.Namespace, r.Release.Source.Name))
}
Expand Down Expand Up @@ -2305,7 +2313,7 @@ func (c *Controller) httpReleaseStreamTable(w http.ResponseWriter, req *http.Req
Tags: releasecontroller.SortedReleaseTags(r),
}
var delays []string
if r.Config.As != releasecontroller.ReleaseConfigModeStable && len(s.Tags) > 0 {
if r.Config.As != releasecontroller.ReleaseConfigModeStable && r.Config.As != releasecontroller.ReleaseConfigModeLayered && len(s.Tags) > 0 {
if ok, _, queueAfter := releasecontroller.IsReleaseDelayedForInterval(r, s.Tags[0]); ok {
delays = append(delays, fmt.Sprintf("waiting for %s", queueAfter.Truncate(time.Second)))
}
Expand All @@ -2316,7 +2324,7 @@ func (c *Controller) httpReleaseStreamTable(w http.ResponseWriter, req *http.Req
if len(delays) > 0 {
s.Delayed = &ReleaseDelay{Message: fmt.Sprintf("Next release may not start: %s", strings.Join(delays, ", "))}
}
if r.Config.As != releasecontroller.ReleaseConfigModeStable {
if r.Config.As != releasecontroller.ReleaseConfigModeStable && r.Config.As != releasecontroller.ReleaseConfigModeLayered {
s.Upgrades = calculateReleaseUpgrades(r, s.Tags, c.graph, false)
}
page.TargetStream = s
Expand Down Expand Up @@ -2377,6 +2385,10 @@ func (c *Controller) httpDashboardOverview(w http.ResponseWriter, req *http.Requ
if len(streamMessage) == 0 {
out = append(out, `<span>stable tags</span>`)
}
case releasecontroller.ReleaseConfigModeLayered:
if len(streamMessage) == 0 {
out = append(out, `<span>layered releases</span>`)
}
default:
out = append(out, fmt.Sprintf(`<span>updated when <code>%s/%s</code> changes</span>`, r.Release.Source.Namespace, r.Release.Source.Name))
}
Expand Down Expand Up @@ -2444,7 +2456,7 @@ func (c *Controller) httpDashboardOverview(w http.ResponseWriter, req *http.Requ
Tags: releasecontroller.SortedReleaseTags(r),
}
var delays []string
if r.Config.As != releasecontroller.ReleaseConfigModeStable && len(s.Tags) > 0 {
if r.Config.As != releasecontroller.ReleaseConfigModeStable && r.Config.As != releasecontroller.ReleaseConfigModeLayered && len(s.Tags) > 0 {
if ok, _, queueAfter := releasecontroller.IsReleaseDelayedForInterval(r, s.Tags[0]); ok {
delays = append(delays, fmt.Sprintf("waiting for %s", queueAfter.Truncate(time.Second)))
}
Expand All @@ -2463,7 +2475,7 @@ func (c *Controller) httpDashboardOverview(w http.ResponseWriter, req *http.Requ
if len(delays) > 0 {
s.Delayed = &ReleaseDelay{Message: fmt.Sprintf("Next release may not start: %s", strings.Join(delays, ", "))}
}
if r.Config.As != releasecontroller.ReleaseConfigModeStable {
if r.Config.As != releasecontroller.ReleaseConfigModeStable && r.Config.As != releasecontroller.ReleaseConfigModeLayered {
s.Upgrades = calculateReleaseUpgrades(r, s.Tags, c.graph, true)
}
page.Streams = append(page.Streams, s)
Expand Down
2 changes: 1 addition & 1 deletion cmd/release-controller-api/http_candidate.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ func (c *Controller) findReleaseByName(includeStableTags bool, names ...string)
}

if includeStableTags {
if version, err := releasecontroller.SemverParseTolerant(r.Config.Name); err == nil || r.Config.As == releasecontroller.ReleaseConfigModeStable {
if version, err := releasecontroller.SemverParseTolerant(r.Config.Name); err == nil || r.Config.As == releasecontroller.ReleaseConfigModeStable || r.Config.As == releasecontroller.ReleaseConfigModeLayered {
stable.Releases = append(stable.Releases, releasecontroller.StableRelease{
Release: r,
Version: version,
Expand Down
6 changes: 3 additions & 3 deletions cmd/release-controller-api/http_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -717,7 +717,7 @@ func renderAlerts(release ReleaseStream) string {
func releaseJoin(streams []ReleaseStream, showStableReleases bool) string {
releases := []string{}
for _, s := range streams {
if !showStableReleases && s.Release.Config.As == releasecontroller.ReleaseConfigModeStable {
if !showStableReleases && (s.Release.Config.As == releasecontroller.ReleaseConfigModeStable || s.Release.Config.As == releasecontroller.ReleaseConfigModeLayered) {
continue
}
releases = append(releases, fmt.Sprintf("<a href=\"#%s\">%s</a>", template.HTMLEscapeString(s.Release.Config.Name), template.HTMLEscapeString(s.Release.Config.Name)))
Expand Down Expand Up @@ -939,7 +939,7 @@ func (r preferredReleases) Less(i, j int) bool {
if a.Release.Config.Hide && !b.Release.Config.Hide {
return false
}
aStable, bStable := a.Release.Config.As == releasecontroller.ReleaseConfigModeStable, b.Release.Config.As == releasecontroller.ReleaseConfigModeStable
aStable, bStable := (a.Release.Config.As == releasecontroller.ReleaseConfigModeStable || a.Release.Config.As == releasecontroller.ReleaseConfigModeLayered), (b.Release.Config.As == releasecontroller.ReleaseConfigModeStable || b.Release.Config.As == releasecontroller.ReleaseConfigModeLayered)
if aStable && !bStable {
return true
}
Expand Down Expand Up @@ -1104,7 +1104,7 @@ func isStaleStatusTag(tag imagev1.NamedTagEventList, target *imagev1.ImageStream
func pruneEndOfLifeTags(page *ReleasePage, endOfLifePrefixes sets.Set[string]) {
for i := range page.Streams {
stream := &page.Streams[i]
if stream.Release.Config.As == releasecontroller.ReleaseConfigModeStable {
if stream.Release.Config.As == releasecontroller.ReleaseConfigModeStable || stream.Release.Config.As == releasecontroller.ReleaseConfigModeLayered {
var tags []*imagev1.TagReference
for _, tag := range stream.Tags {
if version, err := releasecontroller.SemverParseTolerant(tag.Name); err == nil {
Expand Down
149 changes: 149 additions & 0 deletions cmd/release-controller/layered_mode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package main

import (
"testing"

releasecontroller "github.com/openshift/release-controller/pkg/release-controller"
)

func TestLayeredModeConfiguration(t *testing.T) {
testCases := []struct {
name string
configJSON string
expectError bool
errorMsg string
}{
{
name: "Valid layered mode without 'to' field",
configJSON: `{"name": "test-layered", "as": "Layered"}`,
expectError: false,
},
{
name: "Layered mode with optional 'to' field is allowed",
configJSON: `{"name": "test-layered", "as": "Layered", "to": "releases"}`,
expectError: false,
},
{
name: "Stable mode without 'to' field is valid",
configJSON: `{"name": "test-stable", "as": "Stable"}`,
expectError: false,
},
{
name: "Integration mode without 'to' field should error",
configJSON: `{"name": "test-integration"}`,
expectError: true,
errorMsg: "release must specify 'to' unless 'as' is 'Stable' or 'Layered'",
},
{
name: "Integration mode with 'to' field is valid",
configJSON: `{"name": "test-integration", "to": "releases"}`,
expectError: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
config, err := releasecontroller.ParseReleaseConfig(tc.configJSON, nil)

if tc.expectError {
if err == nil {
t.Errorf("Expected error but got none")
return
}
if tc.errorMsg != "" && err.Error() != tc.errorMsg {
t.Errorf("Expected error message %q, got %q", tc.errorMsg, err.Error())
}
return
}

if err != nil {
t.Errorf("Expected no error but got: %v", err)
return
}

if config == nil {
t.Errorf("Expected valid config but got nil")
return
}
})
}
}

func TestExternalRegistryPublishValidation(t *testing.T) {
testCases := []struct {
name string
configJSON string
expectError bool
errorMsg string
}{
{
name: "Valid external registry publish configuration",
configJSON: `{"name": "test", "to": "releases", "publish": {"ext-mirror": {"externalRegistry": {"registry": "quay.io/test/repo", "secretName": "test-secret"}}}}`,
expectError: false,
},
{
name: "External registry publish missing registry",
configJSON: `{"name": "test", "to": "releases", "publish": {"ext-mirror": {"externalRegistry": {"secretName": "test-secret"}}}}`,
expectError: true,
errorMsg: "externalRegistry publish for ext-mirror has no registry",
},
{
name: "External registry publish missing secretName",
configJSON: `{"name": "test", "to": "releases", "publish": {"ext-mirror": {"externalRegistry": {"registry": "quay.io/test/repo"}}}}`,
expectError: true,
errorMsg: "externalRegistry publish for ext-mirror has no secretName",
},
{
name: "External registry publish with empty registry",
configJSON: `{"name": "test", "to": "releases", "publish": {"ext-mirror": {"externalRegistry": {"registry": "", "secretName": "test-secret"}}}}`,
expectError: true,
errorMsg: "externalRegistry publish for ext-mirror has no registry",
},
{
name: "External registry publish with empty secretName",
configJSON: `{"name": "test", "to": "releases", "publish": {"ext-mirror": {"externalRegistry": {"registry": "quay.io/test/repo", "secretName": ""}}}}`,
expectError: true,
errorMsg: "externalRegistry publish for ext-mirror has no secretName",
},
{
name: "External registry publish with optional fields",
configJSON: `{"name": "test", "to": "releases", "publish": {"ext-mirror": {"externalRegistry": {"registry": "quay.io/test/repo", "secretName": "test-secret", "tags": ["latest", "v1.0"], "excludeTags": ["dev"]}}}}`,
expectError: false,
},
{
name: "External registry publish with override CLI image",
configJSON: `{"name": "test", "to": "releases", "publish": {"ext-mirror": {"externalRegistry": {"registry": "quay.io/test/repo", "secretName": "test-secret", "overrideCLIImage": "quay.io/openshift/cli:latest"}}}}`,
expectError: false,
},
{
name: "Layered mode with external registry and override CLI image",
configJSON: `{"name": "test-layered", "as": "Layered", "publish": {"ext-mirror": {"externalRegistry": {"registry": "quay.io/test/layered", "secretName": "test-secret", "overrideCLIImage": "registry.ci.openshift.org/ocp/4.17:cli"}}}}`,
expectError: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
config, err := releasecontroller.ParseReleaseConfig(tc.configJSON, nil)

if tc.expectError {
if err == nil {
t.Errorf("Expected error but got none")
return
}

if tc.errorMsg != "" && err.Error() != tc.errorMsg {
t.Errorf("Expected error message %q, got %q", tc.errorMsg, err.Error())
}
} else {
if err != nil {
t.Errorf("Expected no error but got: %v", err)
}

if config == nil {
t.Errorf("Expected valid config but got nil")
}
}
})
}
}
Loading