-
Notifications
You must be signed in to change notification settings - Fork 65
Extend Node Image Info to handle streams #749
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
28c2fe1
release-controller: per-stream RPM helpers and machine-os stream disc…
sdodson 66eebab
rhcos: multi-stream CoreOS markdown and node image section
sdodson 82fc185
release-controller-api: fetch node image info when machine-os streams…
sdodson 8033282
hack: add changelog-preview helper; gitignore sample output
sdodson ae4ae49
Update pkg/release-controller/release_info.go
sdodson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| // changelog-preview runs the same ChangeLog + RHCOS markdown transforms as the release-controller API | ||
| // without needing a Kubernetes cluster. Requires `oc` on PATH and registry pull access to the | ||
| // release images you pass. | ||
| // | ||
| // By default it also appends the Node Image Info section (RPM lists and diffs per CoreOS stream | ||
| // when applicable), matching the web UI. Use --skip-node-info for changelog-only output. | ||
| // | ||
| // Example: | ||
| // | ||
| // go run ./hack/changelog-preview/ \ | ||
| // --from quay.io/openshift-release-dev/ocp-release@sha256:... \ | ||
| // --to quay.io/openshift-release-dev/ocp-release@sha256:... \ | ||
| // --from-tag 4.20.0-0.nightly-2025-01-01-000000 \ | ||
| // --to-tag 4.21.0-ec.1 | ||
| package main | ||
|
|
||
| import ( | ||
| "flag" | ||
| "fmt" | ||
| "os" | ||
|
|
||
| "github.com/openshift/release-controller/pkg/rhcos" | ||
| releasecontroller "github.com/openshift/release-controller/pkg/release-controller" | ||
| "k8s.io/client-go/kubernetes" | ||
| "k8s.io/client-go/rest" | ||
| "sigs.k8s.io/prow/pkg/jira" | ||
| ) | ||
|
|
||
| func main() { | ||
| from := flag.String("from", "", "from release image pull spec (digest or tag@repo)") | ||
| to := flag.String("to", "", "to release image pull spec") | ||
| fromTag := flag.String("from-tag", "previous", "from tag name (for markdown link substitution)") | ||
| toTag := flag.String("to-tag", "current", "to tag name (for markdown link substitution)") | ||
| arch := flag.String("arch", "amd64", "release architecture (amd64, arm64, ...)") | ||
| skipNode := flag.Bool("skip-node-info", false, "omit Node Image Info (faster; no extra oc rpmdb/image-for calls)") | ||
| flag.Parse() | ||
| if *from == "" || *to == "" { | ||
| fmt.Fprintf(os.Stderr, "usage: changelog-preview --from <pullspec> --to <pullspec> [flags]\n") | ||
| os.Exit(2) | ||
| } | ||
|
|
||
| var archName, archExt string | ||
| switch *arch { | ||
| case "amd64": | ||
| archName = "x86_64" | ||
| case "arm64": | ||
| archName = "aarch64" | ||
| archExt = fmt.Sprintf("-%s", archName) | ||
| default: | ||
| archName = *arch | ||
| archExt = fmt.Sprintf("-%s", archName) | ||
| } | ||
|
|
||
| var nilClient kubernetes.Interface | ||
| var nilCfg *rest.Config | ||
| info := releasecontroller.NewExecReleaseInfo(nilClient, nilCfg, "", "", func() (string, error) { return "", nil }, jira.Client(nil)) | ||
|
|
||
| out, err := info.ChangeLog(*from, *to, false) | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "ChangeLog: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
| out, err = rhcos.TransformMarkDownOutput(out, *fromTag, *toTag, archName, archExt) | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "TransformMarkDownOutput: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| if !*skipNode { | ||
| nodeMD, err := rhcos.NodeImageSectionMarkdown(info, *from, *to, out) | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "NodeImageSectionMarkdown: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
| if nodeMD != "" { | ||
| out = out + "\n\n## Node Image Info\n\n" + nodeMD | ||
| } | ||
| } | ||
|
|
||
| fmt.Print(out) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| package releasecontroller | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "sort" | ||
| "strings" | ||
| ) | ||
|
|
||
| // MachineOSStreamInfo describes one machine-OS image stream (base tag + optional display name from payload). | ||
| type MachineOSStreamInfo struct { | ||
| Tag string `json:"tag"` | ||
| DisplayName string `json:"displayName,omitempty"` | ||
| } | ||
|
|
||
| // releaseInfoImageRefs is the subset of `oc adm release info -o json` needed to list payload tags. | ||
| type releaseInfoImageRefs struct { | ||
| References struct { | ||
| Spec struct { | ||
| Tags []struct { | ||
| Name string `json:"name"` | ||
| Annotations map[string]string `json:"annotations"` | ||
| } `json:"tags"` | ||
| } `json:"spec"` | ||
| } `json:"references"` | ||
| } | ||
|
|
||
| const versionDisplayNamesKey = "io.openshift.build.version-display-names" | ||
|
|
||
| // ListMachineOSStreams returns machine-OS base tags discovered by pairing each *coreos* extensions | ||
| // image with its base tag. Display names come from io.openshift.build.version-display-names | ||
| // (machine-os=...) on the base image, as used in current OCP payloads (e.g. 4.21 nightlies). | ||
| // Convention: extensions tag is "<base>-extensions" (rhel-coreos-extensions, rhel-coreos-10-extensions). | ||
| func (r *ExecReleaseInfo) ListMachineOSStreams(releaseImage string) ([]MachineOSStreamInfo, error) { | ||
| raw, err := r.ReleaseInfo(releaseImage) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return machineOSStreamsFromReleaseJSON(raw) | ||
| } | ||
|
|
||
| // machineOSStreamsFromReleaseJSON parses release JSON for tests and shared logic. | ||
| func machineOSStreamsFromReleaseJSON(raw string) ([]MachineOSStreamInfo, error) { | ||
| var ri releaseInfoImageRefs | ||
| if err := json.Unmarshal([]byte(raw), &ri); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| tagSet := make(map[string]struct{}, len(ri.References.Spec.Tags)) | ||
| annByTag := make(map[string]map[string]string, len(ri.References.Spec.Tags)) | ||
| for _, t := range ri.References.Spec.Tags { | ||
| if t.Name == "" { | ||
| continue | ||
| } | ||
| tagSet[t.Name] = struct{}{} | ||
| if len(t.Annotations) > 0 { | ||
| annByTag[t.Name] = t.Annotations | ||
| } | ||
| } | ||
|
|
||
| var bases []string | ||
| for name := range tagSet { | ||
| if !strings.HasSuffix(name, "-extensions") { | ||
| continue | ||
| } | ||
| if !strings.Contains(name, "coreos") { | ||
| continue | ||
| } | ||
| base := strings.TrimSuffix(name, "-extensions") | ||
| if base == "" { | ||
| continue | ||
| } | ||
| if _, ok := tagSet[base]; !ok { | ||
| continue | ||
| } | ||
| bases = append(bases, base) | ||
| } | ||
|
|
||
| sortMachineOSTags(bases) | ||
| out := make([]MachineOSStreamInfo, 0, len(bases)) | ||
| for _, base := range bases { | ||
| dn := "" | ||
| if a, ok := annByTag[base]; ok { | ||
| dn = machineOSDisplayNameFromAnnotations(a) | ||
| } | ||
| out = append(out, MachineOSStreamInfo{Tag: base, DisplayName: dn}) | ||
| } | ||
| return out, nil | ||
| } | ||
|
|
||
| func machineOSDisplayNameFromAnnotations(annotations map[string]string) string { | ||
| v := strings.TrimSpace(annotations[versionDisplayNamesKey]) | ||
| if v == "" { | ||
| return "" | ||
| } | ||
| // Typical: "machine-os=Red Hat Enterprise Linux CoreOS" (single pair). | ||
| for _, part := range strings.Split(v, ",") { | ||
| part = strings.TrimSpace(part) | ||
| const prefix = "machine-os=" | ||
| if strings.HasPrefix(part, prefix) { | ||
| return strings.TrimSpace(strings.TrimPrefix(part, prefix)) | ||
| } | ||
| } | ||
| return "" | ||
| } | ||
|
|
||
| // MachineOSTitle returns a markdown subsection title for a stream (display name + tag in backticks). | ||
| func MachineOSTitle(s MachineOSStreamInfo) string { | ||
| if s.DisplayName != "" { | ||
| return fmt.Sprintf("%s (`%s`)", s.DisplayName, s.Tag) | ||
| } | ||
| switch s.Tag { | ||
| case "rhel-coreos": | ||
| return "Red Hat Enterprise Linux CoreOS (`rhel-coreos`)" | ||
| case "rhel-coreos-10": | ||
| return "Red Hat Enterprise Linux CoreOS 10 (`rhel-coreos-10`)" | ||
| case "stream-coreos": | ||
| return "Stream CoreOS (`stream-coreos`)" | ||
| default: | ||
| return fmt.Sprintf("Machine OS (`%s`)", s.Tag) | ||
| } | ||
| } | ||
|
|
||
| func sortMachineOSTags(tags []string) { | ||
| sort.SliceStable(tags, func(i, j int) bool { | ||
| return machineOSTagLess(tags[i], tags[j]) | ||
| }) | ||
| } | ||
|
|
||
| func machineOSTagLess(a, b string) bool { | ||
| prio := map[string]int{ | ||
| "rhel-coreos": 0, | ||
| "stream-coreos": 1, | ||
| } | ||
| pa, okA := prio[a] | ||
| pb, okB := prio[b] | ||
| switch { | ||
| case okA && okB: | ||
| return pa < pb | ||
| case okA: | ||
| return true | ||
| case okB: | ||
| return false | ||
| default: | ||
| return a < b | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| package releasecontroller | ||
|
|
||
| import ( | ||
| "reflect" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestMachineOSStreamsFromReleaseJSON_nightly421(t *testing.T) { | ||
| // Subset of oc adm release info -o json for registry.ci.openshift.org/ocp/release:4.21.0-0.nightly-2026-03-30-143812 | ||
| const raw = `{ | ||
| "references": { | ||
| "spec": { | ||
| "tags": [ | ||
| { | ||
| "name": "rhel-coreos", | ||
| "annotations": { | ||
| "io.openshift.build.version-display-names": "machine-os=Red Hat Enterprise Linux CoreOS", | ||
| "io.openshift.build.versions": "machine-os=9.6.20260327-0" | ||
| } | ||
| }, | ||
| { | ||
| "name": "rhel-coreos-10", | ||
| "annotations": { | ||
| "io.openshift.build.version-display-names": "machine-os=Red Hat Enterprise Linux CoreOS 10.2", | ||
| "io.openshift.build.versions": "machine-os=10.2.20260328-0" | ||
| } | ||
| }, | ||
| { | ||
| "name": "rhel-coreos-10-extensions", | ||
| "annotations": {} | ||
| }, | ||
| { | ||
| "name": "rhel-coreos-extensions", | ||
| "annotations": {} | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| }` | ||
|
|
||
| got, err := machineOSStreamsFromReleaseJSON(raw) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| want := []MachineOSStreamInfo{ | ||
| {Tag: "rhel-coreos", DisplayName: "Red Hat Enterprise Linux CoreOS"}, | ||
| {Tag: "rhel-coreos-10", DisplayName: "Red Hat Enterprise Linux CoreOS 10.2"}, | ||
| } | ||
| if !reflect.DeepEqual(got, want) { | ||
| t.Errorf("machineOSStreamsFromReleaseJSON() = %#v, want %#v", got, want) | ||
| } | ||
| } | ||
|
|
||
| func TestMachineOSDisplayNameFromAnnotations(t *testing.T) { | ||
| tests := []struct { | ||
| ann map[string]string | ||
| want string | ||
| }{ | ||
| {nil, ""}, | ||
| {map[string]string{versionDisplayNamesKey: "machine-os=Foo Bar"}, "Foo Bar"}, | ||
| {map[string]string{versionDisplayNamesKey: " machine-os=Foo Bar "}, "Foo Bar"}, | ||
| {map[string]string{versionDisplayNamesKey: "other=x, machine-os=CoreOS 10"}, "CoreOS 10"}, | ||
| } | ||
| for _, tt := range tests { | ||
| if got := machineOSDisplayNameFromAnnotations(tt.ann); got != tt.want { | ||
| t.Errorf("machineOSDisplayNameFromAnnotations(%v) = %q, want %q", tt.ann, got, tt.want) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestMachineOSTitle(t *testing.T) { | ||
| if got := MachineOSTitle(MachineOSStreamInfo{Tag: "rhel-coreos", DisplayName: "Red Hat Enterprise Linux CoreOS"}); got != "Red Hat Enterprise Linux CoreOS (`rhel-coreos`)" { | ||
| t.Errorf("got %q", got) | ||
| } | ||
| if got := MachineOSTitle(MachineOSStreamInfo{Tag: "custom-stream", DisplayName: ""}); got != "Machine OS (`custom-stream`)" { | ||
| t.Errorf("got %q", got) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in help text.
Line 30 says "digest or tag@repo" but the standard format is repository:tag or repository@digest (sha256:...). The help text should reflect this.
📝 Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents