diff --git a/docs/user/status.md b/docs/user/status.md index 0b2c93b94a..2cffa05ff5 100644 --- a/docs/user/status.md +++ b/docs/user/status.md @@ -110,7 +110,7 @@ It could also be an overloaded or otherwise failing Cincinnati server. ### ResponseInvalid -The Cincinnati server returned a response that was not valid JSON. +The Cincinnati server returned a response that was not valid JSON or is otherwise corrupted. This could be caused by a buggy Cincinnati server. It could also be caused by response corruption, e.g. if the configured `upstream` was in the clear over HTTP or via a man-in-the-middle HTTPS proxy, and an intervening component altered the response in flight. diff --git a/go.mod b/go.mod index b732f6696d..34c366503d 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/google/uuid v1.1.1 github.com/hashicorp/golang-lru v0.5.3 // indirect github.com/imdario/mergo v0.3.8 // indirect - github.com/openshift/api v0.0.0-20200723134351-89de68875e7c + github.com/openshift/api v0.0.0-20200724204552-3ae6754513d4 github.com/openshift/client-go v0.0.0-20200723130357-94e1065ab1f8 github.com/openshift/library-go v0.0.0-20200724192307-1ed21c4fa86c github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index f3acc850a4..a1072d8b42 100644 --- a/go.sum +++ b/go.sum @@ -337,6 +337,8 @@ github.com/openshift/api v0.0.0-20200722170803-0ba2c3658da6/go.mod h1:IXsT3F4NjL github.com/openshift/api v0.0.0-20200723113038-c1d9554393d6/go.mod h1:IXsT3F4NjLtRzfnQvwU+g/oPWpoNsVV5vd5aaOMO8eU= github.com/openshift/api v0.0.0-20200723134351-89de68875e7c h1:qsj/GaQ1sdT584yIcGmqqRpR5xtX5jTw5Gis3/09YI4= github.com/openshift/api v0.0.0-20200723134351-89de68875e7c/go.mod h1:IXsT3F4NjLtRzfnQvwU+g/oPWpoNsVV5vd5aaOMO8eU= +github.com/openshift/api v0.0.0-20200724204552-3ae6754513d4 h1:sUTyOVzcbILg4Gtk2j+DLaV4vVnMmt8SNz3efONPQ/s= +github.com/openshift/api v0.0.0-20200724204552-3ae6754513d4/go.mod h1:IXsT3F4NjLtRzfnQvwU+g/oPWpoNsVV5vd5aaOMO8eU= github.com/openshift/build-machinery-go v0.0.0-20200713135615-1f43d26dccc7/go.mod h1:b1BuldmJlbA/xYtdZvKi+7j5YGB44qJUJDZ9zwiNCfE= github.com/openshift/client-go v0.0.0-20200722173614-5a1b0aaeff15/go.mod h1:yd4Zpcdk+8JyMWi6v+h78jPqK0FvXbJY41Wq3SZxl+c= github.com/openshift/client-go v0.0.0-20200723130357-94e1065ab1f8 h1:rCyKXyp937G/UAen+i+m2pUNE7W4ScHpbAes7uhbJfg= diff --git a/pkg/autoupdate/autoupdate.go b/pkg/autoupdate/autoupdate.go index 3b8d3bb7ff..8ef42d8d34 100644 --- a/pkg/autoupdate/autoupdate.go +++ b/pkg/autoupdate/autoupdate.go @@ -175,7 +175,10 @@ func (ctrl *Controller) sync(ctx context.Context, key string) error { return nil } up := nextUpdate(clusterversion.Status.AvailableUpdates) - clusterversion.Spec.DesiredUpdate = &up + clusterversion.Spec.DesiredUpdate = &v1.Update{ + Version: up.Version, + Image: up.Image, + } _, updated, err := resourceapply.ApplyClusterVersionFromCache(ctx, ctrl.cvLister, ctrl.client.ConfigV1(), clusterversion) if updated { @@ -184,11 +187,11 @@ func (ctrl *Controller) sync(ctx context.Context, key string) error { return err } -func updateAvail(ups []v1.Update) bool { +func updateAvail(ups []v1.Release) bool { return len(ups) > 0 } -func nextUpdate(ups []v1.Update) v1.Update { +func nextUpdate(ups []v1.Release) v1.Release { sorted := ups sort.Slice(sorted, func(i, j int) bool { vi := semver.MustParse(sorted[i].Version) diff --git a/pkg/autoupdate/autoupdate_test.go b/pkg/autoupdate/autoupdate_test.go index e6bcda0345..b95d9df55a 100644 --- a/pkg/autoupdate/autoupdate_test.go +++ b/pkg/autoupdate/autoupdate_test.go @@ -29,9 +29,9 @@ func TestNextUpdate(t *testing.T) { }} for idx, test := range tests { t.Run(fmt.Sprintf("test: #%d", idx), func(t *testing.T) { - ups := []v1.Update{} + ups := []v1.Release{} for _, v := range test.avail { - ups = append(ups, v1.Update{Version: v}) + ups = append(ups, v1.Release{Version: v}) } got := nextUpdate(ups) diff --git a/pkg/cincinnati/cincinnati.go b/pkg/cincinnati/cincinnati.go index 6d09c3032c..0d2e20d0a8 100644 --- a/pkg/cincinnati/cincinnati.go +++ b/pkg/cincinnati/cincinnati.go @@ -56,14 +56,15 @@ func (err *Error) Error() string { return fmt.Sprintf("%s: %s", err.Reason, err.Message) } -// GetUpdates fetches the next-applicable update payloads from the specified +// GetUpdates fetches the current and next-applicable update payloads from the specified // upstream Cincinnati stack given the current version and channel. The next- // applicable updates are determined by downloading the update graph, finding // the current version within that graph (typically the root node), and then // finding all of the children. These children are the available updates for // the current version and their payloads indicate from where the actual update // image can be downloaded. -func (c Client) GetUpdates(ctx context.Context, uri *url.URL, arch string, channel string, version semver.Version) ([]Update, error) { +func (c Client) GetUpdates(ctx context.Context, uri *url.URL, arch string, channel string, version semver.Version) (Update, []Update, error) { + var current Update transport := http.Transport{} // Prepare parametrized cincinnati query. queryParams := uri.Query() @@ -76,7 +77,7 @@ func (c Client) GetUpdates(ctx context.Context, uri *url.URL, arch string, chann // Download the update graph. req, err := http.NewRequest("GET", uri.String(), nil) if err != nil { - return nil, &Error{Reason: "InvalidRequest", Message: err.Error(), cause: err} + return current, nil, &Error{Reason: "InvalidRequest", Message: err.Error(), cause: err} } req.Header.Add("Accept", GraphMediaType) if c.tlsConfig != nil { @@ -92,23 +93,23 @@ func (c Client) GetUpdates(ctx context.Context, uri *url.URL, arch string, chann defer cancel() resp, err := client.Do(req.WithContext(timeoutCtx)) if err != nil { - return nil, &Error{Reason: "RemoteFailed", Message: err.Error(), cause: err} + return current, nil, &Error{Reason: "RemoteFailed", Message: err.Error(), cause: err} } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, &Error{Reason: "ResponseFailed", Message: fmt.Sprintf("unexpected HTTP status: %s", resp.Status)} + return current, nil, &Error{Reason: "ResponseFailed", Message: fmt.Sprintf("unexpected HTTP status: %s", resp.Status)} } // Parse the graph. body, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, &Error{Reason: "ResponseFailed", Message: err.Error(), cause: err} + return current, nil, &Error{Reason: "ResponseFailed", Message: err.Error(), cause: err} } var graph graph if err = json.Unmarshal(body, &graph); err != nil { - return nil, &Error{Reason: "ResponseInvalid", Message: err.Error(), cause: err} + return current, nil, &Error{Reason: "ResponseInvalid", Message: err.Error(), cause: err} } // Find the current version within the graph. @@ -117,12 +118,13 @@ func (c Client) GetUpdates(ctx context.Context, uri *url.URL, arch string, chann for i, node := range graph.Nodes { if version.EQ(node.Version) { currentIdx = i + current = Update(graph.Nodes[i]) found = true break } } if !found { - return nil, &Error{ + return current, nil, &Error{ Reason: "VersionNotFound", Message: fmt.Sprintf("currently installed version %s not found in the %q channel", version, channel), } @@ -141,7 +143,7 @@ func (c Client) GetUpdates(ctx context.Context, uri *url.URL, arch string, chann updates = append(updates, Update(graph.Nodes[i])) } - return updates, nil + return current, updates, nil } type graph struct { @@ -150,8 +152,9 @@ type graph struct { } type node struct { - Version semver.Version `json:"version"` - Image string `json:"payload"` + Version semver.Version `json:"version"` + Image string `json:"payload"` + Metadata map[string]string `json:"metadata,omitempty"` } type edge struct { diff --git a/pkg/cincinnati/cincinnati_test.go b/pkg/cincinnati/cincinnati_test.go index 66a22604e5..bb1ce74aaf 100644 --- a/pkg/cincinnati/cincinnati_test.go +++ b/pkg/cincinnati/cincinnati_test.go @@ -25,26 +25,30 @@ func TestGetUpdates(t *testing.T) { version string expectedQuery string + current Update available []Update err string }{{ name: "one update available", version: "4.0.0-4", expectedQuery: "arch=test-arch&channel=test-channel&id=01234567-0123-0123-0123-0123456789ab&version=4.0.0-4", + current: Update{Version: semver.MustParse("4.0.0-4"), Image: "quay.io/openshift-release-dev/ocp-release:4.0.0-4"}, available: []Update{ - {semver.MustParse("4.0.0-5"), "quay.io/openshift-release-dev/ocp-release:4.0.0-5"}, + {Version: semver.MustParse("4.0.0-5"), Image: "quay.io/openshift-release-dev/ocp-release:4.0.0-5"}, }, }, { name: "two updates available", version: "4.0.0-5", expectedQuery: "arch=test-arch&channel=test-channel&id=01234567-0123-0123-0123-0123456789ab&version=4.0.0-5", + current: Update{Version: semver.MustParse("4.0.0-5"), Image: "quay.io/openshift-release-dev/ocp-release:4.0.0-5"}, available: []Update{ - {semver.MustParse("4.0.0-6"), "quay.io/openshift-release-dev/ocp-release:4.0.0-6"}, - {semver.MustParse("4.0.0-6+2"), "quay.io/openshift-release-dev/ocp-release:4.0.0-6+2"}, + {Version: semver.MustParse("4.0.0-6"), Image: "quay.io/openshift-release-dev/ocp-release:4.0.0-6"}, + {Version: semver.MustParse("4.0.0-6+2"), Image: "quay.io/openshift-release-dev/ocp-release:4.0.0-6+2"}, }, }, { name: "no updates available", version: "4.0.0-0.okd-0", + current: Update{Version: semver.MustParse("4.0.0-0.okd-0"), Image: "quay.io/openshift-release-dev/ocp-release:4.0.0-0.okd-0"}, expectedQuery: "arch=test-arch&channel=test-channel&id=01234567-0123-0123-0123-0123456789ab&version=4.0.0-0.okd-0", }, { name: "unknown version", @@ -79,38 +83,31 @@ func TestGetUpdates(t *testing.T) { "nodes": [ { "version": "4.0.0-4", - "payload": "quay.io/openshift-release-dev/ocp-release:4.0.0-4", - "metadata": {} + "payload": "quay.io/openshift-release-dev/ocp-release:4.0.0-4" }, { "version": "4.0.0-5", - "payload": "quay.io/openshift-release-dev/ocp-release:4.0.0-5", - "metadata": {} + "payload": "quay.io/openshift-release-dev/ocp-release:4.0.0-5" }, { "version": "4.0.0-6", - "payload": "quay.io/openshift-release-dev/ocp-release:4.0.0-6", - "metadata": {} + "payload": "quay.io/openshift-release-dev/ocp-release:4.0.0-6" }, { "version": "4.0.0-6+2", - "payload": "quay.io/openshift-release-dev/ocp-release:4.0.0-6+2", - "metadata": {} + "payload": "quay.io/openshift-release-dev/ocp-release:4.0.0-6+2" }, { "version": "4.0.0-0.okd-0", - "payload": "quay.io/openshift-release-dev/ocp-release:4.0.0-0.okd-0", - "metadata": {} + "payload": "quay.io/openshift-release-dev/ocp-release:4.0.0-0.okd-0" }, { "version": "4.0.0-0.2", - "payload": "quay.io/openshift-release-dev/ocp-release:4.0.0-0.2", - "metadata": {} + "payload": "quay.io/openshift-release-dev/ocp-release:4.0.0-0.2" }, { "version": "4.0.0-0.3", - "payload": "quay.io/openshift-release-dev/ocp-release:4.0.0-0.3", - "metadata": {} + "payload": "quay.io/openshift-release-dev/ocp-release:4.0.0-0.3" } ], "edges": [[0,1],[1,2],[1,3],[5,6]] @@ -133,13 +130,16 @@ func TestGetUpdates(t *testing.T) { t.Fatal(err) } - updates, err := c.GetUpdates(context.Background(), uri, arch, channelName, semver.MustParse(test.version)) + current, updates, err := c.GetUpdates(context.Background(), uri, arch, channelName, semver.MustParse(test.version)) if test.err == "" { if err != nil { t.Fatalf("expected nil error, got: %v", err) } + if !reflect.DeepEqual(current, test.current) { + t.Fatalf("expected current %v, got: %v", test.current, current) + } if !reflect.DeepEqual(updates, test.available) { - t.Fatalf("expected %v, got: %v", test.available, updates) + t.Fatalf("expected updates %v, got: %v", test.available, updates) } } else { if err == nil || err.Error() != test.err { @@ -181,7 +181,11 @@ func Test_nodeUnmarshalJSON(t *testing.T) { "metadata": {} }`), - exp: node{semver.MustParse("4.0.0-5"), "quay.io/openshift-release-dev/ocp-release:4.0.0-5"}, + exp: node{ + Version: semver.MustParse("4.0.0-5"), + Image: "quay.io/openshift-release-dev/ocp-release:4.0.0-5", + Metadata: map[string]string{}, + }, }, { raw: []byte(`{ "version": "4.0.0-0.1", @@ -190,7 +194,13 @@ func Test_nodeUnmarshalJSON(t *testing.T) { "description": "This is the beta1 image based on the 4.0.0-0.nightly-2019-01-15-010905 build" } }`), - exp: node{semver.MustParse("4.0.0-0.1"), "quay.io/openshift-release-dev/ocp-release:4.0.0-0.1"}, + exp: node{ + Version: semver.MustParse("4.0.0-0.1"), + Image: "quay.io/openshift-release-dev/ocp-release:4.0.0-0.1", + Metadata: map[string]string{ + "description": "This is the beta1 image based on the 4.0.0-0.nightly-2019-01-15-010905 build", + }, + }, }, { raw: []byte(`{ "version": "v4.0.0-0.1", diff --git a/pkg/cvo/availableupdates.go b/pkg/cvo/availableupdates.go index d1c2d91758..b9fe9a2aa9 100644 --- a/pkg/cvo/availableupdates.go +++ b/pkg/cvo/availableupdates.go @@ -6,6 +6,8 @@ import ( "fmt" "net/url" "runtime" + "sort" + "strings" "time" "github.com/blang/semver/v4" @@ -46,7 +48,7 @@ func (optr *Operator) syncAvailableUpdates(ctx context.Context, config *configv1 return err } - updates, condition := calculateAvailableUpdatesStatus(ctx, string(config.Spec.ClusterID), proxyURL, tlsConfig, upstream, arch, channel, optr.releaseVersion) + current, updates, condition := calculateAvailableUpdatesStatus(ctx, string(config.Spec.ClusterID), proxyURL, tlsConfig, upstream, arch, channel, optr.release.Version) if usedDefaultUpstream { upstream = "" @@ -54,6 +56,7 @@ func (optr *Operator) syncAvailableUpdates(ctx context.Context, config *configv1 optr.setAvailableUpdates(&availableUpdates{ Upstream: upstream, Channel: config.Spec.Channel, + Current: current, Updates: updates, Condition: condition, }) @@ -81,7 +84,8 @@ type availableUpdates struct { // slice was empty. LastSyncOrConfigChange time.Time - Updates []configv1.Update + Current configv1.Release + Updates []configv1.Release Condition configv1.ClusterOperatorStatusCondition } @@ -140,9 +144,10 @@ func (optr *Operator) getAvailableUpdates() *availableUpdates { return optr.availableUpdates } -func calculateAvailableUpdatesStatus(ctx context.Context, clusterID string, proxyURL *url.URL, tlsConfig *tls.Config, upstream, arch, channel, version string) ([]configv1.Update, configv1.ClusterOperatorStatusCondition) { +func calculateAvailableUpdatesStatus(ctx context.Context, clusterID string, proxyURL *url.URL, tlsConfig *tls.Config, upstream, arch, channel, version string) (configv1.Release, []configv1.Release, configv1.ClusterOperatorStatusCondition) { + var cvoCurrent configv1.Release if len(upstream) == 0 { - return nil, configv1.ClusterOperatorStatusCondition{ + return cvoCurrent, nil, configv1.ClusterOperatorStatusCondition{ Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse, Reason: "NoUpstream", Message: "No upstream server has been set to retrieve updates.", } @@ -150,7 +155,7 @@ func calculateAvailableUpdatesStatus(ctx context.Context, clusterID string, prox upstreamURI, err := url.Parse(upstream) if err != nil { - return nil, configv1.ClusterOperatorStatusCondition{ + return cvoCurrent, nil, configv1.ClusterOperatorStatusCondition{ Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse, Reason: "InvalidURI", Message: fmt.Sprintf("failed to parse upstream URL: %s", err), } @@ -158,28 +163,28 @@ func calculateAvailableUpdatesStatus(ctx context.Context, clusterID string, prox uuid, err := uuid.Parse(string(clusterID)) if err != nil { - return nil, configv1.ClusterOperatorStatusCondition{ + return cvoCurrent, nil, configv1.ClusterOperatorStatusCondition{ Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse, Reason: "InvalidID", Message: fmt.Sprintf("invalid cluster ID: %s", err), } } if len(arch) == 0 { - return nil, configv1.ClusterOperatorStatusCondition{ + return cvoCurrent, nil, configv1.ClusterOperatorStatusCondition{ Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse, Reason: "NoArchitecture", Message: "The set of architectures has not been configured.", } } if len(version) == 0 { - return nil, configv1.ClusterOperatorStatusCondition{ + return cvoCurrent, nil, configv1.ClusterOperatorStatusCondition{ Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse, Reason: "NoCurrentVersion", Message: "The cluster version does not have a semantic version assigned and cannot calculate valid upgrades.", } } if len(channel) == 0 { - return nil, configv1.ClusterOperatorStatusCondition{ + return cvoCurrent, nil, configv1.ClusterOperatorStatusCondition{ Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse, Reason: noChannel, Message: "The update channel has not been configured.", } @@ -188,40 +193,71 @@ func calculateAvailableUpdatesStatus(ctx context.Context, clusterID string, prox currentVersion, err := semver.Parse(version) if err != nil { klog.V(2).Infof("Unable to parse current semantic version %q: %v", version, err) - return nil, configv1.ClusterOperatorStatusCondition{ + return cvoCurrent, nil, configv1.ClusterOperatorStatusCondition{ Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse, Reason: "InvalidCurrentVersion", Message: "The current cluster version is not a valid semantic version and cannot be used to calculate upgrades.", } } - updates, err := cincinnati.NewClient(uuid, proxyURL, tlsConfig).GetUpdates(ctx, upstreamURI, arch, channel, currentVersion) + current, updates, err := cincinnati.NewClient(uuid, proxyURL, tlsConfig).GetUpdates(ctx, upstreamURI, arch, channel, currentVersion) if err != nil { klog.V(2).Infof("Upstream server %s could not return available updates: %v", upstream, err) if updateError, ok := err.(*cincinnati.Error); ok { - return nil, configv1.ClusterOperatorStatusCondition{ + return cvoCurrent, nil, configv1.ClusterOperatorStatusCondition{ Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse, Reason: updateError.Reason, Message: fmt.Sprintf("Unable to retrieve available updates: %s", updateError.Message), } } // this should never happen - return nil, configv1.ClusterOperatorStatusCondition{ + return cvoCurrent, nil, configv1.ClusterOperatorStatusCondition{ Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse, Reason: "Unknown", Message: fmt.Sprintf("Unable to retrieve available updates: %s", err), } } - var cvoUpdates []configv1.Update + cvoCurrent, err = convertRetreivedUpdateToRelease(current) + if err != nil { + return cvoCurrent, nil, configv1.ClusterOperatorStatusCondition{ + Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse, Reason: "ResponseInvalid", + Message: fmt.Sprintf("Invalid recommended update node: %s", err), + } + } + + var cvoUpdates []configv1.Release for _, update := range updates { - cvoUpdates = append(cvoUpdates, configv1.Update{ - Version: update.Version.String(), - Image: update.Image, - }) + cvoUpdate, err := convertRetreivedUpdateToRelease(update) + if err != nil { + return cvoCurrent, nil, configv1.ClusterOperatorStatusCondition{ + Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse, Reason: "ResponseInvalid", + Message: fmt.Sprintf("Invalid recommended update node: %s", err), + } + } + cvoUpdates = append(cvoUpdates, cvoUpdate) } - return cvoUpdates, configv1.ClusterOperatorStatusCondition{ + return cvoCurrent, cvoUpdates, configv1.ClusterOperatorStatusCondition{ Type: configv1.RetrievedUpdates, Status: configv1.ConditionTrue, LastTransitionTime: metav1.Now(), } } + +func convertRetreivedUpdateToRelease(update cincinnati.Update) (configv1.Release, error) { + cvoUpdate := configv1.Release{ + Version: update.Version.String(), + Image: update.Image, + } + if urlString, ok := update.Metadata["url"]; ok { + _, err := url.Parse(urlString) + if err != nil { + return cvoUpdate, fmt.Errorf("invalid URL for %s: %s", cvoUpdate.Version, err) + } + cvoUpdate.URL = configv1.URL(urlString) + } + if channels, ok := update.Metadata["io.openshift.upgrades.graph.release.channels"]; ok { + cvoUpdate.Channels = strings.Split(channels, ",") + sort.Strings(cvoUpdate.Channels) + } + return cvoUpdate, nil +} diff --git a/pkg/cvo/cvo.go b/pkg/cvo/cvo.go index f8670b6aa2..4d83341020 100644 --- a/pkg/cvo/cvo.go +++ b/pkg/cvo/cvo.go @@ -10,7 +10,6 @@ import ( "sync" "time" - "github.com/blang/semver/v4" "github.com/google/uuid" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" @@ -87,13 +86,13 @@ type Operator struct { // namespace and name are used to find the ClusterVersion, OperatorStatus. namespace, name string - // releaseImage is the image the current operator points to and allows - // templating of the CVO deployment manifest. - releaseImage string - // releaseVersion is a string identifier for the current version, read - // from the image of the operator. It may be empty if no version exists, in - // which case no available updates will be returned. - releaseVersion string + // release is the release the current operator points to and + // metadata read from the release image. It allows templating of + // the CVO deployment manifest. + // + // Fetch via currentVersion() to populate metadata from + // availableUpdates. + release configv1.Release // releaseCreated, if set, is the timestamp of the current update. releaseCreated time.Time @@ -179,10 +178,12 @@ func New( eventBroadcaster.StartRecordingToSink(&coreclientsetv1.EventSinkImpl{Interface: kubeClient.CoreV1().Events(namespace)}) optr := &Operator{ - nodename: nodename, - namespace: namespace, - name: name, - releaseImage: releaseImage, + nodename: nodename, + namespace: namespace, + name: name, + release: configv1.Release{ + Image: releaseImage, + }, enableDefaultClusterVersion: enableDefaultClusterVersion, @@ -223,17 +224,13 @@ func New( // controller that loads and applies content to the cluster. It returns an error if the payload appears to // be in error rather than continuing. func (optr *Operator) InitializeFromPayload(restConfig *rest.Config, burstRestConfig *rest.Config) error { - update, err := payload.LoadUpdate(optr.defaultPayloadDir(), optr.releaseImage, optr.exclude) + update, err := payload.LoadUpdate(optr.defaultPayloadDir(), optr.release.Image, optr.exclude) if err != nil { return fmt.Errorf("the local release contents are invalid - no current version can be determined from disk: %v", err) } - // XXX: set this to the cincinnati version in preference - if _, err := semver.Parse(update.ImageRef.Name); err != nil { - return fmt.Errorf("the local release contents name %q is not a valid semantic version - no current version will be reported: %v", update.ImageRef.Name, err) - } + optr.release = update.Release optr.releaseCreated = update.ImageRef.CreationTimestamp.Time - optr.releaseVersion = update.ImageRef.Name httpClientConstructor := sigstore.NewCachedHTTPClientConstructor(optr.HTTPClient, nil) configClient, err := coreclientsetv1.NewForConfig(restConfig) @@ -452,7 +449,11 @@ func (optr *Operator) sync(ctx context.Context, key string) error { if ok { klog.V(4).Infof("Desired version from spec is %#v", desired) } else { - desired = optr.currentVersion() + currentVersion := optr.currentVersion() + desired = configv1.Update{ + Version: currentVersion.Version, + Image: currentVersion.Image, + } klog.V(4).Infof("Desired version from operator is %#v", desired) } @@ -601,7 +602,7 @@ func (optr *Operator) getOrCreateClusterVersion(ctx context.Context, enableDefau } // versionString returns a string describing the desired version. -func versionString(update configv1.Update) string { +func versionString(update configv1.Release) string { if len(update.Version) > 0 { return update.Version } @@ -611,12 +612,50 @@ func versionString(update configv1.Update) string { return "" } -// currentVersion returns an update object describing the current known cluster version. -func (optr *Operator) currentVersion() configv1.Update { - return configv1.Update{ - Version: optr.releaseVersion, - Image: optr.releaseImage, +// currentVersion returns an update object describing the current +// known cluster version. Values from the upstream Cincinnati service +// are used as fallbacks for any properties not defined in the release +// image itself. +func (optr *Operator) currentVersion() configv1.Release { + return optr.mergeReleaseMetadata(optr.release) +} + +// mergeReleaseMetadata returns a deep copy of the input release. +// Values from any matching availableUpdates release are used as +// fallbacks for any properties not defined in the input release. +func (optr *Operator) mergeReleaseMetadata(release configv1.Release) configv1.Release { + merged := *release.DeepCopy() + + if merged.Version == "" || len(merged.URL) == 0 || merged.Channels == nil { + // only fill in missing values from availableUpdates, to avoid clobbering data from payload.LoadUpdate. + availableUpdates := optr.getAvailableUpdates() + if availableUpdates != nil { + var update *configv1.Release + if merged.Image == availableUpdates.Current.Image { + update = &availableUpdates.Current + } else { + for _, u := range availableUpdates.Updates { + if u.Image == merged.Image { + update = &u + break + } + } + } + if update != nil { + if merged.Version == "" { + merged.Version = update.Version + } + if len(merged.URL) == 0 { + merged.URL = update.URL + } + if merged.Channels == nil { + merged.Channels = append(update.Channels[:0:0], update.Channels...) // copy + } + } + } } + + return merged } // SetSyncWorkerForTesting updates the sync worker for whitebox testing. diff --git a/pkg/cvo/cvo_scenarios_test.go b/pkg/cvo/cvo_scenarios_test.go index 11ee1a61e8..1b5086ebb4 100644 --- a/pkg/cvo/cvo_scenarios_test.go +++ b/pkg/cvo/cvo_scenarios_test.go @@ -177,9 +177,9 @@ func TestCVO_StartupAndSync(t *testing.T) { // Step 3: Given an operator image, begin synchronizing // - o.releaseImage = "image/image:1" - o.releaseVersion = "4.0.1" - desired := configv1.Update{Version: "4.0.1", Image: "image/image:1"} + o.release.Image = "image/image:1" + o.release.Version = "4.0.1" + desired := configv1.Release{Version: "4.0.1", Image: "image/image:1"} // client.ClearActions() err = o.sync(ctx, o.queueKey()) @@ -222,43 +222,62 @@ func TestCVO_StartupAndSync(t *testing.T) { Step: "RetrievePayload", Initial: true, // the desired version is briefly incorrect (user provided) until we retrieve the image - Actual: configv1.Update{Version: "4.0.1", Image: "image/image:1"}, + Actual: configv1.Release{Version: "4.0.1", Image: "image/image:1"}, }, SyncWorkerStatus{ - Generation: 1, - Step: "ApplyResources", - Initial: true, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Generation: 1, + Step: "ApplyResources", + Initial: true, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, + }, + SyncWorkerStatus{ + Generation: 1, + Fraction: float32(1) / 3, + Step: "ApplyResources", + Initial: true, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(1, 0), }, SyncWorkerStatus{ - Generation: 1, - Fraction: float32(1) / 3, - Step: "ApplyResources", - Initial: true, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Generation: 1, + Fraction: float32(2) / 3, + Initial: true, + Step: "ApplyResources", + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(2, 0), }, SyncWorkerStatus{ - Generation: 1, - Fraction: float32(2) / 3, - Initial: true, - Step: "ApplyResources", - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Generation: 1, + Reconciling: true, + Completed: 1, + Fraction: 1, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(3, 0), }, - SyncWorkerStatus{ - Generation: 1, - Reconciling: true, - Completed: 1, - Fraction: 1, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, - LastProgress: time.Unix(4, 0), - }, ) // Step 4: Now that sync is complete, verify status is updated to represent image contents @@ -287,7 +306,12 @@ func TestCVO_StartupAndSync(t *testing.T) { Status: configv1.ClusterVersionStatus{ ObservedGeneration: 1, // Prefers the image version over the operator's version (although in general they will remain in sync) - Desired: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Desired: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, VersionHash: "6GC9TkkG9PA=", History: []configv1.UpdateHistory{ // Because image and operator had mismatched versions, we get two entries (which shouldn't happen unless there is a bug in the CVO) @@ -311,7 +335,12 @@ func TestCVO_StartupAndSync(t *testing.T) { Reconciling: true, Step: "ApplyResources", VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, }, SyncWorkerStatus{ Generation: 1, @@ -319,7 +348,12 @@ func TestCVO_StartupAndSync(t *testing.T) { Fraction: float32(1) / 3, Step: "ApplyResources", VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, }, SyncWorkerStatus{ Generation: 1, @@ -327,15 +361,25 @@ func TestCVO_StartupAndSync(t *testing.T) { Fraction: float32(2) / 3, Step: "ApplyResources", VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, }, SyncWorkerStatus{ - Generation: 1, - Reconciling: true, - Completed: 2, - Fraction: 1, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Generation: 1, + Reconciling: true, + Completed: 2, + Fraction: 1, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(1, 0), }, ) @@ -448,9 +492,9 @@ func TestCVO_StartupAndSyncUnverifiedPayload(t *testing.T) { // Step 3: Given an operator image, begin synchronizing // - o.releaseImage = "image/image:1" - o.releaseVersion = "4.0.1" - desired := configv1.Update{Version: "4.0.1", Image: "image/image:1"} + o.release.Image = "image/image:1" + o.release.Version = "4.0.1" + desired := configv1.Release{Version: "4.0.1", Image: "image/image:1"} // client.ClearActions() err = o.sync(ctx, o.queueKey()) @@ -492,44 +536,63 @@ func TestCVO_StartupAndSyncUnverifiedPayload(t *testing.T) { Step: "RetrievePayload", Initial: true, // the desired version is briefly incorrect (user provided) until we retrieve the image - Actual: configv1.Update{Version: "4.0.1", Image: "image/image:1"}, + Actual: configv1.Release{Version: "4.0.1", Image: "image/image:1"}, Generation: 1, }, SyncWorkerStatus{ - Step: "ApplyResources", - Initial: true, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Step: "ApplyResources", + Initial: true, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, + Generation: 1, + }, + SyncWorkerStatus{ + Fraction: float32(1) / 3, + Step: "ApplyResources", + Initial: true, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(1, 0), Generation: 1, }, SyncWorkerStatus{ - Fraction: float32(1) / 3, - Step: "ApplyResources", - Initial: true, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Fraction: float32(2) / 3, + Initial: true, + Step: "ApplyResources", + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(2, 0), Generation: 1, }, SyncWorkerStatus{ - Fraction: float32(2) / 3, - Initial: true, - Step: "ApplyResources", - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Reconciling: true, + Completed: 1, + Fraction: 1, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(3, 0), Generation: 1, }, - SyncWorkerStatus{ - Reconciling: true, - Completed: 1, - Fraction: 1, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, - LastProgress: time.Unix(4, 0), - Generation: 1, - }, ) // Step 4: Now that sync is complete, verify status is updated to represent image contents @@ -558,7 +621,12 @@ func TestCVO_StartupAndSyncUnverifiedPayload(t *testing.T) { Status: configv1.ClusterVersionStatus{ ObservedGeneration: 1, // Prefers the image version over the operator's version (although in general they will remain in sync) - Desired: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Desired: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, VersionHash: "6GC9TkkG9PA=", History: []configv1.UpdateHistory{ // Because image and operator had mismatched versions, we get two entries (which shouldn't happen unless there is a bug in the CVO) @@ -581,31 +649,51 @@ func TestCVO_StartupAndSyncUnverifiedPayload(t *testing.T) { Reconciling: true, Step: "ApplyResources", VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, - Generation: 1, + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, + Generation: 1, }, SyncWorkerStatus{ Reconciling: true, Fraction: float32(1) / 3, Step: "ApplyResources", VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, - Generation: 1, + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, + Generation: 1, }, SyncWorkerStatus{ Reconciling: true, Fraction: float32(2) / 3, Step: "ApplyResources", VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, - Generation: 1, + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, + Generation: 1, }, SyncWorkerStatus{ - Reconciling: true, - Completed: 2, - Fraction: 1, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Reconciling: true, + Completed: 2, + Fraction: 1, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(1, 0), Generation: 1, }, @@ -709,9 +797,9 @@ func TestCVO_StartupAndSyncPreconditionFailing(t *testing.T) { // Step 3: Given an operator image, begin synchronizing // - o.releaseImage = "image/image:1" - o.releaseVersion = "4.0.1" - desired := configv1.Update{Version: "4.0.1", Image: "image/image:1"} + o.release.Image = "image/image:1" + o.release.Version = "4.0.1" + desired := configv1.Release{Version: "4.0.1", Image: "image/image:1"} // client.ClearActions() err = o.sync(ctx, o.queueKey()) @@ -753,44 +841,63 @@ func TestCVO_StartupAndSyncPreconditionFailing(t *testing.T) { Step: "RetrievePayload", Initial: true, // the desired version is briefly incorrect (user provided) until we retrieve the image - Actual: configv1.Update{Version: "4.0.1", Image: "image/image:1"}, + Actual: configv1.Release{Version: "4.0.1", Image: "image/image:1"}, Generation: 1, }, SyncWorkerStatus{ - Step: "ApplyResources", - Initial: true, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Step: "ApplyResources", + Initial: true, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, + Generation: 1, + }, + SyncWorkerStatus{ + Fraction: float32(1) / 3, + Step: "ApplyResources", + Initial: true, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(1, 0), Generation: 1, }, SyncWorkerStatus{ - Fraction: float32(1) / 3, - Step: "ApplyResources", - Initial: true, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Fraction: float32(2) / 3, + Initial: true, + Step: "ApplyResources", + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(2, 0), Generation: 1, }, SyncWorkerStatus{ - Fraction: float32(2) / 3, - Initial: true, - Step: "ApplyResources", - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Reconciling: true, + Completed: 1, + Fraction: 1, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(3, 0), Generation: 1, }, - SyncWorkerStatus{ - Reconciling: true, - Completed: 1, - Fraction: 1, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, - LastProgress: time.Unix(4, 0), - Generation: 1, - }, ) // Step 4: Now that sync is complete, verify status is updated to represent image contents @@ -819,7 +926,12 @@ func TestCVO_StartupAndSyncPreconditionFailing(t *testing.T) { Status: configv1.ClusterVersionStatus{ ObservedGeneration: 1, // Prefers the image version over the operator's version (although in general they will remain in sync) - Desired: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Desired: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, VersionHash: "6GC9TkkG9PA=", History: []configv1.UpdateHistory{ // Because image and operator had mismatched versions, we get two entries (which shouldn't happen unless there is a bug in the CVO) @@ -842,31 +954,51 @@ func TestCVO_StartupAndSyncPreconditionFailing(t *testing.T) { Reconciling: true, Step: "ApplyResources", VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, - Generation: 1, + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, + Generation: 1, }, SyncWorkerStatus{ Reconciling: true, Fraction: float32(1) / 3, Step: "ApplyResources", VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, - Generation: 1, + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, + Generation: 1, }, SyncWorkerStatus{ Reconciling: true, Fraction: float32(2) / 3, Step: "ApplyResources", VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, - Generation: 1, + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, + Generation: 1, }, SyncWorkerStatus{ - Reconciling: true, - Completed: 2, - Fraction: 1, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Reconciling: true, + Completed: 2, + Fraction: 1, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(1, 0), Generation: 1, }, @@ -891,9 +1023,9 @@ func TestCVO_UpgradeUnverifiedPayload(t *testing.T) { // Setup: a successful sync from a previous run, and the operator at the same image as before // - o.releaseImage = "image/image:0" - o.releaseVersion = "1.0.0-abc" - desired := configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"} + o.release.Image = "image/image:0" + o.release.Version = "1.0.0-abc" + desired := configv1.Release{Version: "1.0.1-abc", Image: "image/image:1"} uid, _ := uuid.NewRandom() clusterUID := configv1.ClusterID(uid.String()) cvs["version"] = &configv1.ClusterVersion{ @@ -904,7 +1036,7 @@ func TestCVO_UpgradeUnverifiedPayload(t *testing.T) { Spec: configv1.ClusterVersionSpec{ ClusterID: clusterUID, Channel: "fast", - DesiredUpdate: &desired, + DesiredUpdate: &configv1.Update{Version: desired.Version, Image: desired.Image}, }, Status: configv1.ClusterVersionStatus{ // Prefers the image version over the operator's version (although in general they will remain in sync) @@ -957,12 +1089,12 @@ func TestCVO_UpgradeUnverifiedPayload(t *testing.T) { verifyAllStatus(t, worker.StatusCh(), SyncWorkerStatus{ Step: "RetrievePayload", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"}, + Actual: configv1.Release{Version: "1.0.1-abc", Image: "image/image:1"}, }, SyncWorkerStatus{ Step: "RetrievePayload", Failure: payloadErr, - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"}, + Actual: configv1.Release{Version: "1.0.1-abc", Image: "image/image:1"}, }, ) @@ -986,7 +1118,7 @@ func TestCVO_UpgradeUnverifiedPayload(t *testing.T) { Spec: configv1.ClusterVersionSpec{ ClusterID: clusterUID, Channel: "fast", - DesiredUpdate: &desired, + DesiredUpdate: &configv1.Update{Version: desired.Version, Image: desired.Image}, }, Status: configv1.ClusterVersionStatus{ // Prefers the image version over the operator's version (although in general they will remain in sync) @@ -1008,9 +1140,12 @@ func TestCVO_UpgradeUnverifiedPayload(t *testing.T) { // Step 2: Set allowUnverifiedImages to true, trigger a sync and the operator should apply the payload // - // set an updtae - copied := desired - copied.Force = true + // set an update + copied := configv1.Update{ + Version: desired.Version, + Image: desired.Image, + Force: true, + } actual.Spec.DesiredUpdate = &copied retriever.Set(PayloadInfo{Directory: "testdata/payloadtest-2", VerificationError: payloadErr}, nil) // @@ -1029,7 +1164,7 @@ func TestCVO_UpgradeUnverifiedPayload(t *testing.T) { case <-time.After(3 * time.Second): t.Fatalf("never saw expected sync event") } - if status.Step == "RetrievePayload" && reflect.DeepEqual(configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, status.Actual) { + if status.Step == "RetrievePayload" && reflect.DeepEqual(configv1.Release{Version: "1.0.1-abc", Image: "image/image:1"}, status.Actual) { break } t.Logf("Unexpected status waiting to see first retrieve: %#v", status) @@ -1040,37 +1175,52 @@ func TestCVO_UpgradeUnverifiedPayload(t *testing.T) { } verifyAllStatus(t, worker.StatusCh(), SyncWorkerStatus{ - Step: "ApplyResources", - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, + Step: "ApplyResources", + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, + Generation: 1, + }, + SyncWorkerStatus{ + Fraction: float32(1) / 3, + Step: "ApplyResources", + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, LastProgress: time.Unix(1, 0), Generation: 1, }, SyncWorkerStatus{ - Fraction: float32(1) / 3, - Step: "ApplyResources", - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, + Fraction: float32(2) / 3, + Step: "ApplyResources", + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, LastProgress: time.Unix(2, 0), Generation: 1, }, SyncWorkerStatus{ - Fraction: float32(2) / 3, - Step: "ApplyResources", - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, + Reconciling: true, + Completed: 1, + Fraction: 1, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, LastProgress: time.Unix(3, 0), Generation: 1, }, - SyncWorkerStatus{ - Reconciling: true, - Completed: 1, - Fraction: 1, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, - LastProgress: time.Unix(4, 0), - Generation: 1, - }, ) client.ClearActions() err = o.sync(ctx, o.queueKey()) @@ -1095,8 +1245,12 @@ func TestCVO_UpgradeUnverifiedPayload(t *testing.T) { }, Status: configv1.ClusterVersionStatus{ ObservedGeneration: 1, - Desired: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, - VersionHash: "6GC9TkkG9PA=", + Desired: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, + VersionHash: "6GC9TkkG9PA=", History: []configv1.UpdateHistory{ {State: configv1.CompletedUpdate, Image: "image/image:1", Version: "1.0.1-abc", StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, {State: configv1.CompletedUpdate, Image: "image/image:0", Version: "1.0.0-abc", Verified: true, StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, @@ -1116,9 +1270,9 @@ func TestCVO_UpgradeUnverifiedPayloadRetrieveOnce(t *testing.T) { // Setup: a successful sync from a previous run, and the operator at the same image as before // - o.releaseImage = "image/image:0" - o.releaseVersion = "1.0.0-abc" - desired := configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"} + o.release.Image = "image/image:0" + o.release.Version = "1.0.0-abc" + desired := configv1.Release{Version: "1.0.1-abc", Image: "image/image:1"} uid, _ := uuid.NewRandom() clusterUID := configv1.ClusterID(uid.String()) cvs["version"] = &configv1.ClusterVersion{ @@ -1129,7 +1283,7 @@ func TestCVO_UpgradeUnverifiedPayloadRetrieveOnce(t *testing.T) { Spec: configv1.ClusterVersionSpec{ ClusterID: clusterUID, Channel: "fast", - DesiredUpdate: &desired, + DesiredUpdate: &configv1.Update{Version: desired.Version, Image: desired.Image}, }, Status: configv1.ClusterVersionStatus{ // Prefers the image version over the operator's version (although in general they will remain in sync) @@ -1182,12 +1336,12 @@ func TestCVO_UpgradeUnverifiedPayloadRetrieveOnce(t *testing.T) { verifyAllStatus(t, worker.StatusCh(), SyncWorkerStatus{ Step: "RetrievePayload", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"}, + Actual: configv1.Release{Version: "1.0.1-abc", Image: "image/image:1"}, }, SyncWorkerStatus{ Step: "RetrievePayload", Failure: payloadErr, - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"}, + Actual: configv1.Release{Version: "1.0.1-abc", Image: "image/image:1"}, }, ) @@ -1211,7 +1365,7 @@ func TestCVO_UpgradeUnverifiedPayloadRetrieveOnce(t *testing.T) { Spec: configv1.ClusterVersionSpec{ ClusterID: clusterUID, Channel: "fast", - DesiredUpdate: &desired, + DesiredUpdate: &configv1.Update{Version: desired.Version, Image: desired.Image}, }, Status: configv1.ClusterVersionStatus{ // Prefers the image version over the operator's version (although in general they will remain in sync) @@ -1233,9 +1387,12 @@ func TestCVO_UpgradeUnverifiedPayloadRetrieveOnce(t *testing.T) { // Step 2: Set allowUnverifiedImages to true, trigger a sync and the operator should apply the payload // - // set an updtae - copied := desired - copied.Force = true + // set an update + copied := configv1.Update{ + Version: desired.Version, + Image: desired.Image, + Force: true, + } actual.Spec.DesiredUpdate = &copied retriever.Set(PayloadInfo{Directory: "testdata/payloadtest-2", VerificationError: payloadErr}, nil) // @@ -1254,7 +1411,7 @@ func TestCVO_UpgradeUnverifiedPayloadRetrieveOnce(t *testing.T) { case <-time.After(3 * time.Second): t.Fatalf("never saw expected sync event") } - if status.Step == "RetrievePayload" && reflect.DeepEqual(configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, status.Actual) { + if status.Step == "RetrievePayload" && reflect.DeepEqual(configv1.Release{Version: "1.0.1-abc", Image: "image/image:1"}, status.Actual) { break } t.Logf("Unexpected status waiting to see first retrieve: %#v", status) @@ -1265,37 +1422,52 @@ func TestCVO_UpgradeUnverifiedPayloadRetrieveOnce(t *testing.T) { } verifyAllStatus(t, worker.StatusCh(), SyncWorkerStatus{ - Step: "ApplyResources", - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, + Step: "ApplyResources", + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, + Generation: 1, + }, + SyncWorkerStatus{ + Fraction: float32(1) / 3, + Step: "ApplyResources", + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, LastProgress: time.Unix(1, 0), Generation: 1, }, SyncWorkerStatus{ - Fraction: float32(1) / 3, - Step: "ApplyResources", - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, + Fraction: float32(2) / 3, + Step: "ApplyResources", + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, LastProgress: time.Unix(2, 0), Generation: 1, }, SyncWorkerStatus{ - Fraction: float32(2) / 3, - Step: "ApplyResources", - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, + Reconciling: true, + Completed: 1, + Fraction: 1, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, LastProgress: time.Unix(3, 0), Generation: 1, }, - SyncWorkerStatus{ - Reconciling: true, - Completed: 1, - Fraction: 1, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, - LastProgress: time.Unix(4, 0), - Generation: 1, - }, ) client.ClearActions() err = o.sync(ctx, o.queueKey()) @@ -1320,8 +1492,12 @@ func TestCVO_UpgradeUnverifiedPayloadRetrieveOnce(t *testing.T) { }, Status: configv1.ClusterVersionStatus{ ObservedGeneration: 1, - Desired: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, - VersionHash: "6GC9TkkG9PA=", + Desired: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, + VersionHash: "6GC9TkkG9PA=", History: []configv1.UpdateHistory{ {State: configv1.CompletedUpdate, Image: "image/image:1", Version: "1.0.1-abc", StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, {State: configv1.CompletedUpdate, Image: "image/image:0", Version: "1.0.0-abc", Verified: true, StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, @@ -1342,31 +1518,47 @@ func TestCVO_UpgradeUnverifiedPayloadRetrieveOnce(t *testing.T) { Reconciling: true, Step: "ApplyResources", VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, - Generation: 1, + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, + Generation: 1, }, SyncWorkerStatus{ Reconciling: true, Fraction: float32(1) / 3, Step: "ApplyResources", VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, - Generation: 1, + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, + Generation: 1, }, SyncWorkerStatus{ Reconciling: true, Fraction: float32(2) / 3, Step: "ApplyResources", VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, - Generation: 1, + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, + Generation: 1, }, SyncWorkerStatus{ - Reconciling: true, - Completed: 2, - Fraction: 1, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, + Reconciling: true, + Completed: 2, + Fraction: 1, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, LastProgress: time.Unix(1, 0), Generation: 1, }, @@ -1378,9 +1570,9 @@ func TestCVO_UpgradePreconditionFailing(t *testing.T) { // Setup: a successful sync from a previous run, and the operator at the same image as before // - o.releaseImage = "image/image:0" - o.releaseVersion = "1.0.0-abc" - desired := configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"} + o.release.Image = "image/image:0" + o.release.Version = "1.0.0-abc" + desired := configv1.Release{Version: "1.0.1-abc", Image: "image/image:1"} uid, _ := uuid.NewRandom() clusterUID := configv1.ClusterID(uid.String()) cvs["version"] = &configv1.ClusterVersion{ @@ -1391,7 +1583,7 @@ func TestCVO_UpgradePreconditionFailing(t *testing.T) { Spec: configv1.ClusterVersionSpec{ ClusterID: clusterUID, Channel: "fast", - DesiredUpdate: &desired, + DesiredUpdate: &configv1.Update{Version: desired.Version, Image: desired.Image}, }, Status: configv1.ClusterVersionStatus{ // Prefers the image version over the operator's version (although in general they will remain in sync) @@ -1434,16 +1626,16 @@ func TestCVO_UpgradePreconditionFailing(t *testing.T) { verifyAllStatus(t, worker.StatusCh(), SyncWorkerStatus{ Step: "RetrievePayload", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"}, + Actual: configv1.Release{Version: "1.0.1-abc", Image: "image/image:1"}, }, SyncWorkerStatus{ Step: "PreconditionChecks", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"}, + Actual: configv1.Release{Version: "1.0.1-abc", Image: "image/image:1"}, }, SyncWorkerStatus{ Step: "PreconditionChecks", Failure: &payload.UpdateError{Reason: "UpgradePreconditionCheckFailed", Message: "Precondition \"TestPrecondition SuccessAfter: 3\" failed because of \"CheckFailure\": failing, attempt: 1 will succeed after 3 attempt", Name: "PreconditionCheck"}, - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"}, + Actual: configv1.Release{Version: "1.0.1-abc", Image: "image/image:1"}, }, ) @@ -1467,7 +1659,7 @@ func TestCVO_UpgradePreconditionFailing(t *testing.T) { Spec: configv1.ClusterVersionSpec{ ClusterID: clusterUID, Channel: "fast", - DesiredUpdate: &desired, + DesiredUpdate: &configv1.Update{Version: desired.Version, Image: desired.Image}, }, Status: configv1.ClusterVersionStatus{ // Prefers the image version over the operator's version (although in general they will remain in sync) @@ -1489,9 +1681,12 @@ func TestCVO_UpgradePreconditionFailing(t *testing.T) { // Step 2: Set allowUnverifiedImages to true, trigger a sync and the operator should apply the payload // - // set an updtae - copied := desired - copied.Force = true + // set an update + copied := configv1.Update{ + Version: desired.Version, + Image: desired.Image, + Force: true, + } actual.Spec.DesiredUpdate = &copied // // ensure the sync worker tells the sync loop about it @@ -1509,7 +1704,7 @@ func TestCVO_UpgradePreconditionFailing(t *testing.T) { case <-time.After(3 * time.Second): t.Fatalf("never saw expected sync event") } - if status.Step == "RetrievePayload" && reflect.DeepEqual(configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, status.Actual) { + if status.Step == "RetrievePayload" && reflect.DeepEqual(configv1.Release{Version: "1.0.1-abc", Image: "image/image:1"}, status.Actual) { break } t.Logf("Unexpected status waiting to see first retrieve: %#v", status) @@ -1520,41 +1715,55 @@ func TestCVO_UpgradePreconditionFailing(t *testing.T) { } verifyAllStatus(t, worker.StatusCh(), SyncWorkerStatus{ - Step: "PreconditionChecks", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, - LastProgress: time.Unix(1, 0), - Generation: 1, + Step: "PreconditionChecks", + Actual: configv1.Release{Version: "1.0.1-abc", Image: "image/image:1"}, + Generation: 1, }, SyncWorkerStatus{ - Step: "ApplyResources", - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, - LastProgress: time.Unix(2, 0), - Generation: 1, + Step: "ApplyResources", + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, + Generation: 1, }, SyncWorkerStatus{ - Fraction: float32(1) / 3, - Step: "ApplyResources", - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, - LastProgress: time.Unix(3, 0), + Fraction: float32(1) / 3, + Step: "ApplyResources", + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, + LastProgress: time.Unix(1, 0), Generation: 1, }, SyncWorkerStatus{ - Fraction: float32(2) / 3, - Step: "ApplyResources", - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, - LastProgress: time.Unix(4, 0), + Fraction: float32(2) / 3, + Step: "ApplyResources", + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, + LastProgress: time.Unix(2, 0), Generation: 1, }, SyncWorkerStatus{ - Reconciling: true, - Completed: 1, - Fraction: 1, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, - LastProgress: time.Unix(5, 0), + Reconciling: true, + Completed: 1, + Fraction: 1, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, + LastProgress: time.Unix(3, 0), Generation: 1, }, ) @@ -1581,8 +1790,12 @@ func TestCVO_UpgradePreconditionFailing(t *testing.T) { }, Status: configv1.ClusterVersionStatus{ ObservedGeneration: 1, - Desired: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1", Force: true}, - VersionHash: "6GC9TkkG9PA=", + Desired: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, + VersionHash: "6GC9TkkG9PA=", History: []configv1.UpdateHistory{ {State: configv1.CompletedUpdate, Image: "image/image:1", Version: "1.0.1-abc", StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, {State: configv1.CompletedUpdate, Image: "image/image:0", Version: "1.0.0-abc", Verified: true, StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, @@ -1602,9 +1815,9 @@ func TestCVO_UpgradeVerifiedPayload(t *testing.T) { // Setup: a successful sync from a previous run, and the operator at the same image as before // - o.releaseImage = "image/image:0" - o.releaseVersion = "1.0.0-abc" - desired := configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"} + o.release.Image = "image/image:0" + o.release.Version = "1.0.0-abc" + desired := configv1.Release{Version: "1.0.1-abc", Image: "image/image:1"} uid, _ := uuid.NewRandom() clusterUID := configv1.ClusterID(uid.String()) cvs["version"] = &configv1.ClusterVersion{ @@ -1616,7 +1829,7 @@ func TestCVO_UpgradeVerifiedPayload(t *testing.T) { Spec: configv1.ClusterVersionSpec{ ClusterID: clusterUID, Channel: "fast", - DesiredUpdate: &desired, + DesiredUpdate: &configv1.Update{Version: desired.Version, Image: desired.Image}, }, Status: configv1.ClusterVersionStatus{ // Prefers the image version over the operator's version (although in general they will remain in sync) @@ -1669,13 +1882,13 @@ func TestCVO_UpgradeVerifiedPayload(t *testing.T) { verifyAllStatus(t, worker.StatusCh(), SyncWorkerStatus{ Step: "RetrievePayload", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"}, + Actual: configv1.Release{Version: "1.0.1-abc", Image: "image/image:1"}, Generation: 1, }, SyncWorkerStatus{ Step: "RetrievePayload", Failure: payloadErr, - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"}, + Actual: configv1.Release{Version: "1.0.1-abc", Image: "image/image:1"}, Generation: 1, }, ) @@ -1700,7 +1913,7 @@ func TestCVO_UpgradeVerifiedPayload(t *testing.T) { Spec: configv1.ClusterVersionSpec{ ClusterID: clusterUID, Channel: "fast", - DesiredUpdate: &desired, + DesiredUpdate: &configv1.Update{Version: desired.Version, Image: desired.Image}, }, Status: configv1.ClusterVersionStatus{ ObservedGeneration: 1, @@ -1723,7 +1936,11 @@ func TestCVO_UpgradeVerifiedPayload(t *testing.T) { // Step 2: Simulate a verified payload being retrieved and ensure the operator sets verified // - copied := desired + // set an update + copied := configv1.Update{ + Version: desired.Version, + Image: desired.Image, + } actual.ObjectMeta.Generation = 2 actual.Spec.DesiredUpdate = &copied retriever.Set(PayloadInfo{Directory: "testdata/payloadtest-2", Verified: true}, nil) @@ -1742,40 +1959,56 @@ func TestCVO_UpgradeVerifiedPayload(t *testing.T) { verifyAllStatus(t, worker.StatusCh(), SyncWorkerStatus{ Step: "RetrievePayload", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"}, + Actual: configv1.Release{Version: "1.0.1-abc", Image: "image/image:1"}, Generation: 2, }, SyncWorkerStatus{ Step: "ApplyResources", VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"}, - Verified: true, - Generation: 2, + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, + Verified: true, + Generation: 2, }, SyncWorkerStatus{ - Fraction: float32(1) / 3, - Step: "ApplyResources", - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"}, + Fraction: float32(1) / 3, + Step: "ApplyResources", + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, LastProgress: time.Unix(1, 0), Verified: true, Generation: 2, }, SyncWorkerStatus{ - Fraction: float32(2) / 3, - Step: "ApplyResources", - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"}, + Fraction: float32(2) / 3, + Step: "ApplyResources", + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, LastProgress: time.Unix(2, 0), Verified: true, Generation: 2, }, SyncWorkerStatus{ - Reconciling: true, - Completed: 1, - Fraction: 1, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"}, + Reconciling: true, + Completed: 1, + Fraction: 1, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, LastProgress: time.Unix(3, 0), Verified: true, Generation: 2, @@ -1804,8 +2037,12 @@ func TestCVO_UpgradeVerifiedPayload(t *testing.T) { }, Status: configv1.ClusterVersionStatus{ ObservedGeneration: 2, - Desired: configv1.Update{Version: "1.0.1-abc", Image: "image/image:1"}, - VersionHash: "6GC9TkkG9PA=", + Desired: configv1.Release{ + Version: "1.0.1-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.1-abc"), + }, + VersionHash: "6GC9TkkG9PA=", History: []configv1.UpdateHistory{ {State: configv1.CompletedUpdate, Image: "image/image:1", Version: "1.0.1-abc", Verified: true, StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, {State: configv1.CompletedUpdate, Image: "image/image:0", Version: "1.0.0-abc", Verified: true, StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, @@ -1830,9 +2067,10 @@ func TestCVO_RestartAndReconcile(t *testing.T) { // Setup: a successful sync from a previous run, and the operator at the same image as before // - o.releaseImage = "image/image:1" - o.releaseVersion = "1.0.0-abc" - desired := configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"} + o.release.Image = "image/image:1" + o.release.Version = "1.0.0-abc" + o.release.URL = configv1.URL("https://example.com/v1.0.0-abc") + o.release.Channels = []string{"channel-a", "channel-b", "channel-c"} uid, _ := uuid.NewRandom() clusterUID := configv1.ClusterID(uid.String()) cvs["version"] = &configv1.ClusterVersion{ @@ -1846,7 +2084,12 @@ func TestCVO_RestartAndReconcile(t *testing.T) { }, Status: configv1.ClusterVersionStatus{ // Prefers the image version over the operator's version (although in general they will remain in sync) - Desired: desired, + Desired: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, VersionHash: "6GC9TkkG9PA=", History: []configv1.UpdateHistory{ // TODO: this is wrong, should be single partial entry @@ -1894,36 +2137,56 @@ func TestCVO_RestartAndReconcile(t *testing.T) { SyncWorkerStatus{ Reconciling: true, Step: "RetrievePayload", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Actual: configv1.Release{Version: "1.0.0-abc", Image: "image/image:1"}, }, SyncWorkerStatus{ Reconciling: true, Step: "ApplyResources", VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, }, SyncWorkerStatus{ - Reconciling: true, - Fraction: float32(1) / 3, - Step: "ApplyResources", - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Reconciling: true, + Fraction: float32(1) / 3, + Step: "ApplyResources", + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(1, 0), }, SyncWorkerStatus{ - Reconciling: true, - Fraction: float32(2) / 3, - Step: "ApplyResources", - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Reconciling: true, + Fraction: float32(2) / 3, + Step: "ApplyResources", + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(2, 0), }, SyncWorkerStatus{ - Reconciling: true, - Completed: 1, - Fraction: 1, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Reconciling: true, + Completed: 1, + Fraction: 1, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(3, 0), }, ) @@ -1947,28 +2210,48 @@ func TestCVO_RestartAndReconcile(t *testing.T) { Reconciling: true, Step: "ApplyResources", VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, }, SyncWorkerStatus{ Reconciling: true, Fraction: float32(1) / 3, Step: "ApplyResources", VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, }, SyncWorkerStatus{ Reconciling: true, Fraction: float32(2) / 3, Step: "ApplyResources", VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, }, SyncWorkerStatus{ - Reconciling: true, - Completed: 2, - Fraction: 1, - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Reconciling: true, + Completed: 2, + Fraction: 1, + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(1, 0), }, ) @@ -1997,9 +2280,10 @@ func TestCVO_ErrorDuringReconcile(t *testing.T) { // Setup: a successful sync from a previous run, and the operator at the same image as before // - o.releaseImage = "image/image:1" - o.releaseVersion = "1.0.0-abc" - desired := configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"} + o.release.Image = "image/image:1" + o.release.Version = "1.0.0-abc" + o.release.URL = configv1.URL("https://example.com/v1.0.0-abc") + o.release.Channels = []string{"channel-a", "channel-b", "channel-c"} uid, _ := uuid.NewRandom() clusterUID := configv1.ClusterID(uid.String()) cvs["version"] = &configv1.ClusterVersion{ @@ -2013,7 +2297,12 @@ func TestCVO_ErrorDuringReconcile(t *testing.T) { }, Status: configv1.ClusterVersionStatus{ // Prefers the image version over the operator's version (although in general they will remain in sync) - Desired: desired, + Desired: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, VersionHash: "6GC9TkkG9PA=", History: []configv1.UpdateHistory{ {State: configv1.CompletedUpdate, Image: "image/image:1", Version: "1.0.0-abc", Verified: true, StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, @@ -2054,13 +2343,18 @@ func TestCVO_ErrorDuringReconcile(t *testing.T) { SyncWorkerStatus{ Reconciling: true, Step: "RetrievePayload", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Actual: configv1.Release{Version: "1.0.0-abc", Image: "image/image:1"}, }, SyncWorkerStatus{ Reconciling: true, Step: "ApplyResources", VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, }, ) // verify we haven't observed any other events @@ -2087,11 +2381,16 @@ func TestCVO_ErrorDuringReconcile(t *testing.T) { // verify we observe the remaining changes in the first sync verifyAllStatus(t, worker.StatusCh(), SyncWorkerStatus{ - Reconciling: true, - Fraction: float32(1) / 3, - Step: "ApplyResources", - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Reconciling: true, + Fraction: float32(1) / 3, + Step: "ApplyResources", + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(1, 0), }, ) @@ -2104,11 +2403,16 @@ func TestCVO_ErrorDuringReconcile(t *testing.T) { // verify we observe the remaining changes in the first sync verifyAllStatus(t, worker.StatusCh(), SyncWorkerStatus{ - Reconciling: true, - Fraction: float32(2) / 3, - Step: "ApplyResources", - VersionHash: "6GC9TkkG9PA=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Reconciling: true, + Fraction: float32(2) / 3, + Step: "ApplyResources", + VersionHash: "6GC9TkkG9PA=", + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(1, 0), }, ) @@ -2142,7 +2446,12 @@ func TestCVO_ErrorDuringReconcile(t *testing.T) { Message: "Could not update test \"file-yml\" (3 of 3)", Task: &payload.Task{Index: 3, Total: 3, Manifest: &worker.payload.Manifests[2]}, }, - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Actual: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, LastProgress: time.Unix(1, 0), }, ) @@ -2167,7 +2476,12 @@ func TestCVO_ErrorDuringReconcile(t *testing.T) { }, Status: configv1.ClusterVersionStatus{ // Prefers the image version over the operator's version (although in general they will remain in sync) - Desired: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Desired: configv1.Release{ + Version: "1.0.0-abc", + Image: "image/image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, VersionHash: "6GC9TkkG9PA=", History: []configv1.UpdateHistory{ {State: configv1.CompletedUpdate, Image: "image/image:1", Version: "1.0.0-abc", Verified: true, StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, @@ -2205,9 +2519,9 @@ func TestCVO_ParallelError(t *testing.T) { // Setup: an initializing cluster version which will run in parallel // - o.releaseImage = "image/image:1" - o.releaseVersion = "1.0.0-abc" - desired := configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"} + o.release.Image = "image/image:1" + o.release.Version = "1.0.0-abc" + desired := configv1.Release{Version: "1.0.0-abc", Image: "image/image:1"} uid, _ := uuid.NewRandom() clusterUID := configv1.ClusterID(uid.String()) cvs["version"] = &configv1.ClusterVersion{ @@ -2258,13 +2572,13 @@ func TestCVO_ParallelError(t *testing.T) { SyncWorkerStatus{ Initial: true, Step: "RetrievePayload", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Actual: configv1.Release{Version: "1.0.0-abc", Image: "image/image:1"}, }, SyncWorkerStatus{ Initial: true, Step: "ApplyResources", VersionHash: "7m-gGRrpkDU=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Actual: configv1.Release{Version: "1.0.0-abc", Image: "image/image:1"}, }, ) @@ -2282,7 +2596,7 @@ func TestCVO_ParallelError(t *testing.T) { Fraction: status.Fraction, Step: "ApplyResources", VersionHash: "7m-gGRrpkDU=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Actual: configv1.Release{Version: "1.0.0-abc", Image: "image/image:1"}, }) { t.Fatalf("unexpected status: %v", status) } @@ -2303,7 +2617,7 @@ func TestCVO_ParallelError(t *testing.T) { Fraction: float32(1) / 3, Step: "ApplyResources", VersionHash: "7m-gGRrpkDU=", - Actual: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Actual: configv1.Release{Version: "1.0.0-abc", Image: "image/image:1"}, LastProgress: status.LastProgress, }) { t.Fatalf("unexpected final: %v", status) @@ -2334,7 +2648,7 @@ func TestCVO_ParallelError(t *testing.T) { }, Status: configv1.ClusterVersionStatus{ // Prefers the image version over the operator's version (although in general they will remain in sync) - Desired: configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"}, + Desired: configv1.Release{Version: "1.0.0-abc", Image: "image/image:1"}, VersionHash: "7m-gGRrpkDU=", History: []configv1.UpdateHistory{ {State: configv1.PartialUpdate, Image: "image/image:1", Version: "1.0.0-abc", StartedTime: defaultStartedTime}, @@ -2361,9 +2675,9 @@ func TestCVO_VerifyInitializingPayloadState(t *testing.T) { // Setup: a successful sync from a previous run, and the operator at the same image as before // - o.releaseImage = "image/image:1" - o.releaseVersion = "1.0.0-abc" - desired := configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"} + o.release.Image = "image/image:1" + o.release.Version = "1.0.0-abc" + desired := configv1.Release{Version: "1.0.0-abc", Image: "image/image:1"} uid, _ := uuid.NewRandom() clusterUID := configv1.ClusterID(uid.String()) cvs["version"] = &configv1.ClusterVersion{ @@ -2420,9 +2734,9 @@ func TestCVO_VerifyUpdatingPayloadState(t *testing.T) { // Setup: a successful sync from a previous run, and the operator at the same image as before // - o.releaseImage = "image/image:1" - o.releaseVersion = "1.0.0-abc" - desired := configv1.Update{Version: "1.0.0-abc", Image: "image/image:1"} + o.release.Image = "image/image:1" + o.release.Version = "1.0.0-abc" + desired := configv1.Release{Version: "1.0.0-abc", Image: "image/image:1"} uid, _ := uuid.NewRandom() clusterUID := configv1.ClusterID(uid.String()) cvs["version"] = &configv1.ClusterVersion{ diff --git a/pkg/cvo/cvo_test.go b/pkg/cvo/cvo_test.go index e7dd52acc2..15335cd6f1 100644 --- a/pkg/cvo/cvo_test.go +++ b/pkg/cvo/cvo_test.go @@ -272,8 +272,10 @@ func TestOperator_sync(t *testing.T) { { name: "create version and status", optr: Operator{ - releaseVersion: "4.0.1", - releaseImage: "image/image:v4.0.1", + release: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + }, enableDefaultClusterVersion: true, namespace: "test", name: "default", @@ -302,17 +304,19 @@ func TestOperator_sync(t *testing.T) { syncStatus: &SyncWorkerStatus{ Step: "Moving", Reconciling: false, - Actual: configv1.Update{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, + Actual: configv1.Release{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, Failure: &payload.UpdateError{ Reason: "UpdatePayloadIntegrity", Message: "unable to apply object", }, }, optr: Operator{ - releaseVersion: "4.0.1", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fakeClientsetWithUpdates( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -325,7 +329,7 @@ func TestOperator_sync(t *testing.T) { History: []configv1.UpdateHistory{ {Version: "4.0.1", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime}, }, - Desired: configv1.Update{Version: "4.0.1", Image: "image/image:v4.0.1"}, + Desired: configv1.Release{Version: "4.0.1", Image: "image/image:v4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -356,7 +360,7 @@ func TestOperator_sync(t *testing.T) { {State: configv1.PartialUpdate, Version: "0.0.1-abc", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime}, {State: configv1.PartialUpdate, Version: "4.0.1", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, }, - Desired: configv1.Update{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, + Desired: configv1.Release{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -371,15 +375,17 @@ func TestOperator_sync(t *testing.T) { { name: "progressing and previously failed, reconciling", optr: Operator{ - releaseVersion: "4.0.1", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", configSync: &fakeSyncRecorder{ Returns: &SyncWorkerStatus{ Step: "Moving", Reconciling: true, - Actual: configv1.Update{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, + Actual: configv1.Release{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, Failure: &payload.UpdateError{ Reason: "UpdatePayloadIntegrity", Message: "unable to apply object", @@ -398,7 +404,7 @@ func TestOperator_sync(t *testing.T) { History: []configv1.UpdateHistory{ {Version: "4.0.1", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime}, }, - Desired: configv1.Update{Version: "4.0.1", Image: "image/image:v4.0.1"}, + Desired: configv1.Release{Version: "4.0.1", Image: "image/image:v4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -429,7 +435,7 @@ func TestOperator_sync(t *testing.T) { {State: configv1.PartialUpdate, Version: "0.0.1-abc", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime}, {State: configv1.PartialUpdate, Version: "4.0.1", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, }, - Desired: configv1.Update{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, + Desired: configv1.Release{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -444,16 +450,18 @@ func TestOperator_sync(t *testing.T) { { name: "progressing and previously failed, reconciling and multiple completions", optr: Operator{ - releaseVersion: "4.0.1", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", configSync: &fakeSyncRecorder{ Returns: &SyncWorkerStatus{ Step: "Moving", Reconciling: true, Completed: 2, - Actual: configv1.Update{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, + Actual: configv1.Release{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, Failure: &payload.UpdateError{ Reason: "UpdatePayloadIntegrity", Message: "unable to apply object", @@ -472,7 +480,7 @@ func TestOperator_sync(t *testing.T) { History: []configv1.UpdateHistory{ {Version: "4.0.1", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime}, }, - Desired: configv1.Update{Version: "4.0.1", Image: "image/image:v4.0.1"}, + Desired: configv1.Release{Version: "4.0.1", Image: "image/image:v4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -503,7 +511,7 @@ func TestOperator_sync(t *testing.T) { {State: configv1.CompletedUpdate, Version: "0.0.1-abc", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, {State: configv1.PartialUpdate, Version: "4.0.1", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, }, - Desired: configv1.Update{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, + Desired: configv1.Release{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionTrue, Message: "Done applying 0.0.1-abc"}, @@ -518,14 +526,16 @@ func TestOperator_sync(t *testing.T) { { name: "progressing and encounters error during image sync", optr: Operator{ - releaseVersion: "4.0.1", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", configSync: &fakeSyncRecorder{ Returns: &SyncWorkerStatus{ Step: "Moving", - Actual: configv1.Update{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, + Actual: configv1.Release{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, Failure: fmt.Errorf("injected error"), VersionHash: "foo", }, @@ -569,7 +579,7 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Desired: configv1.Update{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, + Desired: configv1.Release{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, History: []configv1.UpdateHistory{ {State: configv1.PartialUpdate, Version: "0.0.1-abc", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime}, {State: configv1.PartialUpdate, Version: "4.0.1", Image: "image/image:v4.0.1", StartedTime: metav1.Time{Time: time.Unix(0, 0)}, CompletionTime: &defaultCompletionTime}, @@ -589,13 +599,15 @@ func TestOperator_sync(t *testing.T) { name: "invalid image reports image error", syncStatus: &SyncWorkerStatus{ Failure: os.ErrNotExist, - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "4.0.1"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "4.0.1"}, }, optr: Operator{ - releaseVersion: "4.0.1", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -622,7 +634,7 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Desired: configv1.Update{Image: "image/image:v4.0.1", Version: "4.0.1"}, + Desired: configv1.Release{Image: "image/image:v4.0.1", Version: "4.0.1"}, History: []configv1.UpdateHistory{ {State: configv1.PartialUpdate, Version: "4.0.1", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime}, }, @@ -643,13 +655,15 @@ func TestOperator_sync(t *testing.T) { Step: "Working", Fraction: 0.6, Failure: os.ErrNotExist, - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "4.0.1"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "4.0.1"}, }, optr: Operator{ - releaseVersion: "4.0.1", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -686,7 +700,7 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Desired: configv1.Update{Image: "image/image:v4.0.1", Version: "4.0.1"}, + Desired: configv1.Release{Image: "image/image:v4.0.1", Version: "4.0.1"}, History: []configv1.UpdateHistory{ // we populate state, but not startedTime {State: configv1.PartialUpdate, Version: "4.0.1", Image: "image/image:v4.0.1", StartedTime: metav1.Time{time.Unix(0, 0)}}, @@ -706,12 +720,14 @@ func TestOperator_sync(t *testing.T) { { name: "set initial status conditions", syncStatus: &SyncWorkerStatus{ - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: ""}, + Actual: configv1.Release{Image: "image/image:v4.0.1"}, }, optr: Operator{ - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fakeClientsetWithUpdates(&configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ Name: "default", @@ -745,11 +761,11 @@ func TestOperator_sync(t *testing.T) { { State: configv1.PartialUpdate, Image: "image/image:v4.0.1", - Version: "", // we don't know our image yet and releaseVersion is unset + Version: "", // we don't know our image yet and release.Version is unset StartedTime: defaultStartedTime, }, }, - Desired: configv1.Update{Version: "", Image: "image/image:v4.0.1"}, + Desired: configv1.Release{Image: "image/image:v4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -764,13 +780,15 @@ func TestOperator_sync(t *testing.T) { { name: "record a new version entry if the controller is restarted with a new image", syncStatus: &SyncWorkerStatus{ - Actual: configv1.Update{Image: "image/image:v4.0.2", Version: "4.0.2"}, + Actual: configv1.Release{Image: "image/image:v4.0.2", Version: "4.0.2"}, }, optr: Operator{ - releaseImage: "image/image:v4.0.2", - releaseVersion: "4.0.2", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "4.0.2", + Image: "image/image:v4.0.2", + }, + namespace: "test", + name: "default", client: fakeClientsetWithUpdates(&configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ Name: "default", @@ -790,7 +808,7 @@ func TestOperator_sync(t *testing.T) { StartedTime: defaultStartedTime, }, }, - Desired: configv1.Update{Image: "image/image:v4.0.1"}, + Desired: configv1.Release{Image: "image/image:v4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -833,7 +851,7 @@ func TestOperator_sync(t *testing.T) { CompletionTime: &defaultCompletionTime, }, }, - Desired: configv1.Update{Image: "image/image:v4.0.2", Version: "4.0.2"}, + Desired: configv1.Release{Image: "image/image:v4.0.2", Version: "4.0.2"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -854,13 +872,15 @@ func TestOperator_sync(t *testing.T) { // update unless we add some sort of delay - which might make clearing status // slightly more useful to the user (instead of two status updates you get // one). - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "4.0.1"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "4.0.1"}, }, optr: Operator{ - releaseImage: "image/image:v4.0.1", - releaseVersion: "4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fakeClientsetWithUpdates(&configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ Name: "default", @@ -887,7 +907,7 @@ func TestOperator_sync(t *testing.T) { CompletionTime: &defaultCompletionTime, }, }, - Desired: configv1.Update{Image: "image/image:v4.0.2"}, + Desired: configv1.Release{Image: "image/image:v4.0.2"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -937,7 +957,7 @@ func TestOperator_sync(t *testing.T) { CompletionTime: &defaultCompletionTime, }, }, - Desired: configv1.Update{ + Desired: configv1.Release{ Version: "4.0.1", Image: "image/image:v4.0.1", }, @@ -956,14 +976,16 @@ func TestOperator_sync(t *testing.T) { { name: "after desired update is cancelled, revert to progressing", syncStatus: &SyncWorkerStatus{ - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "4.0.1"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "4.0.1"}, Fraction: 0.334, }, optr: Operator{ - releaseImage: "image/image:v4.0.1", - releaseVersion: "4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fakeClientsetWithUpdates(&configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ Name: "default", @@ -997,7 +1019,7 @@ func TestOperator_sync(t *testing.T) { CompletionTime: &defaultCompletionTime, }, }, - Desired: configv1.Update{Image: "image/image:v4.0.1", Version: "4.0.1"}, + Desired: configv1.Release{Image: "image/image:v4.0.1", Version: "4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -1048,7 +1070,7 @@ func TestOperator_sync(t *testing.T) { CompletionTime: &defaultCompletionTime, }, }, - Desired: configv1.Update{Image: "image/image:v4.0.1", Version: "4.0.1"}, + Desired: configv1.Release{Image: "image/image:v4.0.1", Version: "4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -1064,14 +1086,16 @@ func TestOperator_sync(t *testing.T) { { name: "report partial retrieved version", syncStatus: &SyncWorkerStatus{ - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: ""}, + Actual: configv1.Release{Image: "image/image:v4.0.1"}, Step: "RetrievePayload", }, optr: Operator{ - releaseImage: "image/image:v4.0.1", - releaseVersion: "4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fakeClientsetWithUpdates(&configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ Name: "default", @@ -1099,7 +1123,7 @@ func TestOperator_sync(t *testing.T) { CompletionTime: &defaultCompletionTime, }, }, - Desired: configv1.Update{Image: "image/image:v4.0.1", Version: "4.0.1"}, + Desired: configv1.Release{Image: "image/image:v4.0.1", Version: "4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{}, }, @@ -1126,7 +1150,7 @@ func TestOperator_sync(t *testing.T) { { State: configv1.PartialUpdate, Image: "image/image:v4.0.1", - Version: "", + Version: "4.0.1", StartedTime: defaultStartedTime, }, { @@ -1144,7 +1168,10 @@ func TestOperator_sync(t *testing.T) { CompletionTime: &defaultCompletionTime, }, }, - Desired: configv1.Update{Image: "image/image:v4.0.1", Version: ""}, + Desired: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + }, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -1161,12 +1188,14 @@ func TestOperator_sync(t *testing.T) { name: "after initial status is set, set hash and correct version number", syncStatus: &SyncWorkerStatus{ VersionHash: "xyz", - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, }, optr: Operator{ - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fakeClientsetWithUpdates(&configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ Name: "default", @@ -1185,7 +1214,7 @@ func TestOperator_sync(t *testing.T) { StartedTime: defaultStartedTime, }, }, - Desired: configv1.Update{Image: "image/image:v4.0.1"}, + Desired: configv1.Release{Image: "image/image:v4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -1217,7 +1246,7 @@ func TestOperator_sync(t *testing.T) { History: []configv1.UpdateHistory{ {State: configv1.PartialUpdate, Image: "image/image:v4.0.1", Version: "0.0.1-abc", StartedTime: defaultStartedTime}, }, - Desired: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Desired: configv1.Release{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, VersionHash: "xyz", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -1236,13 +1265,15 @@ func TestOperator_sync(t *testing.T) { Reconciling: true, Completed: 1, VersionHash: "xyz", - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, }, optr: Operator{ - releaseImage: "image/image:v4.0.1", - releaseVersion: "0.0.1-abc", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "0.0.1-abc", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fakeClientsetWithUpdates( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -1263,7 +1294,7 @@ func TestOperator_sync(t *testing.T) { CompletionTime: &defaultStartedTime, }, }, - Desired: configv1.Update{ + Desired: configv1.Release{ Version: "0.0.1-abc", Image: "image/image:v4.0.1", }, @@ -1294,17 +1325,30 @@ func TestOperator_sync(t *testing.T) { Generation: 2, Reconciling: true, Completed: 1, - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, }, optr: Operator{ - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", availableUpdates: &availableUpdates{ Upstream: "http://localhost:8080/graph", Channel: "fast", - Updates: []configv1.Update{ - {Version: "4.0.2", Image: "test/image:1"}, + Current: configv1.Release{ + Version: "0.0.1-abc", + Image: "image/image:v4.0.1", + URL: configv1.URL("https://example.com/v4.0.1"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, + Updates: []configv1.Release{ + { + Version: "4.0.2", + Image: "test/image:1", + URL: configv1.URL("https://example.com/v4.0.2"), + Channels: []string{"channel-a", "channel-d"}, + }, {Version: "4.0.3", Image: "test/image:2"}, }, Condition: configv1.ClusterOperatorStatusCondition{ @@ -1351,14 +1395,24 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - AvailableUpdates: []configv1.Update{ - {Version: "4.0.2", Image: "test/image:1"}, + AvailableUpdates: []configv1.Release{ + { + Version: "4.0.2", + Image: "test/image:1", + URL: configv1.URL("https://example.com/v4.0.2"), + Channels: []string{"channel-a", "channel-d"}, + }, {Version: "4.0.3", Image: "test/image:2"}, }, History: []configv1.UpdateHistory{ {State: configv1.CompletedUpdate, Version: "0.0.1-abc", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, }, - Desired: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Desired: configv1.Release{ + Image: "image/image:v4.0.1", + Version: "0.0.1-abc", + URL: configv1.URL("https://example.com/v4.0.1"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, ObservedGeneration: 2, Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionTrue, Message: "Done applying 0.0.1-abc"}, @@ -1376,12 +1430,14 @@ func TestOperator_sync(t *testing.T) { Generation: 2, Reconciling: true, Completed: 1, - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, }, optr: Operator{ - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", upgradeable: &upgradeable{ Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.ClusterStatusConditionType("Upgradeable"), Status: configv1.ConditionFalse}, @@ -1431,7 +1487,7 @@ func TestOperator_sync(t *testing.T) { History: []configv1.UpdateHistory{ {State: configv1.CompletedUpdate, Version: "0.0.1-abc", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, }, - Desired: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Desired: configv1.Release{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, ObservedGeneration: 2, Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionTrue, Message: "Done applying 0.0.1-abc"}, @@ -1452,12 +1508,14 @@ func TestOperator_sync(t *testing.T) { Generation: 2, Reconciling: true, Completed: 1, - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, }, optr: Operator{ - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", upgradeable: &upgradeable{ Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.ClusterStatusConditionType("Upgradeable"), Status: configv1.ConditionFalse}, @@ -1507,7 +1565,7 @@ func TestOperator_sync(t *testing.T) { History: []configv1.UpdateHistory{ {State: configv1.CompletedUpdate, Version: "0.0.1-abc", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, }, - Desired: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Desired: configv1.Release{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, ObservedGeneration: 2, Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionTrue, Message: "Done applying 0.0.1-abc"}, @@ -1527,12 +1585,14 @@ func TestOperator_sync(t *testing.T) { Generation: 2, Reconciling: true, Completed: 1, - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, }, optr: Operator{ - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", upgradeable: &upgradeable{ Conditions: []configv1.ClusterOperatorStatusCondition{}, }, @@ -1581,7 +1641,7 @@ func TestOperator_sync(t *testing.T) { History: []configv1.UpdateHistory{ {State: configv1.CompletedUpdate, Version: "0.0.1-abc", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, }, - Desired: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Desired: configv1.Release{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, ObservedGeneration: 2, Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionTrue, Message: "Done applying 0.0.1-abc"}, @@ -1599,17 +1659,19 @@ func TestOperator_sync(t *testing.T) { Generation: 2, Reconciling: true, Completed: 1, - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, }, optr: Operator{ - releaseImage: "image/image:v4.0.1", + release: configv1.Release{ + Image: "image/image:v4.0.1", + }, namespace: "test", name: "default", defaultUpstreamServer: "http://localhost:8080/graph", availableUpdates: &availableUpdates{ Upstream: "", Channel: "fast", - Updates: []configv1.Update{ + Updates: []configv1.Release{ {Version: "4.0.2", Image: "test/image:1"}, {Version: "4.0.3", Image: "test/image:2"}, }, @@ -1657,14 +1719,14 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - AvailableUpdates: []configv1.Update{ + AvailableUpdates: []configv1.Release{ {Version: "4.0.2", Image: "test/image:1"}, {Version: "4.0.3", Image: "test/image:2"}, }, History: []configv1.UpdateHistory{ {State: configv1.CompletedUpdate, Version: "0.0.1-abc", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, }, - Desired: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Desired: configv1.Release{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, ObservedGeneration: 2, Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionTrue, Message: "Done applying 0.0.1-abc"}, @@ -1682,16 +1744,18 @@ func TestOperator_sync(t *testing.T) { Generation: 2, Reconciling: true, Completed: 1, - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, }, optr: Operator{ - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", availableUpdates: &availableUpdates{ Upstream: "http://localhost:8080/graph", Channel: "fast", - Updates: []configv1.Update{ + Updates: []configv1.Release{ {Version: "4.0.2", Image: "test/image:1"}, {Version: "4.0.3", Image: "test/image:2"}, }, @@ -1742,7 +1806,7 @@ func TestOperator_sync(t *testing.T) { History: []configv1.UpdateHistory{ {State: configv1.CompletedUpdate, Version: "0.0.1-abc", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, }, - Desired: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Desired: configv1.Release{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, ObservedGeneration: 2, Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionTrue, Message: "Done applying 0.0.1-abc"}, @@ -1760,12 +1824,14 @@ func TestOperator_sync(t *testing.T) { Generation: 2, Reconciling: true, Completed: 1, - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "4.0.1"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "4.0.1"}, }, optr: Operator{ - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fakeClientsetWithUpdates(&configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ Name: "default", @@ -1804,7 +1870,7 @@ func TestOperator_sync(t *testing.T) { History: []configv1.UpdateHistory{ {State: configv1.CompletedUpdate, Version: "4.0.1", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, }, - Desired: configv1.Update{Image: "image/image:v4.0.1", Version: "4.0.1"}, + Desired: configv1.Release{Image: "image/image:v4.0.1", Version: "4.0.1"}, ObservedGeneration: 2, Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionTrue, Message: "Done applying 4.0.1"}, @@ -1822,12 +1888,14 @@ func TestOperator_sync(t *testing.T) { Generation: 2, Reconciling: true, Completed: 1, - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "4.0.1"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "4.0.1"}, }, optr: Operator{ - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fakeClientsetWithUpdates(&configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ Name: "default", @@ -1841,7 +1909,7 @@ func TestOperator_sync(t *testing.T) { }, }, Status: configv1.ClusterVersionStatus{ - AvailableUpdates: []configv1.Update{ + AvailableUpdates: []configv1.Release{ {Version: "4.0.2", Image: "test/image:1"}, {Version: "4.0.3", Image: "test/image:2"}, }, @@ -1872,8 +1940,8 @@ func TestOperator_sync(t *testing.T) { History: []configv1.UpdateHistory{ {State: configv1.CompletedUpdate, Version: "4.0.1", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, }, - Desired: configv1.Update{Image: "image/image:v4.0.1", Version: "4.0.1"}, - AvailableUpdates: []configv1.Update{ + Desired: configv1.Release{Image: "image/image:v4.0.1", Version: "4.0.1"}, + AvailableUpdates: []configv1.Release{ {Version: "4.0.2", Image: "test/image:1"}, {Version: "4.0.3", Image: "test/image:2"}, }, @@ -1895,12 +1963,14 @@ func TestOperator_sync(t *testing.T) { Generation: 2, Reconciling: true, Completed: 1, - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "4.0.1"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "4.0.1"}, }, optr: Operator{ - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fakeClientsetWithUpdates(&configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ Name: "default", @@ -1914,7 +1984,7 @@ func TestOperator_sync(t *testing.T) { }, }, Status: configv1.ClusterVersionStatus{ - AvailableUpdates: []configv1.Update{ + AvailableUpdates: []configv1.Release{ {Version: "4.0.2", Image: "test/image:1"}, {Version: "4.0.3", Image: "test/image:2"}, {Version: "4.0.3", Image: "test/image:3"}, @@ -1947,8 +2017,8 @@ func TestOperator_sync(t *testing.T) { History: []configv1.UpdateHistory{ {State: configv1.CompletedUpdate, Version: "4.0.1", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, }, - Desired: configv1.Update{Image: "image/image:v4.0.1", Version: "4.0.1"}, - AvailableUpdates: []configv1.Update{ + Desired: configv1.Release{Image: "image/image:v4.0.1", Version: "4.0.1"}, + AvailableUpdates: []configv1.Release{ {Version: "4.0.2", Image: "test/image:1"}, {Version: "4.0.3", Image: "test/image:2"}, {Version: "4.0.3", Image: "test/image:3"}, @@ -1971,13 +2041,15 @@ func TestOperator_sync(t *testing.T) { Reconciling: true, Completed: 1, VersionHash: "y_Kc5IQiIyU=", - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, }, optr: Operator{ - releaseImage: "image/image:v4.0.1", - releaseVersion: "0.0.1-abc", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "0.0.1-abc", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fakeClientsetWithUpdates( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -1999,7 +2071,7 @@ func TestOperator_sync(t *testing.T) { CompletionTime: &defaultCompletionTime, }, }, - Desired: configv1.Update{ + Desired: configv1.Release{ Version: "0.0.1-abc", Image: "image/image:v4.0.1", }, @@ -2031,13 +2103,15 @@ func TestOperator_sync(t *testing.T) { Reconciling: true, Completed: 1, VersionHash: "y_Kc5IQiIyU=", - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, }, optr: Operator{ - releaseImage: "image/image:v4.0.1", - releaseVersion: "0.0.1-abc", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "0.0.1-abc", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -2059,7 +2133,7 @@ func TestOperator_sync(t *testing.T) { CompletionTime: &defaultCompletionTime, }, }, - Desired: configv1.Update{ + Desired: configv1.Release{ Version: "0.0.1-abc", Image: "image/image:v4.0.1", }, @@ -2101,7 +2175,7 @@ func TestOperator_sync(t *testing.T) { StartedTime: metav1.Time{time.Unix(0, 0)}, }, }, - Desired: configv1.Update{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, + Desired: configv1.Release{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, ObservedGeneration: 2, VersionHash: "y_Kc5IQiIyU=", Conditions: []configv1.ClusterOperatorStatusCondition{ @@ -2120,12 +2194,14 @@ func TestOperator_sync(t *testing.T) { syncStatus: &SyncWorkerStatus{ Reconciling: true, Completed: 1, - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, }, optr: Operator{ - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fakeClientsetWithUpdates(&configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ Name: "default", @@ -2160,7 +2236,7 @@ func TestOperator_sync(t *testing.T) { History: []configv1.UpdateHistory{ {State: configv1.CompletedUpdate, Version: "0.0.1-abc", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime, CompletionTime: &defaultCompletionTime}, }, - Desired: configv1.Update{ + Desired: configv1.Release{ Version: "0.0.1-abc", Image: "image/image:v4.0.1", }, VersionHash: "", @@ -2179,12 +2255,14 @@ func TestOperator_sync(t *testing.T) { { name: "invalid cluster version should not block initial sync", syncStatus: &SyncWorkerStatus{ - Actual: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Actual: configv1.Release{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, }, optr: Operator{ - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fakeClientsetWithUpdates(&configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ Name: "default", @@ -2199,7 +2277,7 @@ func TestOperator_sync(t *testing.T) { History: []configv1.UpdateHistory{ {Image: "image/image:v4.0.1", StartedTime: defaultStartedTime}, }, - Desired: configv1.Update{Image: "image/image:v4.0.1"}, + Desired: configv1.Release{Image: "image/image:v4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -2236,9 +2314,9 @@ func TestOperator_sync(t *testing.T) { }, Status: configv1.ClusterVersionStatus{ History: []configv1.UpdateHistory{ - {State: configv1.PartialUpdate, Image: "image/image:v4.0.1", Version: "0.0.1-abc", StartedTime: defaultStartedTime}, + {State: configv1.PartialUpdate, Version: "0.0.1-abc", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime}, }, - Desired: configv1.Update{Image: "image/image:v4.0.1", Version: "0.0.1-abc"}, + Desired: configv1.Release{Version: "0.0.1-abc", Image: "image/image:v4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -2308,11 +2386,13 @@ func TestOperator_availableUpdatesSync(t *testing.T) { { name: "when version is missing, do nothing (other loops should create it)", optr: Operator{ - releaseVersion: "4.0.1", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", - client: fake.NewSimpleClientset(), + release: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", + client: fake.NewSimpleClientset(), }, }, { @@ -2321,10 +2401,11 @@ func TestOperator_availableUpdatesSync(t *testing.T) { http.Error(w, "bad things", http.StatusInternalServerError) }, optr: Operator{ - releaseVersion: "", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -2360,10 +2441,12 @@ func TestOperator_availableUpdatesSync(t *testing.T) { }, optr: Operator{ defaultUpstreamServer: "http://localhost:8080/graph", - releaseVersion: "v4.0.0", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "v4.0.0", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -2399,10 +2482,11 @@ func TestOperator_availableUpdatesSync(t *testing.T) { }, optr: Operator{ defaultUpstreamServer: "http://localhost:8080/graph", - releaseVersion: "", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -2438,10 +2522,12 @@ func TestOperator_availableUpdatesSync(t *testing.T) { }, optr: Operator{ defaultUpstreamServer: "http://localhost:8080/graph", - releaseVersion: "4.0.1", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -2481,7 +2567,14 @@ func TestOperator_availableUpdatesSync(t *testing.T) { fmt.Fprintf(w, ` { "nodes": [ - {"version":"4.0.1", "payload": "image/image:v4.0.1"} + { + "version":"4.0.1", + "payload": "image/image:v4.0.1", + "metadata": { + "url": "https://example.com/v4.0.1", + "io.openshift.upgrades.graph.release.channels": "channel-c,channel-a,channel-b" + } + } ], "edges": [] } @@ -2489,10 +2582,12 @@ func TestOperator_availableUpdatesSync(t *testing.T) { }, optr: Operator{ defaultUpstreamServer: "http://localhost:8080/graph", - releaseVersion: "4.0.1", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -2519,6 +2614,12 @@ func TestOperator_availableUpdatesSync(t *testing.T) { wantUpdates: &availableUpdates{ Upstream: "", Channel: "fast", + Current: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + URL: "https://example.com/v4.0.1", + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, Condition: configv1.ClusterOperatorStatusCondition{ Type: configv1.RetrievedUpdates, Status: configv1.ConditionTrue, @@ -2545,10 +2646,12 @@ func TestOperator_availableUpdatesSync(t *testing.T) { }, optr: Operator{ defaultUpstreamServer: "http://localhost:8080/graph", - releaseVersion: "4.0.1", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -2575,7 +2678,8 @@ func TestOperator_availableUpdatesSync(t *testing.T) { wantUpdates: &availableUpdates{ Upstream: "", Channel: "fast", - Updates: []configv1.Update{ + Current: configv1.Release{Version: "4.0.1", Image: "image/image:v4.0.1"}, + Updates: []configv1.Release{ {Version: "4.0.2-prerelease", Image: "some.other.registry/image/image:v4.0.2"}, {Version: "4.0.2", Image: "image/image:v4.0.2"}, }, @@ -2598,11 +2702,12 @@ func TestOperator_availableUpdatesSync(t *testing.T) { Channel: "fast", LastAttempt: time.Now(), }, - - releaseVersion: "4.0.1", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -2698,20 +2803,23 @@ func TestOperator_upgradeableSync(t *testing.T) { { name: "when version is missing, do nothing (other loops should create it)", optr: Operator{ - releaseVersion: "4.0.1", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", - client: fake.NewSimpleClientset(), + release: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", + client: fake.NewSimpleClientset(), }, }, { name: "report error condition when overrides is set for version", optr: Operator{ - releaseVersion: "", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -2745,10 +2853,12 @@ func TestOperator_upgradeableSync(t *testing.T) { name: "report error condition when the single clusteroperator is not upgradeable", optr: Operator{ defaultUpstreamServer: "http://localhost:8080/graph", - releaseVersion: "v4.0.0", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "v4.0.0", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -2792,10 +2902,12 @@ func TestOperator_upgradeableSync(t *testing.T) { name: "report error condition when single clusteroperator is not upgradeable and another has no conditions", optr: Operator{ defaultUpstreamServer: "http://localhost:8080/graph", - releaseVersion: "v4.0.0", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "v4.0.0", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -2847,10 +2959,12 @@ func TestOperator_upgradeableSync(t *testing.T) { name: "report error condition when single clusteroperator is not upgradeable and another is upgradeable", optr: Operator{ defaultUpstreamServer: "http://localhost:8080/graph", - releaseVersion: "v4.0.0", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "v4.0.0", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -2905,10 +3019,12 @@ func TestOperator_upgradeableSync(t *testing.T) { name: "report error condition when two clusteroperators are not upgradeable", optr: Operator{ defaultUpstreamServer: "http://localhost:8080/graph", - releaseVersion: "v4.0.0", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "v4.0.0", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -2965,10 +3081,12 @@ func TestOperator_upgradeableSync(t *testing.T) { name: "report error condition when clusteroperators and version are not upgradeable", optr: Operator{ defaultUpstreamServer: "http://localhost:8080/graph", - releaseVersion: "v4.0.0", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "v4.0.0", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -3038,10 +3156,12 @@ func TestOperator_upgradeableSync(t *testing.T) { name: "no error conditions", optr: Operator{ defaultUpstreamServer: "http://localhost:8080/graph", - releaseVersion: "v4.0.0", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "v4.0.0", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -3066,10 +3186,12 @@ func TestOperator_upgradeableSync(t *testing.T) { name: "no error conditions", optr: Operator{ defaultUpstreamServer: "http://localhost:8080/graph", - releaseVersion: "v4.0.0", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "v4.0.0", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -3096,10 +3218,12 @@ func TestOperator_upgradeableSync(t *testing.T) { name: "no error conditions", optr: Operator{ defaultUpstreamServer: "http://localhost:8080/graph", - releaseVersion: "v4.0.0", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "v4.0.0", + Image: "image/image:v4.0.1", + }, + namespace: "test", + name: "default", client: fake.NewSimpleClientset( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -3459,3 +3583,120 @@ func Test_loadReleaseVerifierFromConfigMap(t *testing.T) { }) } } + +func TestOperator_mergeReleaseMetadata(t *testing.T) { + for _, testCase := range []struct { + name string + input configv1.Release + availableUpdates *availableUpdates + expected configv1.Release + }{ + { + name: "does not crash with empty inputs", + }, + { + name: "minimal release with no available updates", + input: configv1.Release{Image: "image/image:v1.0.0"}, + expected: configv1.Release{Image: "image/image:v1.0.0"}, + }, + { + name: "minimal release with empty available updates", + input: configv1.Release{Image: "image/image:v1.0.0"}, + availableUpdates: &availableUpdates{}, + expected: configv1.Release{Image: "image/image:v1.0.0"}, + }, + { + name: "minimal release with full, current available update", + input: configv1.Release{Image: "image/image:v1.0.0"}, + availableUpdates: &availableUpdates{ + Current: configv1.Release{ + Version: "1.0.1", + Image: "image/image:v1.0.0", + URL: configv1.URL("https://example.com/v1.0.1"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, + }, + expected: configv1.Release{ + Version: "1.0.1", + Image: "image/image:v1.0.0", + URL: configv1.URL("https://example.com/v1.0.1"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, + }, + { + name: "minimal release with full, next-hop available update", + input: configv1.Release{Image: "image/image:v1.0.0"}, + availableUpdates: &availableUpdates{ + Updates: []configv1.Release{ + { + Version: "1.0.1", + Image: "image/image:v1.0.0", + URL: configv1.URL("https://example.com/v1.0.1"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, + }, + }, + expected: configv1.Release{ + Version: "1.0.1", + Image: "image/image:v1.0.0", + URL: configv1.URL("https://example.com/v1.0.1"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, + }, + { + name: "minimal release with non-matching available updates", + input: configv1.Release{Image: "image/image:v1.0.0"}, + availableUpdates: &availableUpdates{ + Current: configv1.Release{ + Version: "1.0.1", + Image: "image/image:v1.0.1", + URL: configv1.URL("https://example.com/v1.0.1"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, + + Updates: []configv1.Release{ + { + Version: "1.0.2", + Image: "image/image:v1.0.2", + URL: configv1.URL("https://example.com/v1.0.2"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, + }, + }, + expected: configv1.Release{ + Image: "image/image:v1.0.0", + }, + }, + { + name: "fill release with full, current available update", + input: configv1.Release{ + Version: "1.0.0", + Image: "image/image:v1.0.0", + URL: configv1.URL("https://example.com/v1.0.0"), + Channels: []string{"channel-z"}, + }, + availableUpdates: &availableUpdates{ + Current: configv1.Release{ + Version: "1.0.1", + Image: "image/image:v1.0.0", + URL: configv1.URL("https://example.com/v1.0.1"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, + }, + expected: configv1.Release{ + Version: "1.0.0", + Image: "image/image:v1.0.0", + URL: configv1.URL("https://example.com/v1.0.0"), + Channels: []string{"channel-z"}, + }, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + optr := Operator{availableUpdates: testCase.availableUpdates} + actual := optr.mergeReleaseMetadata(testCase.input) + if !reflect.DeepEqual(actual, testCase.expected) { + t.Fatalf("unexpected: %s", diff.ObjectReflectDiff(testCase.expected, actual)) + } + }) + } +} diff --git a/pkg/cvo/metrics_test.go b/pkg/cvo/metrics_test.go index b7de2d573f..58174345ab 100644 --- a/pkg/cvo/metrics_test.go +++ b/pkg/cvo/metrics_test.go @@ -26,8 +26,10 @@ func Test_operatorMetrics_Collect(t *testing.T) { { name: "collects current version", optr: &Operator{ - releaseVersion: "0.0.2", - releaseImage: "test/image:1", + release: configv1.Release{ + Version: "0.0.2", + Image: "test/image:1", + }, releaseCreated: time.Unix(3, 0), }, wants: func(t *testing.T, metrics []prometheus.Metric) { @@ -41,8 +43,10 @@ func Test_operatorMetrics_Collect(t *testing.T) { { name: "collects current version with no age", optr: &Operator{ - releaseVersion: "0.0.2", - releaseImage: "test/image:1", + release: configv1.Release{ + Version: "0.0.2", + Image: "test/image:1", + }, }, wants: func(t *testing.T, metrics []prometheus.Metric) { if len(metrics) != 2 { @@ -55,9 +59,11 @@ func Test_operatorMetrics_Collect(t *testing.T) { { name: "collects completed history", optr: &Operator{ - name: "test", - releaseVersion: "0.0.2", - releaseImage: "test/image:1", + name: "test", + release: configv1.Release{ + Version: "0.0.2", + Image: "test/image:1", + }, releaseCreated: time.Unix(3, 0), cvLister: &cvLister{ Items: []*configv1.ClusterVersion{ @@ -91,9 +97,11 @@ func Test_operatorMetrics_Collect(t *testing.T) { { name: "collects completed history with prior completion", optr: &Operator{ - name: "test", - releaseVersion: "0.0.3", - releaseImage: "test/image:2", + name: "test", + release: configv1.Release{ + Version: "0.0.3", + Image: "test/image:2", + }, releaseCreated: time.Unix(3, 0), cvLister: &cvLister{ Items: []*configv1.ClusterVersion{ @@ -128,9 +136,11 @@ func Test_operatorMetrics_Collect(t *testing.T) { { name: "ignores partial history", optr: &Operator{ - name: "test", - releaseVersion: "0.0.2", - releaseImage: "test/image:1", + name: "test", + release: configv1.Release{ + Version: "0.0.2", + Image: "test/image:1", + }, releaseCreated: time.Unix(3, 0), cvLister: &cvLister{ Items: []*configv1.ClusterVersion{ @@ -237,7 +247,7 @@ func Test_operatorMetrics_Collect(t *testing.T) { CreationTimestamp: metav1.Time{Time: time.Unix(2, 0)}, }, Status: configv1.ClusterVersionStatus{ - AvailableUpdates: []configv1.Update{ + AvailableUpdates: []configv1.Release{ {Version: "1.0.1"}, {Version: "1.0.2"}, }, @@ -293,9 +303,11 @@ func Test_operatorMetrics_Collect(t *testing.T) { { name: "collects update", optr: &Operator{ - releaseVersion: "0.0.2", - releaseImage: "test/image:1", - name: "test", + release: configv1.Release{ + Version: "0.0.2", + Image: "test/image:1", + }, + name: "test", cvLister: &cvLister{ Items: []*configv1.ClusterVersion{ { @@ -330,8 +342,10 @@ func Test_operatorMetrics_Collect(t *testing.T) { { name: "collects failing update", optr: &Operator{ - releaseVersion: "0.0.2", - releaseImage: "test/image:1", + release: configv1.Release{ + Version: "0.0.2", + Image: "test/image:1", + }, name: "test", releaseCreated: time.Unix(6, 0), cvLister: &cvLister{ @@ -373,9 +387,11 @@ func Test_operatorMetrics_Collect(t *testing.T) { { name: "collects failing image", optr: &Operator{ - releaseVersion: "0.0.2", - releaseImage: "test/image:1", - name: "test", + release: configv1.Release{ + Version: "0.0.2", + Image: "test/image:1", + }, + name: "test", cvLister: &cvLister{ Items: []*configv1.ClusterVersion{ { @@ -407,8 +423,10 @@ func Test_operatorMetrics_Collect(t *testing.T) { { name: "collects legacy openshift-install info", optr: &Operator{ - releaseVersion: "0.0.2", - releaseImage: "test/image:1", + release: configv1.Release{ + Version: "0.0.2", + Image: "test/image:1", + }, releaseCreated: time.Unix(3, 0), cmConfigLister: &cmConfigLister{ Items: []*corev1.ConfigMap{{ @@ -428,8 +446,10 @@ func Test_operatorMetrics_Collect(t *testing.T) { { name: "collects openshift-install info", optr: &Operator{ - releaseVersion: "0.0.2", - releaseImage: "test/image:1", + release: configv1.Release{ + Version: "0.0.2", + Image: "test/image:1", + }, releaseCreated: time.Unix(3, 0), cmConfigLister: &cmConfigLister{ Items: []*corev1.ConfigMap{ @@ -455,8 +475,10 @@ func Test_operatorMetrics_Collect(t *testing.T) { { name: "collects openshift-install-manifests info", optr: &Operator{ - releaseVersion: "0.0.2", - releaseImage: "test/image:1", + release: configv1.Release{ + Version: "0.0.2", + Image: "test/image:1", + }, releaseCreated: time.Unix(3, 0), cmConfigLister: &cmConfigLister{ Items: []*corev1.ConfigMap{{ @@ -476,8 +498,10 @@ func Test_operatorMetrics_Collect(t *testing.T) { { name: "collects empty openshift-install info", optr: &Operator{ - releaseVersion: "0.0.2", - releaseImage: "test/image:1", + release: configv1.Release{ + Version: "0.0.2", + Image: "test/image:1", + }, releaseCreated: time.Unix(3, 0), cmConfigLister: &cmConfigLister{ Items: []*corev1.ConfigMap{{ @@ -496,8 +520,10 @@ func Test_operatorMetrics_Collect(t *testing.T) { { name: "skips openshift-install info on error", optr: &Operator{ - releaseVersion: "0.0.2", - releaseImage: "test/image:1", + release: configv1.Release{ + Version: "0.0.2", + Image: "test/image:1", + }, releaseCreated: time.Unix(3, 0), cmConfigLister: &cmConfigLister{ Err: errors.New("dial timeout"), diff --git a/pkg/cvo/status.go b/pkg/cvo/status.go index b2d8fe04b7..3e446e0ceb 100644 --- a/pkg/cvo/status.go +++ b/pkg/cvo/status.go @@ -29,7 +29,7 @@ const ( ClusterStatusFailing = configv1.ClusterStatusConditionType("Failing") ) -func mergeEqualVersions(current *configv1.UpdateHistory, desired configv1.Update) bool { +func mergeEqualVersions(current *configv1.UpdateHistory, desired configv1.Release) bool { if len(desired.Image) > 0 && desired.Image == current.Image { if len(desired.Version) == 0 { return true @@ -48,7 +48,7 @@ func mergeEqualVersions(current *configv1.UpdateHistory, desired configv1.Update return false } -func mergeOperatorHistory(config *configv1.ClusterVersion, desired configv1.Update, verified bool, now metav1.Time, completed bool) { +func mergeOperatorHistory(config *configv1.ClusterVersion, desired configv1.Release, verified bool, now metav1.Time, completed bool) { // if we have no image, we cannot reproduce the update later and so it cannot be part of the history if len(desired.Image) == 0 { // make the array empty @@ -180,8 +180,20 @@ func (optr *Operator) syncStatus(ctx context.Context, original, config *configv1 now := metav1.Now() version := versionString(status.Actual) - - mergeOperatorHistory(config, status.Actual, status.Verified, now, status.Completed > 0) + if status.Actual.Image == optr.release.Image { + // backfill any missing information from the operator (payload). + if status.Actual.Version == "" { + status.Actual.Version = optr.release.Version + } + if len(status.Actual.URL) == 0 { + status.Actual.URL = optr.release.URL + } + if status.Actual.Channels == nil { + status.Actual.Channels = append(optr.release.Channels[:0:0], optr.release.Channels...) // copy + } + } + desired := optr.mergeReleaseMetadata(status.Actual) + mergeOperatorHistory(config, desired, status.Verified, now, status.Completed > 0) // update validation errors var reason string diff --git a/pkg/cvo/status_test.go b/pkg/cvo/status_test.go index 4ff80f0d64..bea36a054c 100644 --- a/pkg/cvo/status_test.go +++ b/pkg/cvo/status_test.go @@ -20,37 +20,37 @@ func Test_mergeEqualVersions(t *testing.T) { tests := []struct { name string current *configv1.UpdateHistory - desired configv1.Update + desired configv1.Release want bool }{ { current: &configv1.UpdateHistory{Image: "test:1", Version: "0.0.1"}, - desired: configv1.Update{Image: "test:1", Version: "0.0.1"}, + desired: configv1.Release{Image: "test:1", Version: "0.0.1"}, want: true, }, { - current: &configv1.UpdateHistory{Image: "test:1", Version: ""}, - desired: configv1.Update{Image: "test:1", Version: "0.0.1"}, + current: &configv1.UpdateHistory{Image: "test:1"}, + desired: configv1.Release{Image: "test:1", Version: "0.0.1"}, want: true, }, { current: &configv1.UpdateHistory{Image: "test:1", Version: "0.0.1"}, - desired: configv1.Update{Image: "test:1", Version: ""}, + desired: configv1.Release{Image: "test:1"}, want: true, }, { current: &configv1.UpdateHistory{Image: "test:1", Version: "0.0.1"}, - desired: configv1.Update{Image: "", Version: "0.0.1"}, + desired: configv1.Release{Version: "0.0.1"}, want: false, }, { current: &configv1.UpdateHistory{Image: "test:1", Version: "0.0.1"}, - desired: configv1.Update{Image: "test:2", Version: "0.0.1"}, + desired: configv1.Release{Image: "test:2", Version: "0.0.1"}, want: false, }, { current: &configv1.UpdateHistory{Image: "test:1", Version: "0.0.1"}, - desired: configv1.Update{Image: "test:1", Version: "0.0.2"}, + desired: configv1.Release{Image: "test:1", Version: "0.0.2"}, want: false, }, } @@ -152,7 +152,7 @@ func TestOperator_syncFailingStatus(t *testing.T) { init func(optr *Operator) wantErr func(*testing.T, error) wantActions func(*testing.T, *Operator) - wantSync []configv1.Update + wantSync []configv1.Release original *configv1.ClusterVersion ierr error @@ -160,10 +160,13 @@ func TestOperator_syncFailingStatus(t *testing.T) { { ierr: fmt.Errorf("bad"), optr: Operator{ - releaseVersion: "4.0.1", - releaseImage: "image/image:v4.0.1", - namespace: "test", - name: "default", + release: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + URL: configv1.URL("https://example.com/v4.0.1"), + }, + namespace: "test", + name: "default", client: fakeClientsetWithUpdates( &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ @@ -176,7 +179,11 @@ func TestOperator_syncFailingStatus(t *testing.T) { History: []configv1.UpdateHistory{ {Version: "4.0.1", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime}, }, - Desired: configv1.Update{Version: "4.0.1", Image: "image/image:v4.0.1"}, + Desired: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + URL: configv1.URL("https://example.com/v4.0.1"), + }, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -212,7 +219,11 @@ func TestOperator_syncFailingStatus(t *testing.T) { History: []configv1.UpdateHistory{ {State: configv1.PartialUpdate, Version: "4.0.1", Image: "image/image:v4.0.1", StartedTime: defaultStartedTime}, }, - Desired: configv1.Update{Version: "4.0.1", Image: "image/image:v4.0.1"}, + Desired: configv1.Release{ + Version: "4.0.1", + Image: "image/image:v4.0.1", + URL: configv1.URL("https://example.com/v4.0.1"), + }, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, diff --git a/pkg/cvo/sync_test.go b/pkg/cvo/sync_test.go index f64bb400bc..13865a7b52 100644 --- a/pkg/cvo/sync_test.go +++ b/pkg/cvo/sync_test.go @@ -120,7 +120,13 @@ func Test_SyncWorker_apply(t *testing.T) { manifests = append(manifests, m) } - up := &payload.Update{ReleaseImage: "test", ReleaseVersion: "v0.0.0", Manifests: manifests} + up := &payload.Update{ + Release: configv1.Release{ + Version: "v0.0.0", + Image: "test", + }, + Manifests: manifests, + } r := &recorder{} testMapper := resourcebuilder.NewResourceMapper() testMapper.RegisterGVK(schema.GroupVersionKind{"test.cvo.io", "v1", "TestA"}, newTestBuilder(r, test.reactors)) @@ -312,7 +318,13 @@ func Test_SyncWorker_apply_generic(t *testing.T) { dynamicScheme.AddKnownTypeWithName(schema.GroupVersionKind{Group: "test.cvo.io", Version: "v1", Kind: "TestB"}, &unstructured.Unstructured{}) dynamicClient := dynamicfake.NewSimpleDynamicClient(dynamicScheme) - up := &payload.Update{ReleaseImage: "test", ReleaseVersion: "v0.0.0", Manifests: manifests} + up := &payload.Update{ + Release: configv1.Release{ + Version: "v0.0.0", + Image: "test", + }, + Manifests: manifests, + } worker := &SyncWorker{eventRecorder: record.NewFakeRecorder(100)} worker.backoff.Steps = 1 worker.builder = &testResourceBuilder{ diff --git a/pkg/cvo/sync_worker.go b/pkg/cvo/sync_worker.go index 4a60a3e5b2..dcd676a907 100644 --- a/pkg/cvo/sync_worker.go +++ b/pkg/cvo/sync_worker.go @@ -98,7 +98,7 @@ type SyncWorkerStatus struct { LastProgress time.Time - Actual configv1.Update + Actual configv1.Release Verified bool } @@ -224,7 +224,10 @@ func (w *SyncWorker) Update(generation int64, desired configv1.Update, overrides w.status = SyncWorkerStatus{ Generation: generation, Reconciling: state.Reconciling(), - Actual: work.Desired, + Actual: configv1.Release{ + Version: work.Desired.Version, + Image: work.Desired.Image, + }, } } @@ -359,14 +362,14 @@ type statusWrapper struct { func (w *statusWrapper) Report(status SyncWorkerStatus) { p := w.previousStatus if p.Failure != nil && status.Failure == nil { - if p.Actual == status.Actual { + if p.Actual.Image == status.Actual.Image { if status.Fraction < p.Fraction { klog.V(5).Infof("Dropping status report from earlier in sync loop") return } } } - if status.Fraction > p.Fraction || status.Completed > p.Completed || (status.Failure == nil && status.Actual != p.Actual) { + if status.Fraction > p.Fraction || status.Completed > p.Completed || (status.Failure == nil && status.Actual.Image != p.Actual.Image) { status.LastProgress = time.Now() } if status.Generation == 0 { @@ -475,54 +478,81 @@ func (w *SyncWorker) Status() *SyncWorkerStatus { // the update could not be completely applied. The status is updated as we progress. // Cancelling the context will abort the execution of the sync. func (w *SyncWorker) syncOnce(ctx context.Context, work *SyncWork, maxWorkers int, reporter StatusReporter, clusterVersion *configv1.ClusterVersion) error { - klog.V(4).Infof("Running sync %s (force=%t) on generation %d in state %s at attempt %d", versionString(work.Desired), work.Desired.Force, work.Generation, work.State, work.Attempt) - update := work.Desired + desired := configv1.Release{ + Version: work.Desired.Version, + Image: work.Desired.Image, + } + klog.V(4).Infof("Running sync %s (force=%t) on generation %d in state %s at attempt %d", versionString(desired), work.Desired.Force, work.Generation, work.State, work.Attempt) // cache the payload until the release image changes validPayload := w.payload - if validPayload == nil || !equalUpdate(configv1.Update{Image: validPayload.ReleaseImage}, configv1.Update{Image: update.Image}) { + if validPayload != nil && validPayload.Release.Image == desired.Image { + // possibly complain here if Version, etc. diverges from the payload content + desired = validPayload.Release + } else if validPayload == nil || !equalUpdate(configv1.Update{Image: validPayload.Release.Image}, configv1.Update{Image: desired.Image}) { klog.V(4).Infof("Loading payload") cvoObjectRef := &corev1.ObjectReference{APIVersion: "config.openshift.io/v1", Kind: "ClusterVersion", Name: "version", Namespace: "openshift-cluster-version"} - w.eventRecorder.Eventf(cvoObjectRef, corev1.EventTypeNormal, "RetrievePayload", "retrieving payload version=%q image=%q", update.Version, update.Image) + w.eventRecorder.Eventf(cvoObjectRef, corev1.EventTypeNormal, "RetrievePayload", "retrieving payload version=%q image=%q", desired.Version, desired.Image) reporter.Report(SyncWorkerStatus{ Generation: work.Generation, Step: "RetrievePayload", Initial: work.State.Initializing(), Reconciling: work.State.Reconciling(), - Actual: update, + Actual: desired, }) - info, err := w.retriever.RetrievePayload(ctx, update) + info, err := w.retriever.RetrievePayload(ctx, work.Desired) if err != nil { - w.eventRecorder.Eventf(cvoObjectRef, corev1.EventTypeWarning, "RetrievePayloadFailed", "retrieving payload failed version=%q image=%q failure=%v", update.Version, update.Image, err) + w.eventRecorder.Eventf(cvoObjectRef, corev1.EventTypeWarning, "RetrievePayloadFailed", "retrieving payload failed version=%q image=%q failure=%v", desired.Version, desired.Image, err) reporter.Report(SyncWorkerStatus{ Generation: work.Generation, Failure: err, Step: "RetrievePayload", Initial: work.State.Initializing(), Reconciling: work.State.Reconciling(), - Actual: update, + Actual: desired, }) return err } - w.eventRecorder.Eventf(cvoObjectRef, corev1.EventTypeNormal, "VerifyPayload", "verifying payload version=%q image=%q", update.Version, update.Image) - payloadUpdate, err := payload.LoadUpdate(info.Directory, update.Image, w.exclude) + w.eventRecorder.Eventf(cvoObjectRef, corev1.EventTypeNormal, "VerifyPayload", "verifying payload version=%q image=%q", desired.Version, desired.Image) + payloadUpdate, err := payload.LoadUpdate(info.Directory, desired.Image, w.exclude) if err != nil { - w.eventRecorder.Eventf(cvoObjectRef, corev1.EventTypeWarning, "VerifyPayloadFailed", "verifying payload failed version=%q image=%q failure=%v", update.Version, update.Image, err) + w.eventRecorder.Eventf(cvoObjectRef, corev1.EventTypeWarning, "VerifyPayloadFailed", "verifying payload failed version=%q image=%q failure=%v", desired.Version, desired.Image, err) reporter.Report(SyncWorkerStatus{ Generation: work.Generation, Failure: err, Step: "VerifyPayload", Initial: work.State.Initializing(), Reconciling: work.State.Reconciling(), - Actual: update, + Actual: desired, Verified: info.Verified, }) return err } + payloadUpdate.VerifiedImage = info.Verified payloadUpdate.LoadedAt = time.Now() + if work.Desired.Version == "" { + work.Desired.Version = payloadUpdate.Release.Version + desired.Version = payloadUpdate.Release.Version + } else if payloadUpdate.Release.Version != work.Desired.Version { + err = fmt.Errorf("release image version %s does not match the expected upstream version %s", payloadUpdate.Release.Version, work.Desired.Version) + w.eventRecorder.Eventf(cvoObjectRef, corev1.EventTypeWarning, "VerifyPayloadVersionFailed", "verifying payload failed version=%q image=%q failure=%v", work.Desired.Version, work.Desired.Image, err) + /* FIXME: Ignore for now. I will make this fatal in a follow-up pivot + reporter.Report(SyncWorkerStatus{ + Generation: work.Generation, + Failure: err, + Step: "VerifyPayloadVersion", + Initial: work.State.Initializing(), + Reconciling: work.State.Reconciling(), + Actual: desired, + Verified: info.Verified, + }) + return err + */ + } + // need to make sure the payload is only set when the preconditions have been successful if len(w.preconditions) == 0 { klog.V(4).Info("No preconditions configured.") @@ -534,33 +564,33 @@ func (w *SyncWorker) syncOnce(ctx context.Context, work *SyncWork, maxWorkers in Step: "PreconditionChecks", Initial: work.State.Initializing(), Reconciling: work.State.Reconciling(), - Actual: update, + Actual: desired, Verified: info.Verified, }) - if err := precondition.Summarize(w.preconditions.RunAll(ctx, precondition.ReleaseContext{DesiredVersion: payloadUpdate.ReleaseVersion}, clusterVersion)); err != nil { - if update.Force { + if err := precondition.Summarize(w.preconditions.RunAll(ctx, precondition.ReleaseContext{DesiredVersion: payloadUpdate.Release.Version}, clusterVersion)); err != nil { + if work.Desired.Force { klog.V(4).Infof("Forcing past precondition failures: %s", err) - w.eventRecorder.Eventf(cvoObjectRef, corev1.EventTypeWarning, "PreconditionsForced", "preconditions forced for payload loaded version=%q image=%q failures=%v", update.Version, update.Image, err) + w.eventRecorder.Eventf(cvoObjectRef, corev1.EventTypeWarning, "PreconditionsForced", "preconditions forced for payload loaded version=%q image=%q failures=%v", desired.Version, desired.Image, err) } else { - w.eventRecorder.Eventf(cvoObjectRef, corev1.EventTypeWarning, "PreconditionsFailed", "preconditions failed for payload loaded version=%q image=%q failures=%v", update.Version, update.Image, err) + w.eventRecorder.Eventf(cvoObjectRef, corev1.EventTypeWarning, "PreconditionsFailed", "preconditions failed for payload loaded version=%q image=%q failures=%v", desired.Version, desired.Image, err) reporter.Report(SyncWorkerStatus{ Generation: work.Generation, Failure: err, Step: "PreconditionChecks", Initial: work.State.Initializing(), Reconciling: work.State.Reconciling(), - Actual: update, + Actual: desired, Verified: info.Verified, }) return err } } - w.eventRecorder.Eventf(cvoObjectRef, corev1.EventTypeNormal, "PreconditionsPassed", "preconditions passed for payload loaded version=%q image=%q", update.Version, update.Image) + w.eventRecorder.Eventf(cvoObjectRef, corev1.EventTypeNormal, "PreconditionsPassed", "preconditions passed for payload loaded version=%q image=%q", desired.Version, desired.Image) } w.payload = payloadUpdate - w.eventRecorder.Eventf(cvoObjectRef, corev1.EventTypeNormal, "PayloadLoaded", "payload loaded version=%q image=%q", update.Version, update.Image) - klog.V(4).Infof("Payload loaded from %s with hash %s", payloadUpdate.ReleaseImage, payloadUpdate.ManifestHash) + w.eventRecorder.Eventf(cvoObjectRef, corev1.EventTypeNormal, "PayloadLoaded", "payload loaded version=%q image=%q", desired.Version, desired.Image) + klog.V(4).Infof("Payload loaded from %s with hash %s", desired.Image, payloadUpdate.ManifestHash) } return w.apply(ctx, w.payload, work, maxWorkers, reporter) @@ -570,14 +600,7 @@ func (w *SyncWorker) syncOnce(ctx context.Context, work *SyncWork, maxWorkers in // Cancelling the context will abort the execution of the sync. Will be executed in parallel if // maxWorkers is set greater than 1. func (w *SyncWorker) apply(ctx context.Context, payloadUpdate *payload.Update, work *SyncWork, maxWorkers int, reporter StatusReporter) error { - update := configv1.Update{ - Version: payloadUpdate.ReleaseVersion, - Image: payloadUpdate.ReleaseImage, - Force: work.Desired.Force, - } - // encapsulate status reporting in a threadsafe updater - version := payloadUpdate.ReleaseVersion total := len(payloadUpdate.Manifests) cr := &consistentReporter{ status: SyncWorkerStatus{ @@ -585,11 +608,11 @@ func (w *SyncWorker) apply(ctx context.Context, payloadUpdate *payload.Update, w Initial: work.State.Initializing(), Reconciling: work.State.Reconciling(), VersionHash: payloadUpdate.ManifestHash, - Actual: update, + Actual: payloadUpdate.Release, Verified: payloadUpdate.VerifiedImage, }, completed: work.Completed, - version: version, + version: payloadUpdate.Release.Version, total: total, reporter: reporter, } @@ -675,7 +698,7 @@ func (w *SyncWorker) apply(ctx context.Context, payloadUpdate *payload.Update, w continue } - if err := task.Run(ctx, version, w.builder, work.State); err != nil { + if err := task.Run(ctx, payloadUpdate.Release.Version, w.builder, work.State); err != nil { return err } cr.Inc() @@ -1000,7 +1023,7 @@ func runThrottledStatusNotifier(ctx context.Context, interval time.Duration, buc return case next := <-ch: // only throttle if we aren't on an edge - if next.Generation == last.Generation && next.Actual == last.Actual && next.Reconciling == last.Reconciling && (next.Failure != nil) == (last.Failure != nil) { + if next.Generation == last.Generation && next.Actual.Image == last.Actual.Image && next.Reconciling == last.Reconciling && (next.Failure != nil) == (last.Failure != nil) { if err := throttle.Wait(ctx); err != nil && err != context.Canceled && err != context.DeadlineExceeded { utilruntime.HandleError(fmt.Errorf("unable to throttle status notification: %v", err)) } diff --git a/pkg/cvo/sync_worker_test.go b/pkg/cvo/sync_worker_test.go index 54285a9d97..b864ea4427 100644 --- a/pkg/cvo/sync_worker_test.go +++ b/pkg/cvo/sync_worker_test.go @@ -3,6 +3,7 @@ package cvo import ( "context" "fmt" + "reflect" "testing" "time" @@ -21,56 +22,56 @@ func Test_statusWrapper_ReportProgress(t *testing.T) { }{ { name: "skip updates that clear an error and are at an earlier fraction", - previous: SyncWorkerStatus{Failure: fmt.Errorf("a"), Actual: configv1.Update{Image: "testing"}, Fraction: 0.1}, - next: SyncWorkerStatus{Actual: configv1.Update{Image: "testing"}}, + previous: SyncWorkerStatus{Failure: fmt.Errorf("a"), Actual: configv1.Release{Image: "testing"}, Fraction: 0.1}, + next: SyncWorkerStatus{Actual: configv1.Release{Image: "testing"}}, want: false, }, { - previous: SyncWorkerStatus{Failure: fmt.Errorf("a"), Actual: configv1.Update{Image: "testing"}, Fraction: 0.1}, - next: SyncWorkerStatus{Actual: configv1.Update{Image: "testing2"}}, + previous: SyncWorkerStatus{Failure: fmt.Errorf("a"), Actual: configv1.Release{Image: "testing"}, Fraction: 0.1}, + next: SyncWorkerStatus{Actual: configv1.Release{Image: "testing2"}}, want: true, wantProgress: true, }, { - previous: SyncWorkerStatus{Failure: fmt.Errorf("a"), Actual: configv1.Update{Image: "testing"}}, - next: SyncWorkerStatus{Actual: configv1.Update{Image: "testing"}}, + previous: SyncWorkerStatus{Failure: fmt.Errorf("a"), Actual: configv1.Release{Image: "testing"}}, + next: SyncWorkerStatus{Actual: configv1.Release{Image: "testing"}}, want: true, }, { - previous: SyncWorkerStatus{Failure: fmt.Errorf("a"), Actual: configv1.Update{Image: "testing"}, Fraction: 0.1}, - next: SyncWorkerStatus{Failure: fmt.Errorf("a"), Actual: configv1.Update{Image: "testing"}}, + previous: SyncWorkerStatus{Failure: fmt.Errorf("a"), Actual: configv1.Release{Image: "testing"}, Fraction: 0.1}, + next: SyncWorkerStatus{Failure: fmt.Errorf("a"), Actual: configv1.Release{Image: "testing"}}, want: true, }, { - previous: SyncWorkerStatus{Failure: fmt.Errorf("a"), Actual: configv1.Update{Image: "testing"}, Fraction: 0.1}, - next: SyncWorkerStatus{Failure: fmt.Errorf("b"), Actual: configv1.Update{Image: "testing"}, Fraction: 0.1}, + previous: SyncWorkerStatus{Failure: fmt.Errorf("a"), Actual: configv1.Release{Image: "testing"}, Fraction: 0.1}, + next: SyncWorkerStatus{Failure: fmt.Errorf("b"), Actual: configv1.Release{Image: "testing"}, Fraction: 0.1}, want: true, }, { - previous: SyncWorkerStatus{Failure: fmt.Errorf("a"), Actual: configv1.Update{Image: "testing"}, Fraction: 0.1}, - next: SyncWorkerStatus{Failure: fmt.Errorf("b"), Actual: configv1.Update{Image: "testing"}, Fraction: 0.2}, + previous: SyncWorkerStatus{Failure: fmt.Errorf("a"), Actual: configv1.Release{Image: "testing"}, Fraction: 0.1}, + next: SyncWorkerStatus{Failure: fmt.Errorf("b"), Actual: configv1.Release{Image: "testing"}, Fraction: 0.2}, want: true, wantProgress: true, }, { - previous: SyncWorkerStatus{Actual: configv1.Update{Image: "testing"}, Completed: 1}, - next: SyncWorkerStatus{Actual: configv1.Update{Image: "testing"}, Completed: 2}, + previous: SyncWorkerStatus{Actual: configv1.Release{Image: "testing"}, Completed: 1}, + next: SyncWorkerStatus{Actual: configv1.Release{Image: "testing"}, Completed: 2}, want: true, wantProgress: true, }, { - previous: SyncWorkerStatus{Actual: configv1.Update{Image: "testing-1"}, Completed: 1}, - next: SyncWorkerStatus{Actual: configv1.Update{Image: "testing-2"}, Completed: 1}, + previous: SyncWorkerStatus{Actual: configv1.Release{Image: "testing-1"}, Completed: 1}, + next: SyncWorkerStatus{Actual: configv1.Release{Image: "testing-2"}, Completed: 1}, want: true, wantProgress: true, }, { - previous: SyncWorkerStatus{Actual: configv1.Update{Image: "testing"}}, - next: SyncWorkerStatus{Actual: configv1.Update{Image: "testing"}}, + previous: SyncWorkerStatus{Actual: configv1.Release{Image: "testing"}}, + next: SyncWorkerStatus{Actual: configv1.Release{Image: "testing"}}, want: true, }, { - next: SyncWorkerStatus{Actual: configv1.Update{Image: "testing"}}, + next: SyncWorkerStatus{Actual: configv1.Release{Image: "testing"}}, want: true, wantProgress: true, }, @@ -93,7 +94,7 @@ func Test_statusWrapper_ReportProgress(t *testing.T) { t.Errorf("unexpected progress timestamp: %#v", evt) } evt.LastProgress = time.Time{} - if evt != tt.next { + if !reflect.DeepEqual(evt, tt.next) { t.Fatalf("unexpected: %#v", evt) } } @@ -153,35 +154,35 @@ func Test_runThrottledStatusNotifier(t *testing.T) { ctx := context.Background() go runThrottledStatusNotifier(ctx, 30*time.Second, 1, in, func() { out <- struct{}{} }) - in <- SyncWorkerStatus{Actual: configv1.Update{Image: "test"}} + in <- SyncWorkerStatus{Actual: configv1.Release{Image: "test"}} select { case <-out: case <-time.After(100 * time.Millisecond): t.Fatalf("should have not throttled") } - in <- SyncWorkerStatus{Reconciling: true, Actual: configv1.Update{Image: "test"}} + in <- SyncWorkerStatus{Reconciling: true, Actual: configv1.Release{Image: "test"}} select { case <-out: case <-time.After(100 * time.Millisecond): t.Fatalf("should have not throttled") } - in <- SyncWorkerStatus{Failure: fmt.Errorf("a"), Reconciling: true, Actual: configv1.Update{Image: "test"}} + in <- SyncWorkerStatus{Failure: fmt.Errorf("a"), Reconciling: true, Actual: configv1.Release{Image: "test"}} select { case <-out: case <-time.After(100 * time.Millisecond): t.Fatalf("should have not throttled") } - in <- SyncWorkerStatus{Failure: fmt.Errorf("a"), Reconciling: true, Actual: configv1.Update{Image: "test"}} + in <- SyncWorkerStatus{Failure: fmt.Errorf("a"), Reconciling: true, Actual: configv1.Release{Image: "test"}} select { case <-out: case <-time.After(100 * time.Millisecond): t.Fatalf("should have not throttled") } - in <- SyncWorkerStatus{Failure: fmt.Errorf("a"), Reconciling: true, Actual: configv1.Update{Image: "test"}} + in <- SyncWorkerStatus{Failure: fmt.Errorf("a"), Reconciling: true, Actual: configv1.Release{Image: "test"}} select { case <-out: t.Fatalf("should have throttled") diff --git a/pkg/cvo/testdata/paralleltest/release-manifests/release-metadata b/pkg/cvo/testdata/paralleltest/release-manifests/release-metadata index e69de29bb2..8521b15aba 100644 --- a/pkg/cvo/testdata/paralleltest/release-manifests/release-metadata +++ b/pkg/cvo/testdata/paralleltest/release-manifests/release-metadata @@ -0,0 +1,4 @@ +{ + "kind": "cincinnati-metadata-v0", + "version": "1.0.0-abc" +} diff --git a/pkg/cvo/testdata/payloadtest-2/release-manifests/release-metadata b/pkg/cvo/testdata/payloadtest-2/release-manifests/release-metadata index e69de29bb2..5ea6a73ceb 100644 --- a/pkg/cvo/testdata/payloadtest-2/release-manifests/release-metadata +++ b/pkg/cvo/testdata/payloadtest-2/release-manifests/release-metadata @@ -0,0 +1,10 @@ +{ + "kind": "cincinnati-metadata-v0", + "version": "1.0.1-abc", + "previous": [ + "1.0.0-abc" + ], + "metadata": { + "url": "https://example.com/v1.0.1-abc" + } +} diff --git a/pkg/cvo/testdata/payloadtest/release-manifests/release-metadata b/pkg/cvo/testdata/payloadtest/release-manifests/release-metadata index e69de29bb2..ffb9068c12 100644 --- a/pkg/cvo/testdata/payloadtest/release-manifests/release-metadata +++ b/pkg/cvo/testdata/payloadtest/release-manifests/release-metadata @@ -0,0 +1,12 @@ +{ + "kind": "cincinnati-metadata-v0", + "version": "1.0.0-abc", + "previous": [ + "1.1.1", + "1.2.2" + ], + "metadata": { + "url": "https://example.com/v1.0.0-abc", + "io.openshift.upgrades.graph.release.channels": "channel-c,channel-a,channel-b" + } +} diff --git a/pkg/cvo/updatepayload.go b/pkg/cvo/updatepayload.go index 1f2e0a326f..a3b3b3972b 100644 --- a/pkg/cvo/updatepayload.go +++ b/pkg/cvo/updatepayload.go @@ -39,7 +39,7 @@ func (optr *Operator) defaultPayloadRetriever() PayloadRetriever { return &payloadRetriever{ kubeClient: optr.kubeClient, operatorName: optr.name, - releaseImage: optr.releaseImage, + releaseImage: optr.release.Image, namespace: optr.namespace, nodeName: optr.nodename, payloadDir: optr.defaultPayloadDir(), @@ -304,13 +304,21 @@ func findUpdateFromConfig(config *configv1.ClusterVersion) (configv1.Update, boo func findUpdateFromConfigVersion(config *configv1.ClusterVersion, version string, force bool) (configv1.Update, bool) { for _, update := range config.Status.AvailableUpdates { - if update.Version == version { - return update, len(update.Image) > 0 + if update.Version == version && len(update.Image) > 0 { + return configv1.Update{ + Version: version, + Image: update.Image, + Force: force, + }, true } } for _, history := range config.Status.History { - if history.Version == version { - return configv1.Update{Image: history.Image, Version: history.Version, Force: force}, len(history.Image) > 0 + if history.Version == version && len(history.Image) > 0 { + return configv1.Update{ + Version: version, + Image: history.Image, + Force: force, + }, true } } return configv1.Update{}, false diff --git a/pkg/payload/payload.go b/pkg/payload/payload.go index 32b9a19dbc..62783d4dc1 100644 --- a/pkg/payload/payload.go +++ b/pkg/payload/payload.go @@ -3,19 +3,24 @@ package payload import ( "bytes" "encoding/base64" + "encoding/json" + "errors" "fmt" "hash/fnv" "io/ioutil" "os" "path/filepath" + "sort" + "strings" "time" - "github.com/pkg/errors" "k8s.io/klog" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" + "github.com/blang/semver/v4" + configv1 "github.com/openshift/api/config/v1" imagev1 "github.com/openshift/api/image/v1" "github.com/openshift/cluster-version-operator/lib" @@ -92,10 +97,9 @@ const ( imageReferencesFile = "image-references" ) +// Update represents the contents of a release image. type Update struct { - ReleaseImage string - ReleaseVersion string - // XXX: cincinatti.json struct + Release configv1.Release VerifiedImage bool LoadedAt time.Time @@ -107,6 +111,25 @@ type Update struct { Manifests []lib.Manifest } +// metadata represents Cincinnati metadata. +// https://github.com/openshift/cincinnati/blob/a8abb826ef00cf91fd0f8a84912d4e0c23b1335d/docs/design/cincinnati.md#update-graph +type metadata struct { + // Kind is the document type. Must be cincinnati-metadata-v0. + Kind string `json:"kind"` + + // Version is the version of the release. + Version string `json:"version"` + + // Previous is a slice of valid previous versions. + Previous []string `json:"previous,omitempty"` + + // Next is a slice of valid next versions. + Next []string `json:"next,omitempty"` + + // Metadata is an opaque object that allows a release to convey arbitrary information to its consumers. + Metadata map[string]interface{} +} + func LoadUpdate(dir, releaseImage, excludeIdentifier string) (*Update, error) { payload, tasks, err := loadUpdatePayloadMetadata(dir, releaseImage) if err != nil { @@ -139,19 +162,19 @@ func LoadUpdate(dir, releaseImage, excludeIdentifier string) (*Update, error) { raw, err := ioutil.ReadFile(p) if err != nil { - errs = append(errs, errors.Wrapf(err, "error reading file %s", file.Name())) + errs = append(errs, err) continue } if task.preprocess != nil { raw, err = task.preprocess(raw) if err != nil { - errs = append(errs, errors.Wrapf(err, "error running preprocess on %s", file.Name())) + errs = append(errs, fmt.Errorf("preprocess %s: %w", file.Name(), err)) continue } } ms, err := lib.ParseManifests(bytes.NewReader(raw)) if err != nil { - errs = append(errs, errors.Wrapf(err, "error parsing %s", file.Name())) + errs = append(errs, fmt.Errorf("parse %s: %w", file.Name(), err)) continue } // Filter out manifests that should be excluded based on annotation @@ -198,25 +221,18 @@ func shouldExclude(excludeIdentifier string, manifest *lib.Manifest) bool { // looking for known files. It returns an error if the directory cannot // be an update. func ValidateDirectory(dir string) error { - // XXX: validate that cincinnati.json is correct - // validate image-references files is correct. - - // make sure cvo and release manifests dirs exist. - _, err := os.Stat(filepath.Join(dir, CVOManifestDir)) - if err != nil { - return err - } - releaseDir := filepath.Join(dir, ReleaseManifestDir) - _, err = os.Stat(releaseDir) - if err != nil { - return err + for _, dirname := range []string{CVOManifestDir, ReleaseManifestDir} { + if _, err := os.Stat(filepath.Join(dir, dirname)); err != nil { + return err + } } - // make sure image-references file exists in releaseDir - _, err = os.Stat(filepath.Join(releaseDir, imageReferencesFile)) - if err != nil { - return err + for _, filename := range []string{cincinnatiJSONFile, imageReferencesFile} { + if _, err := os.Stat(filepath.Join(dir, ReleaseManifestDir, filename)); err != nil { + return err + } } + return nil } @@ -236,14 +252,27 @@ func loadUpdatePayloadMetadata(dir, releaseImage string) (*Update, []payloadTask releaseDir = filepath.Join(dir, ReleaseManifestDir) ) + release, err := loadReleaseFromMetadata(releaseDir) + if err != nil { + return nil, nil, err + } + release.Image = releaseImage + imageRef, err := loadImageReferences(releaseDir) if err != nil { return nil, nil, err } + if imageRef.Name != release.Version { + return nil, nil, fmt.Errorf("Version from %s (%s) differs from %s (%s)", imageReferencesFile, imageRef.Name, cincinnatiJSONFile, release.Version) + } + tasks := getPayloadTasks(releaseDir, cvoDir, releaseImage) - return &Update{ImageRef: imageRef, ReleaseImage: releaseImage, ReleaseVersion: imageRef.Name}, tasks, nil + return &Update{ + Release: release, + ImageRef: imageRef, + }, tasks, nil } func getPayloadTasks(releaseDir, cvoDir, releaseImage string) []payloadTasks { @@ -263,6 +292,52 @@ func getPayloadTasks(releaseDir, cvoDir, releaseImage string) []payloadTasks { }} } +func loadReleaseFromMetadata(releaseDir string) (configv1.Release, error) { + var release configv1.Release + path := filepath.Join(releaseDir, cincinnatiJSONFile) + data, err := ioutil.ReadFile(path) + if err != nil { + return release, err + } + + var metadata metadata + if err := json.Unmarshal(data, &metadata); err != nil { + return release, fmt.Errorf("unmarshal Cincinnati metadata: %w", err) + } + + if metadata.Kind != "cincinnati-metadata-v0" { + return release, fmt.Errorf("unrecognized Cincinnati metadata kind %q", metadata.Kind) + } + + if metadata.Version == "" { + return release, errors.New("missing required Cincinnati metadata version") + } + + if _, err := semver.Parse(metadata.Version); err != nil { + return release, fmt.Errorf("Cincinnati metadata version %q is not a valid semantic version: %v", metadata.Version, err) + } + + release.Version = metadata.Version + + if urlInterface, ok := metadata.Metadata["url"]; ok { + if urlString, ok := urlInterface.(string); ok { + release.URL = configv1.URL(urlString) + } else { + klog.Warningf("URL from %s (%s) is not a string: %v", cincinnatiJSONFile, release.Version, urlInterface) + } + } + if channelsInterface, ok := metadata.Metadata["io.openshift.upgrades.graph.release.channels"]; ok { + if channelsString, ok := channelsInterface.(string); ok { + release.Channels = strings.Split(channelsString, ",") + sort.Strings(release.Channels) + } else { + klog.Warningf("channel list from %s (%s) is not a string: %v", cincinnatiJSONFile, release.Version, channelsInterface) + } + } + + return release, nil +} + func loadImageReferences(releaseDir string) (*imagev1.ImageStream, error) { irf := filepath.Join(releaseDir, imageReferencesFile) imageRefData, err := ioutil.ReadFile(irf) @@ -272,7 +347,7 @@ func loadImageReferences(releaseDir string) (*imagev1.ImageStream, error) { imageRefObj, err := resourceread.Read(imageRefData) if err != nil { - return nil, errors.Wrapf(err, "invalid image-references data %s", irf) + return nil, fmt.Errorf("unmarshal image-references: %w", err) } if imageRef, ok := imageRefObj.(*imagev1.ImageStream); ok { return imageRef, nil diff --git a/pkg/payload/payload_test.go b/pkg/payload/payload_test.go index 40d64f49ac..598dc8eb64 100644 --- a/pkg/payload/payload_test.go +++ b/pkg/payload/payload_test.go @@ -11,6 +11,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/diff" + configv1 "github.com/openshift/api/config/v1" imagev1 "github.com/openshift/api/image/v1" "github.com/openshift/cluster-version-operator/lib" @@ -34,8 +35,12 @@ func Test_loadUpdatePayload(t *testing.T) { releaseImage: "image:1", }, want: &Update{ - ReleaseImage: "image:1", - ReleaseVersion: "1.0.0-abc", + Release: configv1.Release{ + Version: "1.0.0-abc", + Image: "image:1", + URL: configv1.URL("https://example.com/v1.0.0-abc"), + Channels: []string{"channel-a", "channel-b", "channel-c"}, + }, ImageRef: &imagev1.ImageStream{ TypeMeta: metav1.TypeMeta{ Kind: "ImageStream", diff --git a/pkg/start/start_integration_test.go b/pkg/start/start_integration_test.go index 1447dc6f51..153027f029 100644 --- a/pkg/start/start_integration_test.go +++ b/pkg/start/start_integration_test.go @@ -47,6 +47,12 @@ func init() { var ( version_0_0_1 = map[string]interface{}{ "release-manifests": map[string]interface{}{ + "release-metadata": ` + { + "kind": "cincinnati-metadata-v0", + "version": "0.0.1" + } + `, "image-references": ` { "kind": "ImageStream", @@ -92,6 +98,12 @@ var ( } version_0_0_2 = map[string]interface{}{ "release-manifests": map[string]interface{}{ + "release-metadata": ` + { + "kind": "cincinnati-metadata-v0", + "version": "0.0.2" + } + `, "image-references": ` { "kind": "ImageStream", @@ -135,6 +147,12 @@ var ( } version_0_0_2_failing = map[string]interface{}{ "release-manifests": map[string]interface{}{ + "release-metadata": ` + { + "kind": "cincinnati-metadata-v0", + "version": "0.0.2" + } + `, "image-references": ` { "kind": "ImageStream", @@ -674,6 +692,12 @@ func TestIntegrationCVO_cincinnatiRequest(t *testing.T) { if err := os.Mkdir(releaseManifestsDir, 0777); err != nil { t.Fatal(err) } + if err := ioutil.WriteFile(filepath.Join(releaseManifestsDir, "release-metadata"), []byte(`{ + "kind": "cincinnati-metadata-v0", + "version": "0.0.1" +}`), 0777); err != nil { + t.Fatal(err) + } if err := ioutil.WriteFile(filepath.Join(releaseManifestsDir, "image-references"), []byte(`kind: ImageStream apiVersion: image.openshift.io/v1 metadata: @@ -995,7 +1019,7 @@ func verifyClusterVersionStatus(t *testing.T, cv *configv1.ClusterVersion, expec if cv.Status.ObservedGeneration != cv.Generation { t.Fatalf("unexpected: %d instead of %d", cv.Status.ObservedGeneration, cv.Generation) } - if cv.Status.Desired != expectedUpdate { + if cv.Status.Desired.Image != expectedUpdate.Image || cv.Status.Desired.Version != expectedUpdate.Version { t.Fatalf("unexpected: %#v", cv.Status.Desired) } if len(cv.Status.History) != expectHistory { diff --git a/vendor/github.com/openshift/api/config/v1/0000_00_cluster-version-operator_01_clusterversion.crd.yaml b/vendor/github.com/openshift/api/config/v1/0000_00_cluster-version-operator_01_clusterversion.crd.yaml index ccde0db23b..cb26372e00 100644 --- a/vendor/github.com/openshift/api/config/v1/0000_00_cluster-version-operator_01_clusterversion.crd.yaml +++ b/vendor/github.com/openshift/api/config/v1/0000_00_cluster-version-operator_01_clusterversion.crd.yaml @@ -162,28 +162,29 @@ spec: channel has been specified. type: array items: - description: Update represents a release of the ClusterVersionOperator, - referenced by the Image member. + description: Release represents an OpenShift release image and associated + metadata. type: object properties: - force: - description: "force allows an administrator to update to an image - that has failed verification, does not appear in the availableUpdates - list, or otherwise would be blocked by normal protections on - update. This option should only be used when the authenticity - of the provided image has been verified out of band because - the provided image will run with full administrative access - to the cluster. Do not use this flag with images that comes - from unknown or potentially malicious sources. \n This flag - does not override other forms of consistency checking that are - required before a new update is deployed." - type: boolean + channels: + description: channels is the set of Cincinnati channels to which + the release currently belongs. + type: array + items: + type: string image: description: image is a container image location that contains the update. When this field is part of spec, image is optional if version is specified and the availableUpdates field contains a matching version. type: string + url: + description: url contains information about this release. This + URL is set by the 'url' metadata property on a release or the + metadata returned by the update API and should be displayed + as a link in user interfaces. The URL field may not be set for + test or nightly releases. + type: string version: description: version is a semantic versioning identifying the update version. When this field is part of spec, version is @@ -234,24 +235,25 @@ spec: tag. type: object properties: - force: - description: "force allows an administrator to update to an image - that has failed verification, does not appear in the availableUpdates - list, or otherwise would be blocked by normal protections on update. - This option should only be used when the authenticity of the provided - image has been verified out of band because the provided image - will run with full administrative access to the cluster. Do not - use this flag with images that comes from unknown or potentially - malicious sources. \n This flag does not override other forms - of consistency checking that are required before a new update - is deployed." - type: boolean + channels: + description: channels is the set of Cincinnati channels to which + the release currently belongs. + type: array + items: + type: string image: description: image is a container image location that contains the update. When this field is part of spec, image is optional if version is specified and the availableUpdates field contains a matching version. type: string + url: + description: url contains information about this release. This URL + is set by the 'url' metadata property on a release or the metadata + returned by the update API and should be displayed as a link in + user interfaces. The URL field may not be set for test or nightly + releases. + type: string version: description: version is a semantic versioning identifying the update version. When this field is part of spec, version is optional diff --git a/vendor/github.com/openshift/api/config/v1/types_cluster_version.go b/vendor/github.com/openshift/api/config/v1/types_cluster_version.go index 771e962add..58a65228da 100644 --- a/vendor/github.com/openshift/api/config/v1/types_cluster_version.go +++ b/vendor/github.com/openshift/api/config/v1/types_cluster_version.go @@ -84,7 +84,7 @@ type ClusterVersionStatus struct { // with the information available, which may be an image or a tag. // +kubebuilder:validation:Required // +required - Desired Update `json:"desired"` + Desired Release `json:"desired"` // history contains a list of the most recent versions applied to the cluster. // This value may be empty during cluster startup, and then will be updated @@ -127,7 +127,7 @@ type ClusterVersionStatus struct { // +nullable // +kubebuilder:validation:Required // +required - AvailableUpdates []Update `json:"availableUpdates"` + AvailableUpdates []Release `json:"availableUpdates"` } // UpdateState is a constant representing whether an update was successfully @@ -221,8 +221,7 @@ type ComponentOverride struct { // URL is a thin wrapper around string that ensures the string is a valid URL. type URL string -// Update represents a release of the ClusterVersionOperator, referenced by the -// Image member. +// Update represents an administrator update request. // +k8s:deepcopy-gen=true type Update struct { // version is a semantic versioning identifying the update version. When this @@ -251,6 +250,34 @@ type Update struct { Force bool `json:"force"` } +// Release represents an OpenShift release image and associated metadata. +// +k8s:deepcopy-gen=true +type Release struct { + // version is a semantic versioning identifying the update version. When this + // field is part of spec, version is optional if image is specified. + // +required + Version string `json:"version"` + + // image is a container image location that contains the update. When this + // field is part of spec, image is optional if version is specified and the + // availableUpdates field contains a matching version. + // +required + Image string `json:"image"` + + // url contains information about this release. This URL is set by + // the 'url' metadata property on a release or the metadata returned by + // the update API and should be displayed as a link in user + // interfaces. The URL field may not be set for test or nightly + // releases. + // +optional + URL URL `json:"url,omitempty"` + + // channels is the set of Cincinnati channels to which the release + // currently belongs. + // +optional + Channels []string `json:"channels,omitempty"` +} + // RetrievedUpdates reports whether available updates have been retrieved from // the upstream update server. The condition is Unknown before retrieval, False // if the updates could not be retrieved or recently failed, or True if the diff --git a/vendor/github.com/openshift/api/config/v1/zz_generated.deepcopy.go b/vendor/github.com/openshift/api/config/v1/zz_generated.deepcopy.go index b81e4cc1fd..7542490ef9 100644 --- a/vendor/github.com/openshift/api/config/v1/zz_generated.deepcopy.go +++ b/vendor/github.com/openshift/api/config/v1/zz_generated.deepcopy.go @@ -929,7 +929,7 @@ func (in *ClusterVersionSpec) DeepCopy() *ClusterVersionSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterVersionStatus) DeepCopyInto(out *ClusterVersionStatus) { *out = *in - out.Desired = in.Desired + in.Desired.DeepCopyInto(&out.Desired) if in.History != nil { in, out := &in.History, &out.History *out = make([]UpdateHistory, len(*in)) @@ -946,8 +946,10 @@ func (in *ClusterVersionStatus) DeepCopyInto(out *ClusterVersionStatus) { } if in.AvailableUpdates != nil { in, out := &in.AvailableUpdates, &out.AvailableUpdates - *out = make([]Update, len(*in)) - copy(*out, *in) + *out = make([]Release, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } return } @@ -3256,6 +3258,27 @@ func (in *RegistrySources) DeepCopy() *RegistrySources { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Release) DeepCopyInto(out *Release) { + *out = *in + if in.Channels != nil { + in, out := &in.Channels, &out.Channels + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Release. +func (in *Release) DeepCopy() *Release { + if in == nil { + return nil + } + out := new(Release) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RemoteConnectionInfo) DeepCopyInto(out *RemoteConnectionInfo) { *out = *in diff --git a/vendor/github.com/openshift/api/config/v1/zz_generated.swagger_doc_generated.go b/vendor/github.com/openshift/api/config/v1/zz_generated.swagger_doc_generated.go index e1a47303a4..9c4e90014c 100644 --- a/vendor/github.com/openshift/api/config/v1/zz_generated.swagger_doc_generated.go +++ b/vendor/github.com/openshift/api/config/v1/zz_generated.swagger_doc_generated.go @@ -529,8 +529,20 @@ func (ComponentOverride) SwaggerDoc() map[string]string { return map_ComponentOverride } +var map_Release = map[string]string{ + "": "Release represents an OpenShift release image and associated metadata.", + "version": "version is a semantic versioning identifying the update version. When this field is part of spec, version is optional if image is specified.", + "image": "image is a container image location that contains the update. When this field is part of spec, image is optional if version is specified and the availableUpdates field contains a matching version.", + "url": "url contains information about this release. This URL is set by the 'url' metadata property on a release or the metadata returned by the update API and should be displayed as a link in user interfaces. The URL field may not be set for test or nightly releases.", + "channels": "channels is the set of Cincinnati channels to which the release currently belongs.", +} + +func (Release) SwaggerDoc() map[string]string { + return map_Release +} + var map_Update = map[string]string{ - "": "Update represents a release of the ClusterVersionOperator, referenced by the Image member.", + "": "Update represents an administrator update request.", "version": "version is a semantic versioning identifying the update version. When this field is part of spec, version is optional if image is specified.", "image": "image is a container image location that contains the update. When this field is part of spec, image is optional if version is specified and the availableUpdates field contains a matching version.", "force": "force allows an administrator to update to an image that has failed verification, does not appear in the availableUpdates list, or otherwise would be blocked by normal protections on update. This option should only be used when the authenticity of the provided image has been verified out of band because the provided image will run with full administrative access to the cluster. Do not use this flag with images that comes from unknown or potentially malicious sources.\n\nThis flag does not override other forms of consistency checking that are required before a new update is deployed.", diff --git a/vendor/modules.txt b/vendor/modules.txt index 38a772ebe5..4e888baa84 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -54,7 +54,7 @@ github.com/matttproud/golang_protobuf_extensions/pbutil github.com/modern-go/concurrent # github.com/modern-go/reflect2 v1.0.1 github.com/modern-go/reflect2 -# github.com/openshift/api v0.0.0-20200723134351-89de68875e7c +# github.com/openshift/api v0.0.0-20200724204552-3ae6754513d4 github.com/openshift/api/config/v1 github.com/openshift/api/image/docker10 github.com/openshift/api/image/dockerpre012