@@ -12,8 +12,10 @@ import (
12
12
"io"
13
13
"net/http"
14
14
"net/url"
15
+ "runtime"
15
16
"sort"
16
17
18
+ "github.com/elastic/elastic-agent/pkg/testing"
17
19
"github.com/elastic/elastic-agent/pkg/version"
18
20
)
19
21
@@ -26,6 +28,7 @@ const (
26
28
// artifactAPIV1SearchVersionPackage = "v1/search/%s/%s"
27
29
28
30
artifactElasticAgentProject = "elastic-agent-package"
31
+ artifactReleaseCDN = "https://artifacts.elastic.co/downloads/beats/elastic-agent"
29
32
)
30
33
31
34
var (
@@ -119,6 +122,10 @@ func WithUrl(url string) ArtifactAPIClientOpt {
119
122
return func (aac * ArtifactAPIClient ) { aac .url = url }
120
123
}
121
124
125
+ func WithCDNUrl (url string ) ArtifactAPIClientOpt {
126
+ return func (aac * ArtifactAPIClient ) { aac .cdnURL = url }
127
+ }
128
+
122
129
func WithHttpClient (client httpDoer ) ArtifactAPIClientOpt {
123
130
return func (aac * ArtifactAPIClient ) { aac .c = client }
124
131
}
@@ -127,15 +134,17 @@ func WithHttpClient(client httpDoer) ArtifactAPIClientOpt {
127
134
// More information about the API can be found at https://artifacts-api.elastic.co/v1
128
135
// which will print a list of available operations
129
136
type ArtifactAPIClient struct {
130
- c httpDoer
131
- url string
137
+ c httpDoer
138
+ url string
139
+ cdnURL string
132
140
}
133
141
134
142
// NewArtifactAPIClient creates a new Artifact API client
135
143
func NewArtifactAPIClient (opts ... ArtifactAPIClientOpt ) * ArtifactAPIClient {
136
144
c := & ArtifactAPIClient {
137
- url : defaultArtifactAPIURL ,
138
- c : new (http.Client ),
145
+ url : defaultArtifactAPIURL ,
146
+ cdnURL : artifactReleaseCDN ,
147
+ c : new (http.Client ),
139
148
}
140
149
141
150
for _ , opt := range opts {
@@ -161,6 +170,68 @@ func (aac ArtifactAPIClient) GetVersions(ctx context.Context) (list *VersionList
161
170
return checkResponseAndUnmarshal [VersionList ](resp )
162
171
}
163
172
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
+
164
235
// GetBuildsForVersion returns a list of builds for a specific version.
165
236
// version should be one of the version strings returned by the GetVersions (expected format is semver
166
237
// with optional prerelease but no build metadata, for example 8.9.0-SNAPSHOT)
0 commit comments