diff --git a/cli/cmd/release_image_ls.go b/cli/cmd/release_image_ls.go index 6513a1e1a..1f70badd3 100644 --- a/cli/cmd/release_image_ls.go +++ b/cli/cmd/release_image_ls.go @@ -64,13 +64,9 @@ func (r *runners) releaseImageLS(cmd *cobra.Command, args []string) error { var proxyDomain string if r.args.releaseImageLSVersion != "" { - // For specific versions, we need to get all releases - channelReleases, err := r.api.ListChannelReleases(r.appID, r.appType, channel.ID, r.args.releaseImageLSIncludeInstallerImages) - if err != nil { - return fmt.Errorf("failed to list channel releases: %w", err) - } - - targetRelease, err = findTargetRelease(channelReleases, r.args.releaseImageLSVersion) + // For specific versions, try the server-side versionLabel filter first. + // This avoids downloading every page of a large channel history. + targetRelease, err = r.findReleaseByVersion(channel.ID, r.args.releaseImageLSVersion) if err != nil { return err } @@ -165,46 +161,22 @@ func cleanImageName(image string, proxyRegistryDomain string) string { } for _, prefix := range prefixes { - if strings.HasPrefix(cleaned, prefix) { - cleaned = strings.TrimPrefix(cleaned, prefix) - } + cleaned = strings.TrimPrefix(cleaned, prefix) } return cleaned } -// findTargetRelease finds the target release from a list of releases -// If requestedVersion is empty, returns the current release (highest channel sequence) -// If requestedVersion is specified, returns the release with matching semver -func findTargetRelease(releases []*types.ChannelRelease, requestedVersion string) (*types.ChannelRelease, error) { - if len(releases) == 0 { - return nil, errors.New("no releases found in channel") +// findReleaseByVersion returns the channel release matching the requested version. +// ListChannelReleasesByVersion uses a server-side versionLabel filter and returns +// matching releases sorted by channel sequence, so the first result is the target. +func (r *runners) findReleaseByVersion(channelID string, version string) (*types.ChannelRelease, error) { + releases, err := r.api.ListChannelReleasesByVersion(r.appID, r.appType, channelID, version, r.args.releaseImageLSIncludeInstallerImages) + if err != nil { + return nil, fmt.Errorf("failed to list channel releases: %w", err) } - - var targetRelease *types.ChannelRelease - - if requestedVersion != "" { - // Find release by semver - for _, release := range releases { - if release.Semver == requestedVersion { - targetRelease = release - break - } - } - if targetRelease == nil { - return nil, fmt.Errorf("no release found with version %q in channel", requestedVersion) - } - } else { - // Find the current release (highest channel sequence) - for _, release := range releases { - if targetRelease == nil || release.ChannelSequence > targetRelease.ChannelSequence { - targetRelease = release - } - } - if targetRelease == nil { - return nil, errors.New("no current release found") - } + if len(releases) == 0 { + return nil, fmt.Errorf("no release found with version %q in channel", version) } - - return targetRelease, nil + return releases[0], nil } diff --git a/cli/cmd/release_image_ls_test.go b/cli/cmd/release_image_ls_test.go index 05454370c..cb76f07f1 100644 --- a/cli/cmd/release_image_ls_test.go +++ b/cli/cmd/release_image_ls_test.go @@ -3,7 +3,6 @@ package cmd import ( "testing" - "github.com/replicatedhq/replicated/pkg/types" "github.com/stretchr/testify/assert" ) @@ -77,68 +76,3 @@ func TestCleanImageName(t *testing.T) { }) } } - -func TestFindTargetRelease(t *testing.T) { - tests := []struct { - name string - releases []*types.ChannelRelease - requestedVersion string - expectedSequence int32 - expectError bool - errorMsg string - }{ - { - name: "find current release (highest channel sequence)", - releases: []*types.ChannelRelease{ - {ChannelSequence: 5, Semver: "1.0.0"}, - {ChannelSequence: 10, Semver: "1.1.0"}, - {ChannelSequence: 8, Semver: "1.0.5"}, - }, - requestedVersion: "", - expectedSequence: 10, - }, - { - name: "find specific version", - releases: []*types.ChannelRelease{ - {ChannelSequence: 5, Semver: "1.0.0"}, - {ChannelSequence: 10, Semver: "1.1.0"}, - {ChannelSequence: 8, Semver: "1.0.5"}, - }, - requestedVersion: "1.0.5", - expectedSequence: 8, - }, - { - name: "version not found", - releases: []*types.ChannelRelease{ - {ChannelSequence: 5, Semver: "1.0.0"}, - {ChannelSequence: 10, Semver: "1.1.0"}, - }, - requestedVersion: "2.0.0", - expectError: true, - errorMsg: "no release found with version \"2.0.0\"", - }, - { - name: "no releases", - releases: []*types.ChannelRelease{}, - requestedVersion: "", - expectError: true, - errorMsg: "no releases found in channel", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - targetRelease, err := findTargetRelease(tt.releases, tt.requestedVersion) - - if tt.expectError { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.errorMsg) - assert.Nil(t, targetRelease) - } else { - assert.NoError(t, err) - assert.NotNil(t, targetRelease) - assert.Equal(t, tt.expectedSequence, targetRelease.ChannelSequence) - } - }) - } -} diff --git a/client/channel.go b/client/channel.go index bb929cba6..9d8889894 100644 --- a/client/channel.go +++ b/client/channel.go @@ -206,6 +206,15 @@ func (c *Client) ListChannelReleasesPaged(appID string, appType string, channelI return nil, errors.Errorf("unknown app type %q", appType) } +func (c *Client) ListChannelReleasesByVersion(appID string, appType string, channelID string, versionLabel string, includeInstallerImages string) ([]*types.ChannelRelease, error) { + if appType == "platform" { + return nil, errors.New("This feature is not currently supported for Platform applications.") + } else if appType == "kots" { + return c.KotsClient.ListChannelReleasesByVersion(appID, channelID, versionLabel, includeInstallerImages) + } + return nil, errors.Errorf("unknown app type %q", appType) +} + func (c *Client) GetCustomHostnames(appID string, appType string, channelID string) (*types.CustomHostNameOverrides, error) { if appType == "platform" { return nil, errors.New("This feature is not currently supported for Platform applications.") diff --git a/pkg/kotsclient/channel.go b/pkg/kotsclient/channel.go index b2162f525..e0f4c8fc6 100644 --- a/pkg/kotsclient/channel.go +++ b/pkg/kotsclient/channel.go @@ -15,6 +15,10 @@ type ListChannelsResponse struct { Channels []*types.KotsChannel `json:"channels"` } +type listChannelReleasesResponse struct { + Releases []*types.ChannelRelease `json:"releases"` +} + func (c *VendorV3Client) ListKotsChannels(appID string, channelName string, excludeDetails bool) ([]*types.KotsChannel, error) { var response = ListChannelsResponse{} v := url.Values{} @@ -155,14 +159,50 @@ func (c *VendorV3Client) UnDemoteChannelRelease(appID string, channelID string, } func (c *VendorV3Client) ListChannelReleases(appID string, channelID string, includeInstallerImages string) ([]*types.ChannelRelease, error) { - return c.ListChannelReleasesPaged(appID, channelID, includeInstallerImages, 0, 0) + allReleases := []*types.ChannelRelease{} + page := 0 + pageSize := 20 + + for { + releases, err := c.ListChannelReleasesPaged(appID, channelID, includeInstallerImages, page, pageSize) + if err != nil { + return nil, err + } + if len(releases) == 0 { + break + } + allReleases = append(allReleases, releases...) + page += 1 + } + + return allReleases, nil } -func (c *VendorV3Client) ListChannelReleasesPaged(appID string, channelID string, includeInstallerImages string, page int, pageSize int) ([]*types.ChannelRelease, error) { - type listChannelReleasesResponse struct { - Releases []*types.ChannelRelease `json:"releases"` +func (c *VendorV3Client) ListChannelReleasesByVersion(appID string, channelID string, versionLabel string, includeInstallerImages string) ([]*types.ChannelRelease, error) { + response := listChannelReleasesResponse{} + v := url.Values{} + v.Set("versionLabel", versionLabel) + if includeInstallerImages != "" { + v.Set("includeInstallerImages", includeInstallerImages) } + // A versionLabel filter should return a small number of releases, so a + // single page is sufficient. This avoids the cost of paginating through a + // large channel history when looking for a specific version. + v.Set("pageSize", "20") + reqURL := fmt.Sprintf("/v3/app/%s/channel/%s/releases", appID, url.QueryEscape(channelID)) + if encoded := v.Encode(); encoded != "" { + reqURL = fmt.Sprintf("%s?%s", reqURL, encoded) + } + err := c.DoJSON(context.TODO(), "GET", reqURL, http.StatusOK, nil, &response) + if err != nil { + return nil, errors.Wrap(err, "list channel releases by version") + } + + return response.Releases, nil +} + +func (c *VendorV3Client) ListChannelReleasesPaged(appID string, channelID string, includeInstallerImages string, page int, pageSize int) ([]*types.ChannelRelease, error) { response := listChannelReleasesResponse{} v := url.Values{} if includeInstallerImages != "" { diff --git a/pkg/kotsclient/channel_test.go b/pkg/kotsclient/channel_test.go new file mode 100644 index 000000000..d9599da5a --- /dev/null +++ b/pkg/kotsclient/channel_test.go @@ -0,0 +1,36 @@ +package kotsclient + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/replicatedhq/replicated/pkg/platformclient" + "github.com/replicatedhq/replicated/pkg/types" + "github.com/stretchr/testify/require" +) + +func TestListChannelReleasesByVersion(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/v3/app/app-id/channel/channel-id/releases", r.URL.Path) + require.Equal(t, "test-token", r.Header.Get("Authorization")) + require.Equal(t, "2.17.12", r.URL.Query().Get("versionLabel")) + require.Equal(t, "20", r.URL.Query().Get("pageSize")) + + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "releases": []types.ChannelRelease{ + {Semver: "2.17.12"}, + }, + }) + })) + defer server.Close() + + api := platformclient.NewHTTPClient(server.URL, "test-token") + client := VendorV3Client{HTTPClient: *api} + + releases, err := client.ListChannelReleasesByVersion("app-id", "channel-id", "2.17.12", "") + require.NoError(t, err) + require.Len(t, releases, 1) + require.Equal(t, "2.17.12", releases[0].Semver) +}