diff --git a/cmd/release-controller-api/controller.go b/cmd/release-controller-api/controller.go
index d3525afd3..0dbf8ee12 100644
--- a/cmd/release-controller-api/controller.go
+++ b/cmd/release-controller-api/controller.go
@@ -4,6 +4,7 @@ import (
imagev1 "github.com/openshift/api/image/v1"
releasepayloadlister "github.com/openshift/release-controller/pkg/client/listers/release/v1alpha1"
releasecontroller "github.com/openshift/release-controller/pkg/release-controller"
+ "github.com/openshift/release-controller/pkg/releasequalifiers"
lru "github.com/hashicorp/golang-lru"
@@ -66,6 +67,9 @@ type Controller struct {
releasePayloadNamespace string
releasePayloadLister releasepayloadlister.ReleasePayloadLister
+ // configAccessor provides access to release qualifiers configuration (optional, may be nil)
+ configAccessor releasequalifiers.ConfigAccessor
+
// All image streams from app.ci cluster
imageStreams []*imagev1.ImageStream
}
@@ -81,6 +85,7 @@ func NewController(
artSuffix string,
releasePayloadNamespace string,
releasePayloadLister releasepayloadlister.ReleasePayloadLister,
+ configAccessor releasequalifiers.ConfigAccessor,
) *Controller {
// log events at v2 and send them to the server
broadcaster := record.NewBroadcaster()
@@ -115,6 +120,8 @@ func NewController(
releasePayloadNamespace: releasePayloadNamespace,
releasePayloadLister: releasePayloadLister,
+
+ configAccessor: configAccessor,
}
c.dashboards = []Dashboard{
diff --git a/cmd/release-controller-api/http.go b/cmd/release-controller-api/http.go
index 066427086..c846ee30a 100644
--- a/cmd/release-controller-api/http.go
+++ b/cmd/release-controller-api/http.go
@@ -21,6 +21,7 @@ import (
"time"
"github.com/openshift/release-controller/pkg/apis/release/v1alpha1"
+ "github.com/openshift/release-controller/pkg/releasequalifiers"
releasecontroller "github.com/openshift/release-controller/pkg/release-controller"
"github.com/openshift/release-controller/pkg/rhcos"
@@ -177,6 +178,7 @@ func (c *Controller) userInterfaceHandler() http.Handler {
mux.HandleFunc("/releasestream/{release}/latest", c.httpReleaseLatest)
mux.HandleFunc("/releasestream/{release}/latest/download", c.httpReleaseLatestDownload)
mux.HandleFunc("/releasestream/{release}/candidates", c.httpReleaseCandidateList)
+ mux.HandleFunc("/releasestream/{release}/qualifier/{qualifier}", c.httpQualifierStatus)
mux.HandleFunc("/dashboards/overview", c.httpDashboardOverview)
mux.HandleFunc("/dashboards/compare", c.httpDashboardCompare)
@@ -187,11 +189,14 @@ func (c *Controller) userInterfaceHandler() http.Handler {
mux.HandleFunc("/api/v1/releasestream/{release}/candidate", c.apiReleaseCandidate)
mux.HandleFunc("/api/v1/releasestream/{release}/release/{tag}", c.apiReleaseInfo)
mux.HandleFunc("/api/v1/releasestream/{release}/config", c.apiReleaseConfig)
+ mux.HandleFunc("/api/v1/releasestream/{release}/qualifier/{qualifier}/config", c.apiQualifierConfig)
mux.HandleFunc("/api/v1/releasestreams/accepted", c.apiAcceptedStreams)
mux.HandleFunc("/api/v1/releasestreams/rejected", c.apiRejectedStreams)
mux.HandleFunc("/api/v1/releasestreams/all", c.apiAllStreams)
mux.HandleFunc("/api/v1/releasestreams/approvals", c.apiReleaseApprovals)
+ mux.HandleFunc("/api/v1/releasetag/{tag}/qualifiers", c.apiReleaseQualifiersSummaries)
+
//mux.HandleFunc("/api/v1/features/{tag}", c.apiFeatureInfo)
//mux.HandleFunc("/features/{tag}", c.httpFeatureInfo)
@@ -1432,12 +1437,22 @@ func (c *Controller) httpReleaseInfo(w http.ResponseWriter, req *http.Request) {
renderInstallInstructions(w, tagInfo.Info.Tag, tagInfo.TagPullSpec, c.artifactsHost)
}
- fmt.Fprintf(w, "Team Approvals: ")
- teamApprovedList := c.renderTeamApprovals(tagInfo.Tag, true)
- if teamApprovedList == "" {
- fmt.Fprintf(w, "None
")
+ qualifierStatusAPI := fmt.Sprintf(`(status api)`, template.HTMLEscapeString(url.PathEscape(tagInfo.Tag)))
+ qualifierBadges := c.renderQualifierBadges(tagInfo.Tag, true)
+ if qualifierBadges == "" {
+ fmt.Fprintf(w, `
Qualifiers %s: None
`, qualifierStatusAPI) } else { - fmt.Fprint(w, "Qualifiers %s:
Failure Labels:
ID: %s
`, template.HTMLEscapeString(string(qualifierID))) + } + fmt.Fprintf(w, `Stream: %s
`, template.HTMLEscapeString(streamName)) + fmt.Fprintf(w, `Payload: %s
`, + template.HTMLEscapeString(url.PathEscape(payload.Name)), template.HTMLEscapeString(payload.Name)) + + if c.configAccessor != nil { + if globalConfig := c.configAccessor.Get(); globalConfig != nil { + if qualifier, ok := globalConfig[qualifierID]; ok { + if qualifier.Summary != "" { + fmt.Fprintf(w, `Summary: %s
`, template.HTMLEscapeString(qualifier.Summary)) + } + if qualifier.Description != "" { + fmt.Fprintf(w, `Description: %s
`, template.HTMLEscapeString(qualifier.Description)) + } + } } - approvals.WriteString("Overall Status: %s
`, + badgeClass, template.HTMLEscapeString(qualifierStateText(summary))) + + // Failure Labels for this specific qualifier (from merged config) + if len(summary.FailureLabels) > 0 { + fmt.Fprintf(w, `Failure Labels:
No jobs found for this qualifier.
`) + } else { + fmt.Fprintf(w, `| Job Name | Prowjob | Status | Attempts | Last Run | Config | `) + fmt.Fprintf(w, `|||||
|---|---|---|---|---|---|---|---|---|---|---|
| %s | `, template.HTMLEscapeString(jobRef.CIConfigurationName)) + fmt.Fprintf(w, `%s | `, template.HTMLEscapeString(jobRef.CIConfigurationJobName)) + + if js != nil { + var jobBadgeClass string + switch js.AggregateState { + case v1alpha1.JobStateSuccess: + jobBadgeClass = "badge-success" + case v1alpha1.JobStateFailure: + jobBadgeClass = "badge-danger" + default: + jobBadgeClass = "badge-warning" + } + fmt.Fprintf(w, `%s | `, + jobBadgeClass, template.HTMLEscapeString(string(js.AggregateState))) + fmt.Fprintf(w, `%d/%d | `, len(js.JobRunResults), 1+js.MaxRetries) + + // Last run link + if len(js.JobRunResults) > 0 { + lastRun := js.JobRunResults[len(js.JobRunResults)-1] + if lastRun.HumanProwResultsURL != "" { + fmt.Fprintf(w, `View | `, + template.HTMLEscapeString(lastRun.HumanProwResultsURL)) + } else { + fmt.Fprintf(w, `- | `) + } + } else { + fmt.Fprintf(w, `- | `) + } + } else { + fmt.Fprintf(w, `- | - | - | `) + } + configURL := fmt.Sprintf("/api/v1/releasestream/%s/qualifier/%s/config?job=%s", + url.PathEscape(streamName), url.PathEscape(string(qualifierID)), + url.QueryEscape(jobRef.CIConfigurationName)) + fmt.Fprintf(w, `View | `, + template.HTMLEscapeString(configURL)) + fmt.Fprintf(w, `
| Thread | Jira Ticket | Escalation | Priority | Status | Last Transition | `) + fmt.Fprintf(w, `||
|---|---|---|---|---|---|---|---|
| %s | `, template.HTMLEscapeString(threadID)) + + if notif.IssueKey != "" { + fmt.Fprintf(w, `%s | `, + template.HTMLEscapeString(notif.IssueKey), template.HTMLEscapeString(notif.IssueKey)) + } else { + fmt.Fprintf(w, `- | `) + } + + fmt.Fprintf(w, `%s | `, template.HTMLEscapeString(notif.ActiveEscalation)) + + var priorityBadgeClass string + switch strings.ToLower(notif.ActivePriority) { + case "critical", "high": + priorityBadgeClass = "badge-danger" + case "normal": + priorityBadgeClass = "badge-warning" + default: + priorityBadgeClass = "badge-info" + } + fmt.Fprintf(w, `%s | `, + priorityBadgeClass, template.HTMLEscapeString(notif.ActivePriority)) + + if notif.Abated { + fmt.Fprintf(w, `Abated | `) + } else { + fmt.Fprintf(w, `Active | `) + } + + fmt.Fprintf(w, `%s | `, template.HTMLEscapeString(notif.LastTransitionTime.Format(time.RFC3339))) + fmt.Fprintf(w, `
Tests:
Tests:
No tests for this release`) } } -func (c *Controller) renderVerificationJobsList(jobs releasecontroller.VerificationStatusMap, release *releasecontroller.Release, tag imagev1.TagReference, final bool) string { +// getJobQualifierMap builds a reverse mapping from job CIConfigurationName to qualifier IDs +func (c *Controller) getJobQualifierMap(tagName string) map[string][]releasequalifiers.QualifierId { + payload := c.GetReleasePayload(tagName) + if payload == nil || payload.Status.QualifiersSummary == nil || len(payload.Status.QualifiersSummary.Qualifiers) == 0 { + return nil + } + result := make(map[string][]releasequalifiers.QualifierId) + for qID, summary := range payload.Status.QualifiersSummary.Qualifiers { + for _, job := range summary.Jobs { + result[job.CIConfigurationName] = append(result[job.CIConfigurationName], qID) + } + } + // Sort qualifier IDs for deterministic display + for _, qIDs := range result { + slices.Sort(qIDs) + } + return result +} + +// renderJobQualifierBadges renders small inline badge indicators for qualifiers associated with a job +func renderJobQualifierBadges(jobName string, jobQualifiers map[string][]releasequalifiers.QualifierId) string { + if jobQualifiers == nil { + return "" + } + qIDs, ok := jobQualifiers[jobName] + if !ok || len(qIDs) == 0 { + return "" + } + var buf bytes.Buffer + for _, qID := range qIDs { + fmt.Fprintf(&buf, ` %s`, + template.HTMLEscapeString(string(qID)), template.HTMLEscapeString(string(qID))) + } + return buf.String() +} + +func (c *Controller) renderVerificationJobsList(jobs releasecontroller.VerificationStatusMap, release *releasecontroller.Release, tag imagev1.TagReference, final bool, jobQualifiers map[string][]releasequalifiers.QualifierId) string { buf := &bytes.Buffer{} keys := make([]string, 0, len(jobs)) for k := range jobs { @@ -532,7 +574,9 @@ func (c *Controller) renderVerificationJobsList(jobs releasecontroller.Verificat if !verificationJobs[key].Disabled && !final { buf.WriteString("