Skip to content

Commit 84e6bc1

Browse files
authored
Add support fetching artefacts for a build ID (#4163)
Previously it was not implemented but some of the tests requested a specific build by its ID. The fetcher was incorrectly returning just the latest snapshot in this case. Also, the artifact caching system was not aware of build IDs and didn't distinguish snapshots of different builds. This led to caching the latest snapshot and returning it for any requested build ID.
1 parent ece91fe commit 84e6bc1

8 files changed

+6760
-82
lines changed

pkg/testing/fetcher_artifact.go

+105-53
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
"strings"
1717
"sync/atomic"
1818
"time"
19+
20+
semver "github.com/elastic/elastic-agent/pkg/version"
1921
)
2022

2123
type httpDoer interface {
@@ -63,29 +65,34 @@ func (f *artifactFetcher) Fetch(ctx context.Context, operatingSystem string, arc
6365
return nil, err
6466
}
6567

66-
var uri string
67-
var prevErr error
68-
if !f.snapshotOnly {
69-
uri, prevErr = findURI(ctx, f.doer, version)
68+
ver, err := semver.ParseVersion(version)
69+
if err != nil {
70+
return nil, fmt.Errorf("invalid version: %q: %w", ver, err)
7071
}
71-
preVersion := version
72-
version, _ = splitBuildID(version)
73-
if uri == "" {
74-
if !strings.HasSuffix(version, "-SNAPSHOT") {
75-
version += "-SNAPSHOT"
76-
}
77-
uri, err = findURI(ctx, f.doer, version)
78-
if err != nil {
79-
return nil, fmt.Errorf("failed to find snapshot URI for version %s: %w (previous error: %w)", preVersion, err, prevErr)
72+
73+
if f.snapshotOnly && !ver.IsSnapshot() {
74+
if ver.Prerelease() == "" {
75+
ver = semver.NewParsedSemVer(ver.Major(), ver.Minor(), ver.Patch(), "SNAPSHOT", ver.BuildMetadata())
76+
} else {
77+
ver = semver.NewParsedSemVer(ver.Major(), ver.Minor(), ver.Patch(), ver.Prerelease()+"-SNAPSHOT", ver.BuildMetadata())
8078
}
8179
}
8280

83-
path := fmt.Sprintf("elastic-agent-%s-%s", version, suffix)
84-
downloadSrc := fmt.Sprintf("%s%s", uri, path)
81+
uri, err := findURI(ctx, f.doer, ver)
82+
if err != nil {
83+
return nil, fmt.Errorf("failed to find snapshot URI for version %s: %w", ver, err)
84+
}
85+
86+
// this remote path cannot have the build metadata in it
87+
srcPath := fmt.Sprintf("elastic-agent-%s-%s", ver.VersionWithPrerelease(), suffix)
88+
downloadSrc := fmt.Sprintf("%s%s", uri, srcPath)
89+
8590
return &artifactResult{
8691
doer: f.doer,
8792
src: downloadSrc,
88-
path: path,
93+
// this path must have the build metadata in it, so we don't mix such files with
94+
// no build-specific snapshots. If build metadata is empty, it's just `srcPath`.
95+
path: filepath.Join(ver.BuildMetadata(), srcPath),
8996
}, nil
9097
}
9198

@@ -102,56 +109,117 @@ func (r *artifactResult) Name() string {
102109

103110
// Fetch performs the actual fetch into the provided directory.
104111
func (r *artifactResult) Fetch(ctx context.Context, l Logger, dir string) error {
105-
err := DownloadPackage(ctx, l, r.doer, r.src, filepath.Join(dir, r.path))
112+
dst := filepath.Join(dir, r.Name())
113+
// the artifact name can contain a subfolder that needs to be created
114+
err := os.MkdirAll(filepath.Dir(dst), 0755)
115+
if err != nil {
116+
return fmt.Errorf("failed to create path %q: %w", dst, err)
117+
}
118+
119+
err = DownloadPackage(ctx, l, r.doer, r.src, dst)
106120
if err != nil {
107121
return fmt.Errorf("failed to download %s: %w", r.src, err)
108122
}
109123

110124
// fetch package hash
111-
err = DownloadPackage(ctx, l, r.doer, r.src+extHash, filepath.Join(dir, r.path+extHash))
125+
err = DownloadPackage(ctx, l, r.doer, r.src+extHash, dst+extHash)
112126
if err != nil {
113127
return fmt.Errorf("failed to download %s: %w", r.src, err)
114128
}
115129

116130
// fetch package asc
117-
err = DownloadPackage(ctx, l, r.doer, r.src+extAsc, filepath.Join(dir, r.path+extAsc))
131+
err = DownloadPackage(ctx, l, r.doer, r.src+extAsc, dst+extAsc)
118132
if err != nil {
119133
return fmt.Errorf("failed to download %s: %w", r.src, err)
120134
}
121135

122136
return nil
123137
}
124138

125-
func findURI(ctx context.Context, doer httpDoer, version string) (string, error) {
126-
version, buildID := splitBuildID(version)
127-
artifactsURI := fmt.Sprintf("https://artifacts-api.elastic.co/v1/search/%s/elastic-agent", version)
128-
req, err := http.NewRequestWithContext(ctx, "GET", artifactsURI, nil)
139+
type projectResponse struct {
140+
Packages map[string]interface{} `json:"packages"`
141+
}
142+
143+
type projectsResponse struct {
144+
ElasticPackage projectResponse `json:"elastic-agent-package"`
145+
}
146+
147+
type manifestResponse struct {
148+
Projects projectsResponse `json:"projects"`
149+
}
150+
151+
func findBuild(ctx context.Context, doer httpDoer, version *semver.ParsedSemVer) (*projectResponse, error) {
152+
// e.g. https://snapshots.elastic.co/8.13.0-l5snflwr/manifest-8.13.0-SNAPSHOT.json
153+
manifestURI := fmt.Sprintf("https://snapshots.elastic.co/%s-%s/manifest-%s-SNAPSHOT.json", version.CoreVersion(), version.BuildMetadata(), version.CoreVersion())
154+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, manifestURI, nil)
155+
if err != nil {
156+
return nil, err
157+
}
158+
resp, err := doer.Do(req)
159+
if err != nil {
160+
return nil, err
161+
}
162+
defer resp.Body.Close()
163+
if resp.StatusCode != http.StatusOK {
164+
return nil, fmt.Errorf("%s; bad status: %s", manifestURI, resp.Status)
165+
}
166+
167+
var body manifestResponse
168+
169+
dec := json.NewDecoder(resp.Body)
170+
if err := dec.Decode(&body); err != nil {
171+
return nil, err
172+
}
173+
174+
return &body.Projects.ElasticPackage, nil
175+
}
176+
177+
func findVersion(ctx context.Context, doer httpDoer, version *semver.ParsedSemVer) (*projectResponse, error) {
178+
artifactsURI := fmt.Sprintf("https://artifacts-api.elastic.co/v1/search/%s/elastic-agent", version.VersionWithPrerelease())
179+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, artifactsURI, nil)
129180
if err != nil {
130-
return "", err
181+
return nil, err
131182
}
132183
resp, err := doer.Do(req)
133184
if err != nil {
134-
return "", err
185+
return nil, err
135186
}
136187
defer resp.Body.Close()
137188
if resp.StatusCode != http.StatusOK {
138-
return "", fmt.Errorf("%s; bad status: %s", artifactsURI, resp.Status)
189+
return nil, fmt.Errorf("%s; bad status: %s", artifactsURI, resp.Status)
139190
}
140191

141-
body := struct {
142-
Packages map[string]interface{} `json:"packages"`
143-
}{}
192+
var body projectResponse
144193

145194
dec := json.NewDecoder(resp.Body)
146195
if err := dec.Decode(&body); err != nil {
147-
return "", err
196+
return nil, err
148197
}
149198

150-
if len(body.Packages) == 0 {
199+
return &body, nil
200+
}
201+
202+
func findURI(ctx context.Context, doer httpDoer, version *semver.ParsedSemVer) (string, error) {
203+
var (
204+
project *projectResponse
205+
err error
206+
)
207+
208+
if version.BuildMetadata() != "" {
209+
project, err = findBuild(ctx, doer, version)
210+
} else {
211+
project, err = findVersion(ctx, doer, version)
212+
}
213+
214+
if err != nil {
215+
return "", fmt.Errorf("failed to find package URL: %w", err)
216+
}
217+
218+
if len(project.Packages) == 0 {
151219
return "", fmt.Errorf("no packages found in repo")
152220
}
153221

154-
for k, pkg := range body.Packages {
222+
for k, pkg := range project.Packages {
155223
pkgMap, ok := pkg.(map[string]interface{})
156224
if !ok {
157225
return "", fmt.Errorf("content of '%s' is not a map", k)
@@ -177,36 +245,20 @@ func findURI(ctx context.Context, doer httpDoer, version string) (string, error)
177245
// https://snapshots.elastic.co/8.7.0-d050210c/downloads/elastic-agent-shipper/elastic-agent-shipper-8.7.0-SNAPSHOT-linux-x86_64.tar.gz
178246
index := strings.Index(uri, "/beats/elastic-agent/")
179247
if index != -1 {
180-
if buildID == "" {
248+
if version.BuildMetadata() == "" {
181249
// no build id, first is selected
182250
return fmt.Sprintf("%s/beats/elastic-agent/", uri[:index]), nil
183251
}
184-
if strings.Contains(uri, fmt.Sprintf("%s-%s", stripSnapshot(version), buildID)) {
252+
if strings.Contains(uri, fmt.Sprintf("%s-%s", version.CoreVersion(), version.BuildMetadata())) {
185253
return fmt.Sprintf("%s/beats/elastic-agent/", uri[:index]), nil
186254
}
187255
}
188256
}
189257

190-
if buildID == "" {
191-
return "", fmt.Errorf("uri not detected")
192-
}
193-
return "", fmt.Errorf("uri not detected with specific buildid %s", buildID)
194-
}
195-
196-
func splitBuildID(version string) (string, string) {
197-
split := strings.SplitN(version, "+", 2)
198-
if len(split) == 1 {
199-
// no build ID
200-
return split[0], ""
201-
}
202-
return split[0], split[1]
203-
}
204-
205-
func stripSnapshot(version string) string {
206-
if strings.HasSuffix(version, "-SNAPSHOT") {
207-
return strings.TrimSuffix(version, "-SNAPSHOT")
258+
if version.BuildMetadata() == "" {
259+
return "", fmt.Errorf("uri for version %q not detected", version)
208260
}
209-
return version
261+
return "", fmt.Errorf("uri not detected with specific build ID %q", version.BuildMetadata())
210262
}
211263

212264
func DownloadPackage(ctx context.Context, l Logger, doer httpDoer, downloadPath string, packageFile string) error {

0 commit comments

Comments
 (0)