Skip to content

Commit b425e4f

Browse files
authored
Check if non-snapshot version is released before using for testing (#4276)
Currently, we don't check whether a version without the `-SNAPSHOT` suffix is actually released and available on our public CDN. The version is present on the artifact API once the first BC is built, however, this does not mean the version is published on the CDN. This leads to failing upgrade attempts by the agent in our integration tests.
1 parent f23c279 commit b425e4f

File tree

3 files changed

+153
-4
lines changed

3 files changed

+153
-4
lines changed

pkg/testing/tools/artifacts_api.go

+75-4
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import (
1212
"io"
1313
"net/http"
1414
"net/url"
15+
"runtime"
1516
"sort"
1617

18+
"github.com/elastic/elastic-agent/pkg/testing"
1719
"github.com/elastic/elastic-agent/pkg/version"
1820
)
1921

@@ -26,6 +28,7 @@ const (
2628
// artifactAPIV1SearchVersionPackage = "v1/search/%s/%s"
2729

2830
artifactElasticAgentProject = "elastic-agent-package"
31+
artifactReleaseCDN = "https://artifacts.elastic.co/downloads/beats/elastic-agent"
2932
)
3033

3134
var (
@@ -119,6 +122,10 @@ func WithUrl(url string) ArtifactAPIClientOpt {
119122
return func(aac *ArtifactAPIClient) { aac.url = url }
120123
}
121124

125+
func WithCDNUrl(url string) ArtifactAPIClientOpt {
126+
return func(aac *ArtifactAPIClient) { aac.cdnURL = url }
127+
}
128+
122129
func WithHttpClient(client httpDoer) ArtifactAPIClientOpt {
123130
return func(aac *ArtifactAPIClient) { aac.c = client }
124131
}
@@ -127,15 +134,17 @@ func WithHttpClient(client httpDoer) ArtifactAPIClientOpt {
127134
// More information about the API can be found at https://artifacts-api.elastic.co/v1
128135
// which will print a list of available operations
129136
type ArtifactAPIClient struct {
130-
c httpDoer
131-
url string
137+
c httpDoer
138+
url string
139+
cdnURL string
132140
}
133141

134142
// NewArtifactAPIClient creates a new Artifact API client
135143
func NewArtifactAPIClient(opts ...ArtifactAPIClientOpt) *ArtifactAPIClient {
136144
c := &ArtifactAPIClient{
137-
url: defaultArtifactAPIURL,
138-
c: new(http.Client),
145+
url: defaultArtifactAPIURL,
146+
cdnURL: artifactReleaseCDN,
147+
c: new(http.Client),
139148
}
140149

141150
for _, opt := range opts {
@@ -161,6 +170,68 @@ func (aac ArtifactAPIClient) GetVersions(ctx context.Context) (list *VersionList
161170
return checkResponseAndUnmarshal[VersionList](resp)
162171
}
163172

173+
// RemoveUnreleasedVersions from the list
174+
// There is a period of time when a version is already marked as released
175+
// but not published on the CDN. This happens when we already have build candidates.
176+
// This function checks if a version marked as released actually has published artifacts.
177+
// If there are no published artifacts, the version is removed from the list.
178+
func (aac ArtifactAPIClient) RemoveUnreleasedVersions(ctx context.Context, vList *VersionList) error {
179+
suffix, err := testing.GetPackageSuffix(runtime.GOOS, runtime.GOARCH)
180+
if err != nil {
181+
return fmt.Errorf("failed to generate the artifact suffix: %w", err)
182+
}
183+
184+
results := make([]string, 0, len(vList.Versions))
185+
186+
for _, versionItem := range vList.Versions {
187+
parsedVersion, err := version.ParseVersion(versionItem)
188+
if err != nil {
189+
return fmt.Errorf("failed to parse version %s: %w", versionItem, err)
190+
}
191+
// we check only release versions without `-SNAPSHOT`
192+
if parsedVersion.Prerelease() != "" {
193+
results = append(results, versionItem)
194+
continue
195+
}
196+
url := fmt.Sprintf("%s/elastic-agent-%s-%s", aac.cdnURL, versionItem, suffix)
197+
// using method `HEAD` to avoid downloading the file
198+
req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
199+
if err != nil {
200+
return fmt.Errorf("failed to create an HTTP request to %q: %w", url, err)
201+
}
202+
203+
resp, err := http.DefaultClient.Do(req)
204+
if err != nil {
205+
return fmt.Errorf("failed to request %q: %w", url, err)
206+
}
207+
208+
// we don't read the response. However, we must drain when it's present,
209+
// so the connection can be re-used later, see:
210+
// https://cs.opensource.google/go/go/+/refs/tags/go1.22.0:src/net/http/response.go;l=62-64
211+
_, _ = io.Copy(io.Discard, resp.Body)
212+
_ = resp.Body.Close()
213+
214+
switch resp.StatusCode {
215+
case http.StatusNotFound:
216+
continue
217+
case http.StatusOK:
218+
results = append(results, versionItem)
219+
continue
220+
default:
221+
return fmt.Errorf("unexpected status code from %s - %d", url, resp.StatusCode)
222+
}
223+
}
224+
225+
// nothing changed
226+
if len(vList.Versions) == len(results) {
227+
return nil
228+
}
229+
230+
vList.Versions = results
231+
232+
return nil
233+
}
234+
164235
// GetBuildsForVersion returns a list of builds for a specific version.
165236
// version should be one of the version strings returned by the GetVersions (expected format is semver
166237
// with optional prerelease but no build metadata, for example 8.9.0-SNAPSHOT)

pkg/testing/tools/artifacts_api_test.go

+74
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ package tools
66

77
import (
88
"context"
9+
"encoding/json"
910
"fmt"
1011
"net/http"
1112
"net/http/httptest"
13+
"regexp"
1214
"testing"
1315

1416
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/require"
1518
)
1619

1720
const (
@@ -216,3 +219,74 @@ func TestDefaultArtifactAPIClient(t *testing.T) {
216219
assert.NotEmpty(t, buildDetails.Build.Projects)
217220
assert.Contains(t, buildDetails.Build.Projects, "elastic-agent")
218221
}
222+
223+
func createVersionList(t *testing.T) *VersionList {
224+
var list VersionList
225+
err := json.Unmarshal([]byte(cannedVersions), &list)
226+
require.NoError(t, err)
227+
return &list
228+
}
229+
230+
func TestRemoveUnreleasedVersions(t *testing.T) {
231+
versionRegExp := regexp.MustCompile(`/elastic-agent-(\d+\.\d+\.\d+)-.+$`)
232+
var unreleasedVersions map[string]struct{}
233+
server := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
234+
matches := versionRegExp.FindAllStringSubmatch(req.URL.Path, 2)
235+
if len(matches) == 0 || len(matches[0]) < 2 {
236+
resp.WriteHeader(http.StatusBadRequest)
237+
return
238+
}
239+
version := matches[0][1]
240+
if _, unreleased := unreleasedVersions[version]; unreleased {
241+
resp.WriteHeader(http.StatusNotFound)
242+
return
243+
}
244+
245+
resp.WriteHeader(http.StatusOK)
246+
}))
247+
248+
client := NewArtifactAPIClient(WithCDNUrl(server.URL))
249+
250+
t.Run("removes unreleased versions", func(t *testing.T) {
251+
unreleasedVersions = map[string]struct{}{
252+
"8.6.1": {},
253+
"8.8.0": {},
254+
}
255+
256+
versionList := createVersionList(t)
257+
err := client.RemoveUnreleasedVersions(context.Background(), versionList)
258+
require.NoError(t, err)
259+
exp := []string{
260+
"7.17.9",
261+
"7.17.10",
262+
"8.6.0",
263+
"8.6.2",
264+
"8.7.0",
265+
"8.7.1",
266+
"8.8.1",
267+
"8.9.0-SNAPSHOT",
268+
}
269+
require.Equal(t, exp, versionList.Versions)
270+
})
271+
272+
t.Run("does not change the list if all released", func(t *testing.T) {
273+
unreleasedVersions = map[string]struct{}{} // everything is released
274+
275+
versionList := createVersionList(t)
276+
err := client.RemoveUnreleasedVersions(context.Background(), versionList)
277+
require.NoError(t, err)
278+
exp := []string{
279+
"7.17.9",
280+
"7.17.10",
281+
"8.6.0",
282+
"8.6.1",
283+
"8.6.2",
284+
"8.7.0",
285+
"8.7.1",
286+
"8.8.0",
287+
"8.8.1",
288+
"8.9.0-SNAPSHOT",
289+
}
290+
require.Equal(t, exp, versionList.Versions)
291+
})
292+
}

testing/upgradetest/versions.go

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ func GetUpgradableVersions(ctx context.Context, upgradeToVersion string, current
4444
if len(vList.Versions) == 0 {
4545
return nil, errors.New("retrieved versions list from Artifact API is empty")
4646
}
47+
err = aac.RemoveUnreleasedVersions(ctx, vList)
48+
if err != nil {
49+
return nil, fmt.Errorf("failed to remove unreleased versions: %w", err)
50+
}
4751

4852
return getUpgradableVersions(ctx, vList, upgradeToVersion, currentMajorVersions, previousMajorVersions)
4953
}

0 commit comments

Comments
 (0)