Skip to content

Commit a077100

Browse files
committed
pkg/cincinnati: Centralize release metadata parsing
Instead of carring similar code in the payload and Cincinnati packages, centralize in one place to make maintenance easier. And with the logic centralized, I've also put some time into hardening the channel parsing, to grumble about some possible issues. For payload loading, errors get a logged warning, but are not fatal. Having the CVO come up with logged warnings still allows cluster admins to update into a fix. But a crash-looping CVO would not notice ClusterVersion spec.desiredUpdate changes or be able to roll out a requested update into a fix. So this branch processes as much of the metadata as it can (to not let, for example, an invalid URL block us from loading architecture information), logs the warning about what it failed to parse, and continues on. For Cincinnati processing, errors are fatal. Cluster admins can take the ClusterVersion RetrievedUpdates=False message and complain to their Update Service maintainers, who can fix the metadata.
1 parent b21fbd2 commit a077100

File tree

3 files changed

+67
-60
lines changed

3 files changed

+67
-60
lines changed

pkg/cincinnati/cincinnati.go

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cincinnati
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"io"
89
"net/http"
@@ -289,9 +290,9 @@ type graph struct {
289290
}
290291

291292
type node struct {
292-
Version semver.Version `json:"version"`
293-
Image string `json:"payload"`
294-
Metadata map[string]string `json:"metadata,omitempty"`
293+
Version semver.Version `json:"version"`
294+
Image string `json:"payload"`
295+
Metadata map[string]interface{} `json:"metadata,omitempty"`
295296
}
296297

297298
type edge struct {
@@ -329,28 +330,65 @@ func (e *edge) UnmarshalJSON(data []byte) error {
329330
}
330331

331332
func convertRetrievedUpdateToRelease(update node) (configv1.Release, error) {
332-
cvoUpdate := configv1.Release{
333-
Version: update.Version.String(),
334-
Image: update.Image,
335-
}
336-
if urlString, ok := update.Metadata["url"]; ok {
337-
_, err := url.Parse(urlString)
338-
if err != nil {
339-
return cvoUpdate, fmt.Errorf("invalid URL for %s: %s", cvoUpdate.Version, err)
333+
release, err := ParseMetadata(update.Metadata)
334+
release.Version = update.Version.String()
335+
release.Image = update.Image
336+
return release, err
337+
}
338+
339+
// ParseMetadata parses release metadata (URL, channels, etc.). It does not populate the version or image properties.
340+
func ParseMetadata(metadata map[string]interface{}) (configv1.Release, error) {
341+
release := configv1.Release{}
342+
errs := []error{}
343+
if urlInterface, hasURL := metadata["url"]; hasURL {
344+
if urlString, isString := urlInterface.(string); isString {
345+
if _, err := url.Parse(urlString); err == nil {
346+
release.URL = configv1.URL(urlString)
347+
} else {
348+
errs = append(errs, fmt.Errorf("invalid release URL: %s", err))
349+
}
350+
} else {
351+
errs = append(errs, fmt.Errorf("URL is not a string: %v", urlInterface))
340352
}
341-
cvoUpdate.URL = configv1.URL(urlString)
342353
}
343-
if arch, ok := update.Metadata["release.openshift.io/architecture"]; ok {
344-
switch arch {
345-
case "multi":
346-
cvoUpdate.Architecture = configv1.ClusterVersionArchitectureMulti
347-
default:
348-
klog.Warningf("Unrecognized release.openshift.io/architecture value %q", arch)
354+
if archInterface, hasArch := metadata["release.openshift.io/architecture"]; hasArch {
355+
if arch, isString := archInterface.(string); isString {
356+
if arch == "multi" {
357+
release.Architecture = configv1.ClusterVersionArchitectureMulti
358+
} else {
359+
errs = append(errs, fmt.Errorf("unrecognized release.openshift.io/architecture value %q", arch))
360+
}
361+
} else {
362+
errs = append(errs, fmt.Errorf("release.openshift.io/architecture is not a string: %v", archInterface))
349363
}
350364
}
351-
if channels, ok := update.Metadata["io.openshift.upgrades.graph.release.channels"]; ok {
352-
cvoUpdate.Channels = strings.Split(channels, ",")
353-
sort.Strings(cvoUpdate.Channels)
365+
if channelsInterface, hasChannels := metadata["io.openshift.upgrades.graph.release.channels"]; hasChannels {
366+
if channelsString, isString := channelsInterface.(string); isString {
367+
if len(channelsString) == 0 {
368+
errs = append(errs, fmt.Errorf("io.openshift.upgrades.graph.release.channels is an empty string"))
369+
} else {
370+
channels := strings.Split(channelsString, ",")
371+
if len(channels) == 0 {
372+
errs = append(errs, fmt.Errorf("no comma-delimited channels in io.openshift.upgrades.graph.release.channels %q", channelsString))
373+
} else {
374+
for i := len(channels) - 1; i >= 0; i-- {
375+
if len(channels[i]) == 0 {
376+
errs = append(errs, fmt.Errorf("io.openshift.upgrades.graph.release.channels entry %d is an empty string: %q", i, channelsString))
377+
channels = append(channels[:i], channels[i+1:]...)
378+
}
379+
}
380+
if len(channels) == 0 {
381+
errs = append(errs, fmt.Errorf("no non-empty channels in io.openshift.upgrades.graph.release.channels %q", channels))
382+
} else {
383+
sort.Strings(channels)
384+
release.Channels = channels
385+
}
386+
}
387+
}
388+
} else {
389+
errs = append(errs, fmt.Errorf("io.openshift.upgrades.graph.release.channels is not a string: %v", channelsInterface))
390+
}
354391
}
355-
return cvoUpdate, nil
392+
klog.V(2).Infof("parsed metadata: URL %q, architecture %q, channels %v, errors %v", release.URL, release.Architecture, release.Channels, errs)
393+
return release, errors.Join(errs...)
356394
}

pkg/cincinnati/cincinnati_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -816,7 +816,7 @@ func Test_nodeUnmarshalJSON(t *testing.T) {
816816
exp: node{
817817
Version: semver.MustParse("4.0.0-5"),
818818
Image: "quay.io/openshift-release-dev/ocp-release:4.0.0-5",
819-
Metadata: map[string]string{},
819+
Metadata: map[string]interface{}{},
820820
},
821821
}, {
822822
raw: []byte(`{
@@ -829,7 +829,7 @@ func Test_nodeUnmarshalJSON(t *testing.T) {
829829
exp: node{
830830
Version: semver.MustParse("4.0.0-0.1"),
831831
Image: "quay.io/openshift-release-dev/ocp-release:4.0.0-0.1",
832-
Metadata: map[string]string{
832+
Metadata: map[string]interface{}{
833833
"description": "This is the beta1 image based on the 4.0.0-0.nightly-2019-01-15-010905 build",
834834
},
835835
},

pkg/payload/payload.go

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import (
1010
"os"
1111
"path/filepath"
1212
"runtime"
13-
"sort"
14-
"strings"
1513
"time"
1614

1715
"k8s.io/klog/v2"
@@ -27,6 +25,7 @@ import (
2725
"github.com/openshift/cluster-version-operator/lib/capability"
2826
localmanifest "github.com/openshift/cluster-version-operator/lib/manifest"
2927
"github.com/openshift/cluster-version-operator/lib/resourceread"
28+
"github.com/openshift/cluster-version-operator/pkg/cincinnati"
3029
)
3130

3231
// State describes the state of the payload and alters
@@ -68,9 +67,6 @@ const (
6867
// provide better visibility during install and upgrade of
6968
// error conditions.
7069
PrecreatingPayload
71-
72-
// releaseMultiArchID identifies a multi architecture release.
73-
releaseMultiArchID = "multi"
7470
)
7571

7672
// Initializing is true if the state is InitializingPayload.
@@ -368,39 +364,12 @@ func loadReleaseMetadata(releaseDir string) (configv1.Release, error) {
368364
return release, fmt.Errorf("Cincinnati metadata version %q is not a valid semantic version: %v", metadata.Version, err)
369365
}
370366

371-
release.Version = metadata.Version
372-
373-
if archRaw, hasArch := metadata.Metadata["release.openshift.io/architecture"]; hasArch {
374-
arch, isString := archRaw.(string)
375-
if !isString {
376-
return release, fmt.Errorf("Architecture from %s (%s) is not a string: %v",
377-
cincinnatiJSONFile, release.Version, archRaw)
378-
}
379-
380-
if arch != releaseMultiArchID {
381-
return release, fmt.Errorf("Architecture from %s (%s) contains invalid value: %q. Valid value is %q.",
382-
cincinnatiJSONFile, release.Version, arch, releaseMultiArchID)
383-
}
384-
385-
release.Architecture = configv1.ClusterVersionArchitectureMulti
386-
klog.V(2).Infof("Architecture from %s (%s) is multi: %q", cincinnatiJSONFile, release.Version, arch)
367+
release, err = cincinnati.ParseMetadata(metadata.Metadata)
368+
if err != nil {
369+
klog.Warningf("errors while parsing metadata from %s (%s): %v", cincinnatiJSONFile, release.Version, err)
387370
}
388371

389-
if urlInterface, ok := metadata.Metadata["url"]; ok {
390-
if urlString, ok := urlInterface.(string); ok {
391-
release.URL = configv1.URL(urlString)
392-
} else {
393-
klog.Warningf("URL from %s (%s) is not a string: %v", cincinnatiJSONFile, release.Version, urlInterface)
394-
}
395-
}
396-
if channelsInterface, ok := metadata.Metadata["io.openshift.upgrades.graph.release.channels"]; ok {
397-
if channelsString, ok := channelsInterface.(string); ok {
398-
release.Channels = strings.Split(channelsString, ",")
399-
sort.Strings(release.Channels)
400-
} else {
401-
klog.Warningf("channel list from %s (%s) is not a string: %v", cincinnatiJSONFile, release.Version, channelsInterface)
402-
}
403-
}
372+
release.Version = metadata.Version
404373

405374
return release, nil
406375
}

0 commit comments

Comments
 (0)