@@ -16,6 +16,8 @@ import (
16
16
"strings"
17
17
"sync/atomic"
18
18
"time"
19
+
20
+ semver "github.com/elastic/elastic-agent/pkg/version"
19
21
)
20
22
21
23
type httpDoer interface {
@@ -63,29 +65,34 @@ func (f *artifactFetcher) Fetch(ctx context.Context, operatingSystem string, arc
63
65
return nil , err
64
66
}
65
67
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 )
70
71
}
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 ())
80
78
}
81
79
}
82
80
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
+
85
90
return & artifactResult {
86
91
doer : f .doer ,
87
92
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 ),
89
96
}, nil
90
97
}
91
98
@@ -102,56 +109,117 @@ func (r *artifactResult) Name() string {
102
109
103
110
// Fetch performs the actual fetch into the provided directory.
104
111
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 )
106
120
if err != nil {
107
121
return fmt .Errorf ("failed to download %s: %w" , r .src , err )
108
122
}
109
123
110
124
// 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 )
112
126
if err != nil {
113
127
return fmt .Errorf ("failed to download %s: %w" , r .src , err )
114
128
}
115
129
116
130
// 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 )
118
132
if err != nil {
119
133
return fmt .Errorf ("failed to download %s: %w" , r .src , err )
120
134
}
121
135
122
136
return nil
123
137
}
124
138
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 )
129
180
if err != nil {
130
- return "" , err
181
+ return nil , err
131
182
}
132
183
resp , err := doer .Do (req )
133
184
if err != nil {
134
- return "" , err
185
+ return nil , err
135
186
}
136
187
defer resp .Body .Close ()
137
188
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 )
139
190
}
140
191
141
- body := struct {
142
- Packages map [string ]interface {} `json:"packages"`
143
- }{}
192
+ var body projectResponse
144
193
145
194
dec := json .NewDecoder (resp .Body )
146
195
if err := dec .Decode (& body ); err != nil {
147
- return "" , err
196
+ return nil , err
148
197
}
149
198
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 {
151
219
return "" , fmt .Errorf ("no packages found in repo" )
152
220
}
153
221
154
- for k , pkg := range body .Packages {
222
+ for k , pkg := range project .Packages {
155
223
pkgMap , ok := pkg .(map [string ]interface {})
156
224
if ! ok {
157
225
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)
177
245
// https://snapshots.elastic.co/8.7.0-d050210c/downloads/elastic-agent-shipper/elastic-agent-shipper-8.7.0-SNAPSHOT-linux-x86_64.tar.gz
178
246
index := strings .Index (uri , "/beats/elastic-agent/" )
179
247
if index != - 1 {
180
- if buildID == "" {
248
+ if version . BuildMetadata () == "" {
181
249
// no build id, first is selected
182
250
return fmt .Sprintf ("%s/beats/elastic-agent/" , uri [:index ]), nil
183
251
}
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 () )) {
185
253
return fmt .Sprintf ("%s/beats/elastic-agent/" , uri [:index ]), nil
186
254
}
187
255
}
188
256
}
189
257
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 )
208
260
}
209
- return version
261
+ return "" , fmt . Errorf ( "uri not detected with specific build ID %q" , version . BuildMetadata ())
210
262
}
211
263
212
264
func DownloadPackage (ctx context.Context , l Logger , doer httpDoer , downloadPath string , packageFile string ) error {
0 commit comments