From 377a3b36b340b973f98a359d8bbc47f58fc0a655 Mon Sep 17 00:00:00 2001 From: Matt Devy Date: Wed, 26 Nov 2025 17:33:42 +0000 Subject: [PATCH 1/4] chore: migrate to new artifact api refactor: create a tested artifact client --- .../generate/commands/genexamples/model.go | 10 - .../generate/commands/gensource/overrides.go | 4 - .../cmd/generate/commands/gentests/patches.go | 1 - .../build/cmd/tools/commands/spec/command.go | 265 ++------------ .../cmd/tools/commands/spec/command_test.go | 66 ---- internal/build/go.mod | 12 +- internal/build/go.sum | 32 +- internal/build/sdk/artifacts/client.go | 112 ++++++ .../sdk/artifacts/latest_snapshot_get.go | 42 +++ .../sdk/artifacts/latest_snapshot_get_test.go | 124 +++++++ internal/build/sdk/artifacts/manifest_get.go | 128 +++++++ .../build/sdk/artifacts/manifest_get_test.go | 196 +++++++++++ internal/build/sdk/artifacts/releases_list.go | 34 ++ .../build/sdk/artifacts/releases_list_test.go | 198 +++++++++++ .../sdk/artifacts/rest_resources_download.go | 167 +++++++++ .../artifacts/rest_resources_download_test.go | 332 ++++++++++++++++++ internal/build/sdk/ref/ref.go | 56 +++ internal/build/sdk/version/version.go | 135 +++++++ internal/build/sdk/version/version_test.go | 64 ++++ internal/build/utils/chromatize.go | 1 - internal/build/utils/debug.go | 1 - internal/build/utils/strings.go | 1 - internal/build/utils/terminal.go | 3 - 23 files changed, 1645 insertions(+), 339 deletions(-) delete mode 100644 internal/build/cmd/tools/commands/spec/command_test.go create mode 100644 internal/build/sdk/artifacts/client.go create mode 100644 internal/build/sdk/artifacts/latest_snapshot_get.go create mode 100644 internal/build/sdk/artifacts/latest_snapshot_get_test.go create mode 100644 internal/build/sdk/artifacts/manifest_get.go create mode 100644 internal/build/sdk/artifacts/manifest_get_test.go create mode 100644 internal/build/sdk/artifacts/releases_list.go create mode 100644 internal/build/sdk/artifacts/releases_list_test.go create mode 100644 internal/build/sdk/artifacts/rest_resources_download.go create mode 100644 internal/build/sdk/artifacts/rest_resources_download_test.go create mode 100644 internal/build/sdk/ref/ref.go create mode 100644 internal/build/sdk/version/version.go create mode 100644 internal/build/sdk/version/version_test.go diff --git a/internal/build/cmd/generate/commands/genexamples/model.go b/internal/build/cmd/generate/commands/genexamples/model.go index 12fb5dcdac..0b6ddbad6e 100644 --- a/internal/build/cmd/generate/commands/genexamples/model.go +++ b/internal/build/cmd/generate/commands/genexamples/model.go @@ -30,7 +30,6 @@ func init() { } // EnabledFiles contains a list of files where documentation should be generated. -// var EnabledFiles = []string{ "aggregations/bucket/datehistogram-aggregation.asciidoc", "aggregations/bucket/filter-aggregation.asciidoc", @@ -103,7 +102,6 @@ var ( // Example represents the code example in documentation. // // See: https://github.com/elastic/built-docs/blob/master/raw/en/elasticsearch/reference/master/alternatives_report.json -// type Example struct { SourceLocation struct { File string @@ -115,7 +113,6 @@ type Example struct { } // IsEnabled returns true when the example should be processed. -// func (e Example) IsEnabled() bool { // TODO(karmi): Use "filepatch.Match()" to support glob patterns @@ -129,38 +126,32 @@ func (e Example) IsEnabled() bool { } // IsExecutable returns true when the example contains a request. -// func (e Example) IsExecutable() bool { return reHTTPMethod.MatchString(e.Source) } // IsTranslated returns true when the example can be converted to Go source code. -// func (e Example) IsTranslated() bool { return Translator{Example: e}.IsTranslated() } // ID returns example identifier. -// func (e Example) ID() string { return fmt.Sprintf("%s:%d", e.SourceLocation.File, e.SourceLocation.Line) } // Chapter returns the example chapter. -// func (e Example) Chapter() string { r := strings.NewReplacer("/", "_", "-", "_", ".asciidoc", "") return r.Replace(e.SourceLocation.File) } // GithubURL returns a link for the example source. -// func (e Example) GithubURL() string { return fmt.Sprintf("https://github.com/elastic/elasticsearch/blob/master/docs/reference/%s#L%d", e.SourceLocation.File, e.SourceLocation.Line) } // Commands returns the list of commands from source. -// func (e Example) Commands() ([]string, error) { var ( buf strings.Builder @@ -200,7 +191,6 @@ func (e Example) Commands() ([]string, error) { } // Translated returns the code translated from Console to Go. -// func (e Example) Translated() (string, error) { return Translator{Example: e}.Translate() } diff --git a/internal/build/cmd/generate/commands/gensource/overrides.go b/internal/build/cmd/generate/commands/gensource/overrides.go index 0a8f5a7602..c78d89b30f 100644 --- a/internal/build/cmd/generate/commands/gensource/overrides.go +++ b/internal/build/cmd/generate/commands/gensource/overrides.go @@ -22,18 +22,15 @@ var ( ) // OverrideFunc defines a function to override generated code for endpoint. -// type OverrideFunc func(*Endpoint, ...interface{}) string // OverrideRule represents an override rule. -// type OverrideRule struct { Func OverrideFunc Matching []string } // GetOverride returns an override function for id and API name. -// func (g *Generator) GetOverride(id, apiName string) OverrideFunc { if rr, ok := overrideRules[id]; ok { for _, r := range rr { @@ -46,7 +43,6 @@ func (g *Generator) GetOverride(id, apiName string) OverrideFunc { } // Match returns true when API name matches a rule. -// func (r OverrideRule) Match(apiName string) bool { for _, v := range r.Matching { if v == "*" { diff --git a/internal/build/cmd/generate/commands/gentests/patches.go b/internal/build/cmd/generate/commands/gentests/patches.go index 6840547dc0..ed3056bea2 100644 --- a/internal/build/cmd/generate/commands/gentests/patches.go +++ b/internal/build/cmd/generate/commands/gentests/patches.go @@ -32,7 +32,6 @@ var ( ) // PatchTestSource performs a regex based patching of the input. -// func PatchTestSource(fpath string, r io.Reader) (io.Reader, error) { c, err := ioutil.ReadAll(r) if err != nil { diff --git a/internal/build/cmd/tools/commands/spec/command.go b/internal/build/cmd/tools/commands/spec/command.go index c268691198..26f50d53bf 100644 --- a/internal/build/cmd/tools/commands/spec/command.go +++ b/internal/build/cmd/tools/commands/spec/command.go @@ -18,21 +18,14 @@ package cmd import ( - "archive/zip" - "bytes" - "encoding/json" + "context" "fmt" - "io/ioutil" "log" - "net/http" - "net/url" "os" - "path/filepath" - "strings" - "time" "github.com/elastic/go-elasticsearch/v9/internal/build/cmd" - "github.com/elastic/go-elasticsearch/v9/internal/build/utils" + "github.com/elastic/go-elasticsearch/v9/internal/build/sdk/artifacts" + "github.com/elastic/go-elasticsearch/v9/internal/build/sdk/ref" "github.com/elastic/go-elasticsearch/v9/internal/version" "github.com/spf13/cobra" ) @@ -46,7 +39,7 @@ var ( func init() { output = toolsCmd.Flags().StringP("output", "o", "", "Path to a folder for generated output") - commitHash = toolsCmd.Flags().StringP("commit_hash", "c", "", "Elasticsearch commit hash") + commitHash = toolsCmd.Flags().StringP("commit_hash", "c", "", "Elasticsearch commit hash (deprecated: no longer supported)") debug = toolsCmd.Flags().BoolP("debug", "d", false, "Print the generated source to terminal") info = toolsCmd.Flags().Bool("info", false, "Print the API details to terminal") @@ -65,7 +58,7 @@ var toolsCmd = &cobra.Command{ } err := command.Execute() if err != nil { - utils.PrintErr(err) + log.Fatal(err) os.Exit(1) } }, @@ -78,251 +71,43 @@ type Command struct { Info bool } -// download-spec runs a query to the Elastic artifact API, retrieve the list of active artifacts -// downloads, extract and write to disk the rest-resources spec alongside a json with relevant build information. -func (c Command) Execute() (err error) { - const artifactsUrl = "https://artifacts-api.elastic.co/v1/versions" +// Execute downloads the rest-resources specification artifact using the artifacts API. +// It retrieves the spec for the version specified by ELASTICSEARCH_BUILD_VERSION env var, +// or falls back to the client version if not set. +func (c Command) Execute() error { + // Warn if deprecated --commit_hash flag is used + if c.CommitHash != "" { + log.Printf("Warning: --commit_hash flag is deprecated and no longer supported. It will be ignored.") + } esBuildVersion := os.Getenv("ELASTICSEARCH_BUILD_VERSION") if esBuildVersion == "" { esBuildVersion = version.Client } - versionUrl := strings.Join([]string{artifactsUrl, esBuildVersion}, "/") - - res, err := http.Get(versionUrl) - if err != nil { - log.Fatal(err.Error()) - } - defer res.Body.Close() - - if res.StatusCode == http.StatusNotFound { - // Try with version without -SNAPSHOT (e.g., "9.1.0-SNAPSHOT" -> "9.1.0") - fallbackVersion := esBuildVersion - if strings.HasSuffix(esBuildVersion, "-SNAPSHOT") { - fallbackVersion = strings.TrimSuffix(fallbackVersion, "-SNAPSHOT") - } else { - log.Fatalf("Version not found: %s returned 404", esBuildVersion) - } - - fallbackUrl := strings.Join([]string{artifactsUrl, fallbackVersion}, "/") - res, err = http.Get(fallbackUrl) - if err != nil { - log.Fatal(err.Error()) - } - defer res.Body.Close() - - if res.StatusCode == http.StatusNotFound { - log.Fatalf("Version not found: both %s and %s returned 404", esBuildVersion, fallbackVersion) - } - } - - var v Versions - dec := json.NewDecoder(res.Body) - err = dec.Decode(&v) - if err != nil { - log.Fatal(err.Error()) - } - - if c.Debug { - log.Printf("%d builds found", len(v.Version.Builds)) - } - - var build Build - if c.CommitHash != "" { - if build, err = findBuildByCommitHash(c.CommitHash, v.Version.Builds); err != nil { - build = findMostRecentBuild(v.Version.Builds) - } - } else { - build = findMostRecentBuild(v.Version.Builds) - } - if c.Debug { - log.Printf("Build found : %s", build.Projects.Elasticsearch.CommitHash) - } - - data, err := c.downloadZip(build) - if err != nil { - log.Fatalf("Cannot download zip from %s, reason : %s", build.zipfileUrl(), err) - } - - if err := c.extractZipToDest(data); err != nil { - log.Fatal(err.Error()) - } - - d, _ := json.Marshal(build) - - err = c.writeFileToDest("elasticsearch.json", d) - if err != nil { - log.Fatal(err.Error()) - } - - return nil -} - -func (c Command) writeFileToDest(filename string, data []byte) error { - path := filepath.Join(c.Output, filename) - if err := ioutil.WriteFile(path, data, 0644); err != nil { - return fmt.Errorf("cannot write file: %s", err) - } if c.Debug { - log.Printf("Successfuly written file to : %s", path) - } - return nil -} - -type Versions struct { - Version struct { - Builds []Build `json:"builds"` - } `json:"version"` -} - -type PackageUrl struct { - *url.URL -} - -func (p *PackageUrl) UnmarshalJSON(data []byte) error { - if string(data) == "null" { - return nil + log.Printf("Using version: %s", esBuildVersion) } - url, err := url.Parse(string(data[1 : len(data)-1])) - if err == nil { - p.URL = url - } - return err -} - -type BuildStartTime struct { - *time.Time -} - -func (t *BuildStartTime) UnmarshalJSON(data []byte) error { - if string(data) == "null" { - return nil - } - var err error - parsedTime, err := time.Parse(`"`+"Mon, 2 Jan 2006 15:04:05 MST"+`"`, string(data)) - t.Time = &parsedTime - return err -} - -type Build struct { - StartTime BuildStartTime `json:"start_time"` - Version string `json:"version"` - BuildId string `json:"build_id"` - Projects struct { - Elasticsearch struct { - Branch string `json:"branch"` - CommitHash string `json:"commit_hash"` - Packages map[string]struct { - Url PackageUrl `json:"url"` - Type string `json:"type"` - } - } `json:"elasticsearch"` - } `json:"projects"` -} - -func NewBuild() Build { - t := time.Date(1970, 0, 0, 0, 0, 0, 0, time.UTC) - startTime := BuildStartTime{Time: &t} - return Build{StartTime: startTime} -} -// zipfileUrl return the file URL for the rest-resources artifact from Build -// There should be only one artifact matching the requirements par Build. -func (b Build) zipfileUrl() string { - for _, pack := range b.Projects.Elasticsearch.Packages { - if pack.Type == "zip" && strings.Contains(pack.Url.String(), "rest-resources") { - return pack.Url.String() + // Parse the version/branch reference, renaming "main" to "master" for branch lookups + r, err := ref.Parse(esBuildVersion, func(s string) string { + if s == "main" { + return "master" } - } - return "" -} - -// extractZipToDest extract the data from a previously downloaded file loaded in memory to Output target. -func (c Command) extractZipToDest(data []byte) error { - zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) + return s + }) if err != nil { - return err + return fmt.Errorf("invalid version %q: %w", esBuildVersion, err) } - if err = os.MkdirAll(c.Output, 0744); err != nil { - return fmt.Errorf("cannot created destination directory: %s", err) - } - - for _, file := range zipReader.File { - f, err := file.Open() - if err != nil { - return fmt.Errorf("cannot read file in zipfile: %s", err) - } - defer f.Close() - - if file.FileInfo().IsDir() { - path := filepath.Join(c.Output, file.Name) - _ = os.MkdirAll(path, 0744) - } else { - data, err := ioutil.ReadAll(f) - if err != nil { - return err - } - - if err := c.writeFileToDest(file.Name, data); err != nil { - return err - } - } + client := artifacts.NewClient() + if err := client.DownloadRestResources(context.Background(), r, c.Output); err != nil { + return fmt.Errorf("failed to download rest-resources: %w", err) } if c.Debug { - log.Printf("Zipfile successfully extracted to %s", c.Output) + log.Printf("Successfully downloaded rest-resources to %s", c.Output) } return nil } - -// downloadZip fetches the rest-resources artifact from a Build and return its content as []byte. -func (c Command) downloadZip(b Build) ([]byte, error) { - url := b.zipfileUrl() - if c.Debug { - log.Printf("Zipfile url : %s", b.zipfileUrl()) - } - - client := &http.Client{} - - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - return nil, err - } - - req.Header.Add("Accept-Content", "gzip") - res, err := client.Do(req) - if err != nil { - return nil, err - } - defer res.Body.Close() - - data, _ := ioutil.ReadAll(res.Body) - return data, err -} - -// findMostRecentBuild iterates through the builds retrieved from the api -// and return the latest one based on the StartTime of each Build. -func findMostRecentBuild(builds []Build) Build { - var latestBuild Build - latestBuild = NewBuild() - for _, build := range builds { - if build.StartTime.After(*latestBuild.StartTime.Time) { - latestBuild = build - } - } - return latestBuild -} - -// findBuildByCommitHash iterates through the builds and returns the first occurrence of Build -// that matches the provided commitHash. -func findBuildByCommitHash(commitHash string, builds []Build) (Build, error) { - for _, build := range builds { - if build.Projects.Elasticsearch.CommitHash == commitHash { - return build, nil - } - } - return Build{}, fmt.Errorf("Build with commit hash %s not found", commitHash) -} diff --git a/internal/build/cmd/tools/commands/spec/command_test.go b/internal/build/cmd/tools/commands/spec/command_test.go deleted file mode 100644 index c4c5e66637..0000000000 --- a/internal/build/cmd/tools/commands/spec/command_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// Licensed to Elasticsearch B.V. under one or more contributor -// license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright -// ownership. Elasticsearch B.V. licenses this file to you under -// the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package cmd - -import ( - "encoding/json" - "testing" -) - -func TestBuild_zipfileUrl(t *testing.T) { - tests := []struct { - name string - json string - want string - }{ - { - name: "Simple test with valid url", - json: ` - { - "start_time": "Mon, 19 Apr 2021 12:15:47 GMT", - "version": "8.0.0-SNAPSHOT", - "build_id": "8.0.0-ab7cd914", - "projects": { - "elasticsearch": { - "branch": "master", - "commit_hash": "d3be79018b5b70a118ea5a897a539428b728df5a", - "Packages": { - "rest-resources-zip-8.0.0-SNAPSHOT.zip": { - "url": "https://snapshots.elastic.co/8.0.0-ab7cd914/downloads/elasticsearch/rest-resources-zip-8.0.0-SNAPSHOT.zip", - "type": "zip" - } - } - } - } - } - `, - want: "https://snapshots.elastic.co/8.0.0-ab7cd914/downloads/elasticsearch/rest-resources-zip-8.0.0-SNAPSHOT.zip", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - b := Build{} - if err := json.Unmarshal([]byte(tt.json), &b); err != nil { - t.Fatalf(err.Error()) - } - if got := b.zipfileUrl(); got != tt.want { - t.Errorf("zipfileUrl() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/internal/build/go.mod b/internal/build/go.mod index ef78599a26..6ba76771e9 100644 --- a/internal/build/go.mod +++ b/internal/build/go.mod @@ -9,18 +9,22 @@ replace github.com/elastic/go-elasticsearch/v9 => ../../ require ( github.com/alecthomas/chroma v0.10.0 github.com/elastic/go-elasticsearch/v9 v9.0.0-00010101000000-000000000000 + github.com/hashicorp/go-retryablehttp v0.7.8 + github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.8.0 golang.org/x/crypto v0.37.0 - golang.org/x/tools v0.32.0 + golang.org/x/tools v0.35.0 gopkg.in/yaml.v2 v2.4.0 ) require ( github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/sync v0.13.0 // indirect - golang.org/x/sys v0.32.0 // indirect + golang.org/x/mod v0.26.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.34.0 // indirect golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.28.0 // indirect ) diff --git a/internal/build/go.sum b/internal/build/go.sum index 0028607c15..fc362bc8fb 100644 --- a/internal/build/go.sum +++ b/internal/build/go.sum @@ -6,13 +6,27 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -22,16 +36,18 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= -golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= -golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/internal/build/sdk/artifacts/client.go b/internal/build/sdk/artifacts/client.go new file mode 100644 index 0000000000..ed60dd597b --- /dev/null +++ b/internal/build/sdk/artifacts/client.go @@ -0,0 +1,112 @@ +package artifacts + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/hashicorp/go-retryablehttp" + "github.com/spf13/afero" +) + +const ( + defaultReleasesBaseURL = "https://artifacts.elastic.co/" + defaultSnapshotBaseURL = "https://artifacts-snapshot.elastic.co/" +) + +type config struct { + releasesBaseURL string + snapshotBaseURL string + fs afero.Fs +} + +// Client is a client for interacting with the Elastic Artifacts API. +type Client struct { + releasesBaseURL string + snapshotBaseURL string + client *retryablehttp.Client + fs afero.Fs +} + +// Option is a functional option for configuring a Client. +type Option func(*config) + +// WithReleasesBaseURL sets the base URL for release artifacts. +func WithReleasesBaseURL(url string) Option { + return func(c *config) { + c.releasesBaseURL = url + } +} + +// WithSnapshotBaseURL sets the base URL for snapshot artifacts. +func WithSnapshotBaseURL(url string) Option { + return func(c *config) { + c.snapshotBaseURL = url + } +} + +// WithFS sets the filesystem to use for downloading artifacts. +// Defaults to afero.NewOsFs(). +// This is useful for testing. +func WithFS(fs afero.Fs) Option { + return func(c *config) { + c.fs = fs + } +} + +// NewClient returns a new Client. +func NewClient(options ...Option) *Client { + c := &config{ + releasesBaseURL: defaultReleasesBaseURL, + snapshotBaseURL: defaultSnapshotBaseURL, + fs: afero.NewOsFs(), + } + for _, opt := range options { + opt(c) + } + return &Client{ + releasesBaseURL: c.releasesBaseURL, + snapshotBaseURL: c.snapshotBaseURL, + client: retryablehttp.NewClient(), + fs: c.fs, + } +} + +func doGet[T any](ctx context.Context, c *Client, url string) (*T, error) { + request, err := retryablehttp.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + resp, err := c.client.Do(request.WithContext(ctx)) + if err != nil { + return nil, err + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != 200 { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + var response T + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return nil, err + } + return &response, nil +} + +func doDownload(ctx context.Context, c *Client, url string) ([]byte, error) { + request, err := retryablehttp.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + request.Header.Add("Accept-Encoding", "gzip") + resp, err := c.client.Do(request.WithContext(ctx)) + if err != nil { + return nil, err + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != 200 { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + return io.ReadAll(resp.Body) +} diff --git a/internal/build/sdk/artifacts/latest_snapshot_get.go b/internal/build/sdk/artifacts/latest_snapshot_get.go new file mode 100644 index 0000000000..8adfc64ce6 --- /dev/null +++ b/internal/build/sdk/artifacts/latest_snapshot_get.go @@ -0,0 +1,42 @@ +package artifacts + +import ( + "context" + "fmt" + "net/url" +) + +// GetLatestSnapshotRequest represents the request parameters for the GetLatestSnapshot API. +type GetLatestSnapshotRequest struct { + Branch string +} + +// Validate checks the request parameters for errors. +func (r GetLatestSnapshotRequest) Validate() error { + if len(r.Branch) == 0 { + return fmt.Errorf("branch is required") + } + return nil +} + +// GetLatestSnapshotResponse represents the response from the GetLatestSnapshot API. +type GetLatestSnapshotResponse struct { + Version string `json:"version"` + BuildID string `json:"build_id"` + ManifestURL string `json:"manifest_url"` + SummaryURL string `json:"summary_url"` +} + +// GetLatestSnapshot gets the latest snapshot build for a given branch. +func (c *Client) GetLatestSnapshot(ctx context.Context, req *GetLatestSnapshotRequest) (*GetLatestSnapshotResponse, error) { + if err := req.Validate(); err != nil { + return nil, err + } + + requestUrl, err := url.JoinPath(c.snapshotBaseURL, fmt.Sprintf("elasticsearch/latest/%s.json", req.Branch)) + if err != nil { + return nil, err + } + + return doGet[GetLatestSnapshotResponse](ctx, c, requestUrl) +} diff --git a/internal/build/sdk/artifacts/latest_snapshot_get_test.go b/internal/build/sdk/artifacts/latest_snapshot_get_test.go new file mode 100644 index 0000000000..b191defdcb --- /dev/null +++ b/internal/build/sdk/artifacts/latest_snapshot_get_test.go @@ -0,0 +1,124 @@ +package artifacts + +import ( + "context" + "errors" + "io" + "net/http" + "reflect" + "strings" + "testing" + + "github.com/hashicorp/go-retryablehttp" +) + +type mockRoundTripper struct { + RoundTripFunc func(*http.Request) (*http.Response, error) +} + +func (m mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if m.RoundTripFunc == nil { + return nil, errors.New("the RoundTripFunc of the mock is nil") + } + return m.RoundTripFunc(req) +} + +func TestClient_GetLatestInfo(t *testing.T) { + type fields struct { + baseURL string + client *retryablehttp.Client + } + type args struct { + ctx context.Context + req *GetLatestSnapshotRequest + } + tests := []struct { + name string + fields fields + args args + want *GetLatestSnapshotResponse + wantErr bool + }{ + { + name: "success", + fields: fields{ + baseURL: defaultSnapshotBaseURL, + client: func() *retryablehttp.Client { + c := retryablehttp.NewClient() + c.Logger = nil + c.HTTPClient.Transport = &mockRoundTripper{ + RoundTripFunc: func(r *http.Request) (*http.Response, error) { + return &http.Response{ + Request: r.Clone(context.Background()), + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{ + "version": "9.3.0-SNAPSHOT", + "build_id": "9.3.0-dd19c56f", + "manifest_url": "https://artifacts-snapshot.elastic.co/elasticsearch/9.3.0-dd19c56f/manifest-9.3.0-SNAPSHOT.json", + "summary_url": "https://artifacts-snapshot.elastic.co/elasticsearch/9.3.0-dd19c56f/summary-9.3.0-SNAPSHOT.html" +}`)), + }, nil + }, + } + return c + }(), + }, + args: args{ + ctx: context.Background(), + req: &GetLatestSnapshotRequest{ + Branch: "master", + }, + }, + wantErr: false, + want: &GetLatestSnapshotResponse{ + Version: "9.3.0-SNAPSHOT", + BuildID: "9.3.0-dd19c56f", + ManifestURL: "https://artifacts-snapshot.elastic.co/elasticsearch/9.3.0-dd19c56f/manifest-9.3.0-SNAPSHOT.json", + SummaryURL: "https://artifacts-snapshot.elastic.co/elasticsearch/9.3.0-dd19c56f/summary-9.3.0-SNAPSHOT.html", + }, + }, + { + name: "404", + fields: fields{ + baseURL: defaultSnapshotBaseURL, + client: func() *retryablehttp.Client { + c := retryablehttp.NewClient() + c.Logger = nil + c.HTTPClient.Transport = &mockRoundTripper{ + RoundTripFunc: func(r *http.Request) (*http.Response, error) { + return &http.Response{ + Request: r.Clone(context.Background()), + StatusCode: http.StatusNotFound, + Body: io.NopCloser(strings.NewReader(`Not Found`)), + }, nil + }, + } + return c + }(), + }, + args: args{ + ctx: context.Background(), + req: &GetLatestSnapshotRequest{ + Branch: "master", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Client{ + snapshotBaseURL: tt.fields.baseURL, + client: tt.fields.client, + } + got, err := c.GetLatestSnapshot(tt.args.ctx, tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("GetLatestSnapshot() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetLatestSnapshot() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/build/sdk/artifacts/manifest_get.go b/internal/build/sdk/artifacts/manifest_get.go new file mode 100644 index 0000000000..7b9ec3f4ef --- /dev/null +++ b/internal/build/sdk/artifacts/manifest_get.go @@ -0,0 +1,128 @@ +package artifacts + +import ( + "context" + "fmt" + "strings" +) + +// GetManifestRequest represents the request parameters for the GetManifest API. +type GetManifestRequest struct { + URL string +} + +// Validate checks the request parameters for errors. +func (r GetManifestRequest) Validate() error { + if len(r.URL) == 0 { + return fmt.Errorf("url is required") + } + + return nil +} + +// Package represents a package in the manifest. +type Package struct { + URL string `json:"url"` + ShaURL string `json:"sha_url"` + Type string `json:"type"` + Architecture string `json:"architecture"` + OS []string `json:"os"` +} + +// Dependency represents a dependency in the manifest. +type Dependency struct { + Prefix string `json:"prefix"` + BuildURI string `json:"build_uri"` +} + +// Project represents a project in the manifest. +type Project struct { + Branch string `json:"branch"` + CommitHash string `json:"commit_hash"` + CommitURL string `json:"commit_url"` + BuildDurationSeconds int `json:"build_duration_seconds"` + Packages map[string]Package `json:"packages"` + Dependencies []Dependency `json:"dependencies"` +} + +// Manifest represents the manifest for a build. +type Manifest struct { + Branch string `json:"branch"` + ReleaseBranch string `json:"release_branch"` + Version string `json:"version"` + BuildID string `json:"build_id"` + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` + BuildDurationSeconds int `json:"build_duration_seconds"` + ManifestVersion string `json:"manifest_version"` + Prefix string `json:"prefix"` + Projects struct { + Elasticsearch Project `json:"elasticsearch"` + } `json:"projects"` +} + +// GetManifestResponse represents the response from the GetManifest API. +type GetManifestResponse struct { + Manifest *Manifest +} + +// Build represents the legacy build format for backward compatibility with elasticsearch.json +type Build struct { + StartTime string `json:"start_time"` + Version string `json:"version"` + BuildID string `json:"build_id"` + Projects struct { + Elasticsearch BuildProject `json:"elasticsearch"` + } `json:"projects"` +} + +// BuildProject represents the legacy build project format for backward compatibility with elasticsearch.json +type BuildProject struct { + Branch string `json:"branch"` + CommitHash string `json:"commit_hash"` + Packages map[string]BuildPackage `json:"packages"` +} + +// BuildPackage represents the legacy build package format for backward compatibility with elasticsearch.json +type BuildPackage struct { + URL string `json:"url"` + Type string `json:"type"` +} + +// ToBuild converts a Manifest to the legacy Build format for backward compatibility +func (m *Manifest) ToBuild() Build { + var build Build + + build.StartTime = m.StartTime + build.Version = m.Version + build.BuildID = m.BuildID + build.Projects.Elasticsearch.Branch = m.Projects.Elasticsearch.Branch + build.Projects.Elasticsearch.CommitHash = m.Projects.Elasticsearch.CommitHash + build.Projects.Elasticsearch.Packages = make(map[string]BuildPackage) + + for key, pkg := range m.Projects.Elasticsearch.Packages { + build.Projects.Elasticsearch.Packages[key] = BuildPackage{ + URL: pkg.URL, + Type: pkg.Type, + } + } + + return build +} + +// GetManifest retrieves the manifest for a build. +func (c *Client) GetManifest(ctx context.Context, req *GetManifestRequest) (*GetManifestResponse, error) { + if err := req.Validate(); err != nil { + return nil, err + } + + if !strings.HasPrefix(req.URL, c.snapshotBaseURL) && !strings.HasPrefix(req.URL, c.releasesBaseURL) { + return nil, fmt.Errorf("invalid url: %s, must be child of: %s or %s", req.URL, c.snapshotBaseURL, c.releasesBaseURL) + } + + manifest, err := doGet[Manifest](ctx, c, req.URL) + if err != nil { + return nil, err + } + return &GetManifestResponse{Manifest: manifest}, nil +} diff --git a/internal/build/sdk/artifacts/manifest_get_test.go b/internal/build/sdk/artifacts/manifest_get_test.go new file mode 100644 index 0000000000..c9309344b5 --- /dev/null +++ b/internal/build/sdk/artifacts/manifest_get_test.go @@ -0,0 +1,196 @@ +package artifacts + +import ( + "context" + "io" + "net/http" + "reflect" + "strings" + "testing" + + "github.com/hashicorp/go-retryablehttp" +) + +func TestClient_GetManifest(t *testing.T) { + type fields struct { + snapshotBaseURL string + releasesBaseURL string + client *retryablehttp.Client + } + type args struct { + ctx context.Context + req *GetManifestRequest + } + tests := []struct { + name string + fields fields + args args + want *GetManifestResponse + wantErr bool + }{ + { + name: "success", + fields: fields{ + snapshotBaseURL: defaultSnapshotBaseURL, + releasesBaseURL: defaultReleasesBaseURL, + client: func() *retryablehttp.Client { + c := retryablehttp.NewClient() + c.Logger = nil + c.HTTPClient.Transport = &mockRoundTripper{ + RoundTripFunc: func(r *http.Request) (*http.Response, error) { + return &http.Response{ + Request: r.Clone(context.Background()), + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{ + "branch": "main", + "release_branch": "main", + "version": "9.3.0-SNAPSHOT", + "build_id": "9.3.0-abc123", + "start_time": "2025-05-22T12:43:21+00:00", + "end_time": "2025-05-22T13:00:00+00:00", + "build_duration_seconds": 1000, + "manifest_version": "1.0", + "prefix": "elasticsearch", + "projects": { + "elasticsearch": { + "branch": "main", + "commit_hash": "abc123def456", + "commit_url": "https://github.com/elastic/elasticsearch/commit/abc123def456", + "build_duration_seconds": 500, + "packages": { + "rest-resources-zip-9.3.0-SNAPSHOT.zip": { + "url": "https://artifacts-snapshot.elastic.co/elasticsearch/rest-resources.zip", + "sha_url": "https://artifacts-snapshot.elastic.co/elasticsearch/rest-resources.zip.sha512", + "type": "zip", + "architecture": "any", + "os": ["any"] + } + }, + "dependencies": [] + } + } +}`)), + }, nil + }, + } + return c + }(), + }, + args: args{ + ctx: context.Background(), + req: &GetManifestRequest{ + URL: "https://artifacts-snapshot.elastic.co/elasticsearch/9.3.0-abc123/manifest.json", + }, + }, + wantErr: false, + want: &GetManifestResponse{ + Manifest: &Manifest{ + Branch: "main", + ReleaseBranch: "main", + Version: "9.3.0-SNAPSHOT", + BuildID: "9.3.0-abc123", + StartTime: "2025-05-22T12:43:21+00:00", + EndTime: "2025-05-22T13:00:00+00:00", + BuildDurationSeconds: 1000, + ManifestVersion: "1.0", + Prefix: "elasticsearch", + Projects: struct { + Elasticsearch Project `json:"elasticsearch"` + }{ + Elasticsearch: Project{ + Branch: "main", + CommitHash: "abc123def456", + CommitURL: "https://github.com/elastic/elasticsearch/commit/abc123def456", + BuildDurationSeconds: 500, + Packages: map[string]Package{ + "rest-resources-zip-9.3.0-SNAPSHOT.zip": { + URL: "https://artifacts-snapshot.elastic.co/elasticsearch/rest-resources.zip", + ShaURL: "https://artifacts-snapshot.elastic.co/elasticsearch/rest-resources.zip.sha512", + Type: "zip", + Architecture: "any", + OS: []string{"any"}, + }, + }, + Dependencies: []Dependency{}, + }, + }, + }, + }, + }, + { + name: "404", + fields: fields{ + snapshotBaseURL: defaultSnapshotBaseURL, + releasesBaseURL: defaultReleasesBaseURL, + client: func() *retryablehttp.Client { + c := retryablehttp.NewClient() + c.Logger = nil + c.HTTPClient.Transport = &mockRoundTripper{ + RoundTripFunc: func(r *http.Request) (*http.Response, error) { + return &http.Response{ + Request: r.Clone(context.Background()), + StatusCode: http.StatusNotFound, + Body: io.NopCloser(strings.NewReader(`Not Found`)), + }, nil + }, + } + return c + }(), + }, + args: args{ + ctx: context.Background(), + req: &GetManifestRequest{ + URL: "https://artifacts-snapshot.elastic.co/elasticsearch/not-found/manifest.json", + }, + }, + wantErr: true, + }, + { + name: "validation error - empty URL", + fields: fields{ + snapshotBaseURL: defaultSnapshotBaseURL, + releasesBaseURL: defaultReleasesBaseURL, + client: retryablehttp.NewClient(), + }, + args: args{ + ctx: context.Background(), + req: &GetManifestRequest{ + URL: "", + }, + }, + wantErr: true, + }, + { + name: "invalid URL - not child of base URLs", + fields: fields{ + snapshotBaseURL: defaultSnapshotBaseURL, + releasesBaseURL: defaultReleasesBaseURL, + client: retryablehttp.NewClient(), + }, + args: args{ + ctx: context.Background(), + req: &GetManifestRequest{ + URL: "https://malicious-site.com/manifest.json", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Client{ + snapshotBaseURL: tt.fields.snapshotBaseURL, + releasesBaseURL: tt.fields.releasesBaseURL, + client: tt.fields.client, + } + got, err := c.GetManifest(tt.args.ctx, tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("GetManifest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetManifest() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/build/sdk/artifacts/releases_list.go b/internal/build/sdk/artifacts/releases_list.go new file mode 100644 index 0000000000..9133170d8c --- /dev/null +++ b/internal/build/sdk/artifacts/releases_list.go @@ -0,0 +1,34 @@ +package artifacts + +import ( + "context" + "net/url" +) + +// Release represents a release in the Stack API. +type Release struct { + Version string `json:"version"` + PublicReleaseDate *string `json:"public_release_date"` + IsEndOfSupport *bool `json:"is_end_of_support"` + EndOfSupportDate *string `json:"end_of_support_date"` + IsEndOfMaintenance *bool `json:"is_end_of_maintenance"` + EndOfMaintenanceDate *string `json:"end_of_maintenance_date"` + IsRetired *bool `json:"is_retired"` + RetiredDate *string `json:"retired_date"` + Manifest *string `json:"manifest"` +} + +// ListReleasesResponse represents the response from the ListReleases API. +type ListReleasesResponse struct { + Releases []Release `json:"releases"` +} + +// ListReleases lists all releases. +func (c *Client) ListReleases(ctx context.Context) (*ListReleasesResponse, error) { + requestUrl, err := url.JoinPath(c.releasesBaseURL, "releases", "stack.json") + if err != nil { + return nil, err + } + + return doGet[ListReleasesResponse](ctx, c, requestUrl) +} diff --git a/internal/build/sdk/artifacts/releases_list_test.go b/internal/build/sdk/artifacts/releases_list_test.go new file mode 100644 index 0000000000..b8cd8e9c06 --- /dev/null +++ b/internal/build/sdk/artifacts/releases_list_test.go @@ -0,0 +1,198 @@ +package artifacts + +import ( + "context" + "io" + "net/http" + "reflect" + "strings" + "testing" + + "github.com/hashicorp/go-retryablehttp" +) + +func ptr[T any](v T) *T { + return &v +} + +func TestClient_ListReleases(t *testing.T) { + type fields struct { + releasesBaseURL string + client *retryablehttp.Client + } + type args struct { + ctx context.Context + } + tests := []struct { + name string + fields fields + args args + want *ListReleasesResponse + wantErr bool + }{ + { + name: "success with multiple releases", + fields: fields{ + releasesBaseURL: defaultReleasesBaseURL, + client: func() *retryablehttp.Client { + c := retryablehttp.NewClient() + c.Logger = nil + c.HTTPClient.Transport = &mockRoundTripper{ + RoundTripFunc: func(r *http.Request) (*http.Response, error) { + return &http.Response{ + Request: r.Clone(context.Background()), + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{ + "releases": [ + { + "version": "8.15.0", + "public_release_date": "2024-08-08", + "is_end_of_support": false, + "end_of_support_date": "2026-02-08", + "is_end_of_maintenance": false, + "end_of_maintenance_date": "2025-02-08", + "is_retired": false, + "retired_date": null, + "manifest": "https://artifacts.elastic.co/releases/8.15.0/manifest.json" + }, + { + "version": "8.14.3", + "public_release_date": "2024-07-10", + "is_end_of_support": false, + "end_of_support_date": "2026-01-10", + "is_end_of_maintenance": true, + "end_of_maintenance_date": "2024-08-08", + "is_retired": false, + "retired_date": null, + "manifest": "https://artifacts.elastic.co/releases/8.14.3/manifest.json" + }, + { + "version": "7.17.22", + "public_release_date": "2024-06-01", + "is_end_of_support": true, + "end_of_support_date": "2023-08-01", + "is_end_of_maintenance": true, + "end_of_maintenance_date": "2023-02-01", + "is_retired": true, + "retired_date": "2024-01-01", + "manifest": null + } + ] +}`)), + }, nil + }, + } + return c + }(), + }, + args: args{ + ctx: context.Background(), + }, + wantErr: false, + want: &ListReleasesResponse{ + Releases: []Release{ + { + Version: "8.15.0", + PublicReleaseDate: ptr("2024-08-08"), + IsEndOfSupport: ptr(false), + EndOfSupportDate: ptr("2026-02-08"), + IsEndOfMaintenance: ptr(false), + EndOfMaintenanceDate: ptr("2025-02-08"), + IsRetired: ptr(false), + RetiredDate: nil, + Manifest: ptr("https://artifacts.elastic.co/releases/8.15.0/manifest.json"), + }, + { + Version: "8.14.3", + PublicReleaseDate: ptr("2024-07-10"), + IsEndOfSupport: ptr(false), + EndOfSupportDate: ptr("2026-01-10"), + IsEndOfMaintenance: ptr(true), + EndOfMaintenanceDate: ptr("2024-08-08"), + IsRetired: ptr(false), + RetiredDate: nil, + Manifest: ptr("https://artifacts.elastic.co/releases/8.14.3/manifest.json"), + }, + { + Version: "7.17.22", + PublicReleaseDate: ptr("2024-06-01"), + IsEndOfSupport: ptr(true), + EndOfSupportDate: ptr("2023-08-01"), + IsEndOfMaintenance: ptr(true), + EndOfMaintenanceDate: ptr("2023-02-01"), + IsRetired: ptr(true), + RetiredDate: ptr("2024-01-01"), + Manifest: nil, + }, + }, + }, + }, + { + name: "404", + fields: fields{ + releasesBaseURL: defaultReleasesBaseURL, + client: func() *retryablehttp.Client { + c := retryablehttp.NewClient() + c.Logger = nil + c.HTTPClient.Transport = &mockRoundTripper{ + RoundTripFunc: func(r *http.Request) (*http.Response, error) { + return &http.Response{ + Request: r.Clone(context.Background()), + StatusCode: http.StatusNotFound, + Body: io.NopCloser(strings.NewReader(`Not Found`)), + }, nil + }, + } + return c + }(), + }, + args: args{ + ctx: context.Background(), + }, + wantErr: true, + }, + { + name: "empty releases array", + fields: fields{ + releasesBaseURL: defaultReleasesBaseURL, + client: func() *retryablehttp.Client { + c := retryablehttp.NewClient() + c.Logger = nil + c.HTTPClient.Transport = &mockRoundTripper{ + RoundTripFunc: func(r *http.Request) (*http.Response, error) { + return &http.Response{ + Request: r.Clone(context.Background()), + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"releases": []}`)), + }, nil + }, + } + return c + }(), + }, + args: args{ + ctx: context.Background(), + }, + wantErr: false, + want: &ListReleasesResponse{ + Releases: []Release{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Client{ + releasesBaseURL: tt.fields.releasesBaseURL, + client: tt.fields.client, + } + got, err := c.ListReleases(tt.args.ctx) + if (err != nil) != tt.wantErr { + t.Errorf("ListReleases() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ListReleases() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/build/sdk/artifacts/rest_resources_download.go b/internal/build/sdk/artifacts/rest_resources_download.go new file mode 100644 index 0000000000..c45fdd6b3b --- /dev/null +++ b/internal/build/sdk/artifacts/rest_resources_download.go @@ -0,0 +1,167 @@ +package artifacts + +import ( + "archive/zip" + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "path/filepath" + "strings" + + "github.com/elastic/go-elasticsearch/v9/internal/build/sdk/ref" +) + +// DownloadRestResources downloads the rest-resources zip file for the given Elasticsearch version +// and extracts it to the destination directory. +// It also writes elasticsearch.json with build info for backward compatibility. +func (c *Client) DownloadRestResources(ctx context.Context, ref ref.Ref, dest string) error { + if ref.IsEmpty() { + return errors.New("empty ref") + } + if dest == "" { + return errors.New("empty destination") + } + + var manifestURL string + + if ref.IsPreRelease() { + latestSnapshot, err := c.GetLatestSnapshot(ctx, &GetLatestSnapshotRequest{Branch: ref.TargetBranch()}) + if err != nil { + return fmt.Errorf("cannot get latest snapshot: %w", err) + } + manifestURL = latestSnapshot.ManifestURL + } else { + releases, err := c.ListReleases(ctx) + if err != nil { + return fmt.Errorf("cannot list releases: %w", err) + } + var release *Release + for i := range releases.Releases { + if releases.Releases[i].Version == ref.String() { + release = &releases.Releases[i] + break + } + } + if release == nil { + return fmt.Errorf("release %s not found", ref.String()) + } + if release.Manifest == nil { + return fmt.Errorf("release %s does not have a manifest", ref.String()) + } + manifestURL = *release.Manifest + } + + manifest, err := c.GetManifest(ctx, &GetManifestRequest{URL: manifestURL}) + if err != nil { + return fmt.Errorf("cannot get manifest: %w", err) + } + + elasticsearchProject := manifest.Manifest.Projects.Elasticsearch + + var zipURL string + + for key, value := range elasticsearchProject.Packages { + if strings.Contains(key, "rest-resources") { + zipURL = value.URL + break + } + } + + if zipURL == "" { + return errors.New("rest-resources package not found in manifest") + } + + // Download the zip file + zipData, err := doDownload(ctx, c, zipURL) + if err != nil { + return fmt.Errorf("cannot download rest-resources zip: %w", err) + } + + // Extract zip to destination + if err := c.extractZipToDestination(zipData, dest); err != nil { + return fmt.Errorf("cannot extract zip: %w", err) + } + + // Write elasticsearch.json with build info for backward compatibility + build := manifest.Manifest.ToBuild() + buildJSON, err := json.MarshalIndent(build, "", " ") + if err != nil { + return fmt.Errorf("cannot marshal build info: %w", err) + } + + if err := c.writeFile(filepath.Join(dest, "elasticsearch.json"), buildJSON); err != nil { + return fmt.Errorf("cannot write elasticsearch.json: %w", err) + } + + return nil +} + +// extractZipToDestination extracts zip data to the destination directory using the client's filesystem +func (c *Client) extractZipToDestination(data []byte, dest string) error { + zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) + if err != nil { + return fmt.Errorf("cannot read zip: %w", err) + } + + if err := c.fs.MkdirAll(dest, 0755); err != nil { + return fmt.Errorf("cannot create destination directory: %w", err) + } + + for _, file := range zipReader.File { + destPath := filepath.Join(dest, file.Name) + + if file.FileInfo().IsDir() { + if err := c.fs.MkdirAll(destPath, 0755); err != nil { + return fmt.Errorf("cannot create directory %s: %w", destPath, err) + } + continue + } + + // Ensure parent directory exists + if err := c.fs.MkdirAll(filepath.Dir(destPath), 0755); err != nil { + return fmt.Errorf("cannot create parent directory for %s: %w", destPath, err) + } + + if err := c.extractZipFile(file, destPath); err != nil { + return err + } + } + + return nil +} + +// extractZipFile extracts a single file from the zip archive +func (c *Client) extractZipFile(file *zip.File, destPath string) error { + src, err := file.Open() + if err != nil { + return fmt.Errorf("cannot open file in zip: %w", err) + } + defer func() { _ = src.Close() }() + + dst, err := c.fs.Create(destPath) + if err != nil { + return fmt.Errorf("cannot create file %s: %w", destPath, err) + } + defer func() { _ = dst.Close() }() + + if _, err := io.Copy(dst, src); err != nil { + return fmt.Errorf("cannot write file %s: %w", destPath, err) + } + + return nil +} + +// writeFile writes data to a file using the client's filesystem +func (c *Client) writeFile(path string, data []byte) error { + f, err := c.fs.Create(path) + if err != nil { + return err + } + defer func() { _ = f.Close() }() + + _, err = f.Write(data) + return err +} diff --git a/internal/build/sdk/artifacts/rest_resources_download_test.go b/internal/build/sdk/artifacts/rest_resources_download_test.go new file mode 100644 index 0000000000..c7439a257d --- /dev/null +++ b/internal/build/sdk/artifacts/rest_resources_download_test.go @@ -0,0 +1,332 @@ +package artifacts + +import ( + "archive/zip" + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "strings" + "testing" + + "github.com/elastic/go-elasticsearch/v9/internal/build/sdk/ref" + "github.com/hashicorp/go-retryablehttp" + "github.com/spf13/afero" +) + +// createTestZip creates a zip file in memory with the given files +func createTestZip(files map[string]string) ([]byte, error) { + buf := new(bytes.Buffer) + w := zip.NewWriter(buf) + + for name, content := range files { + f, err := w.Create(name) + if err != nil { + return nil, err + } + if _, err := f.Write([]byte(content)); err != nil { + return nil, err + } + } + + if err := w.Close(); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func TestClient_DownloadRestResources(t *testing.T) { + // Create test zip content + testZipFiles := map[string]string{ + "rest-api-spec/api/search.json": `{"name": "search"}`, + "rest-api-spec/api/index.json": `{"name": "index"}`, + "rest-api-spec/test/test1.yaml": "test: 1", + } + testZipData, err := createTestZip(testZipFiles) + if err != nil { + t.Fatalf("failed to create test zip: %v", err) + } + + tests := []struct { + name string + ref string + dest string + responses map[string]string + wantErr bool + wantErrMsg string + wantFiles []string + }{ + { + name: "success with snapshot", + ref: "main", + dest: "/output", + responses: map[string]string{ + "https://artifacts-snapshot.elastic.co/elasticsearch/latest/main.json": `{ + "version": "9.3.0-SNAPSHOT", + "build_id": "9.3.0-abc123", + "manifest_url": "https://artifacts-snapshot.elastic.co/elasticsearch/9.3.0-abc123/manifest-9.3.0-SNAPSHOT.json", + "summary_url": "https://artifacts-snapshot.elastic.co/elasticsearch/9.3.0-abc123/summary.html" + }`, + "https://artifacts-snapshot.elastic.co/elasticsearch/9.3.0-abc123/manifest-9.3.0-SNAPSHOT.json": `{ + "branch": "main", + "version": "9.3.0-SNAPSHOT", + "build_id": "9.3.0-abc123", + "start_time": "2025-05-22T12:43:21+00:00", + "projects": { + "elasticsearch": { + "branch": "main", + "commit_hash": "abc123def456", + "packages": { + "rest-resources-zip-9.3.0-SNAPSHOT.zip": { + "url": "https://artifacts-snapshot.elastic.co/elasticsearch/rest-resources.zip", + "type": "zip" + } + } + } + } + }`, + }, + wantErr: false, + wantFiles: []string{ + "/output/rest-api-spec/api/search.json", + "/output/rest-api-spec/api/index.json", + "/output/rest-api-spec/test/test1.yaml", + "/output/elasticsearch.json", + }, + }, + { + name: "success with release", + ref: "8.15.0", + dest: "/output", + responses: map[string]string{ + "https://artifacts.elastic.co/releases/stack.json": `{ + "releases": [ + { + "version": "8.15.0", + "manifest": "https://artifacts.elastic.co/releases/8.15.0/manifest.json" + } + ] + }`, + "https://artifacts.elastic.co/releases/8.15.0/manifest.json": `{ + "branch": "8.15", + "version": "8.15.0", + "build_id": "8.15.0-release", + "start_time": "2025-01-15T10:00:00+00:00", + "projects": { + "elasticsearch": { + "branch": "8.15", + "commit_hash": "release123", + "packages": { + "rest-resources-zip-8.15.0.zip": { + "url": "https://artifacts.elastic.co/releases/rest-resources.zip", + "type": "zip" + } + } + } + } + }`, + }, + wantErr: false, + wantFiles: []string{ + "/output/rest-api-spec/api/search.json", + "/output/rest-api-spec/api/index.json", + "/output/rest-api-spec/test/test1.yaml", + "/output/elasticsearch.json", + }, + }, + { + name: "empty ref", + ref: "", + dest: "/output", + wantErr: true, + wantErrMsg: "empty ref", + }, + { + name: "empty destination", + ref: "main", + dest: "", + wantErr: true, + wantErrMsg: "empty destination", + }, + { + name: "release not found", + ref: "99.99.99", + dest: "/output", + responses: map[string]string{ + "https://artifacts.elastic.co/releases/stack.json": `{ + "releases": [ + { + "version": "8.15.0", + "manifest": "https://artifacts.elastic.co/releases/8.15.0/manifest.json" + } + ] + }`, + }, + wantErr: true, + wantErrMsg: "release 99.99.99 not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + memFs := afero.NewMemMapFs() + + httpClient := retryablehttp.NewClient() + httpClient.Logger = nil + httpClient.RetryMax = 0 + httpClient.HTTPClient.Transport = &mockRoundTripper{ + RoundTripFunc: func(r *http.Request) (*http.Response, error) { + url := r.URL.String() + + // Check for zip download + if strings.HasSuffix(url, "rest-resources.zip") { + return &http.Response{ + Request: r.Clone(context.Background()), + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(testZipData)), + }, nil + } + + // Check for JSON responses + if response, ok := tt.responses[url]; ok { + return &http.Response{ + Request: r.Clone(context.Background()), + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(response)), + }, nil + } + + return &http.Response{ + Request: r.Clone(context.Background()), + StatusCode: http.StatusNotFound, + Body: io.NopCloser(strings.NewReader("Not Found")), + }, nil + }, + } + + client := &Client{ + releasesBaseURL: defaultReleasesBaseURL, + snapshotBaseURL: defaultSnapshotBaseURL, + client: httpClient, + fs: memFs, + } + + parsedRef, _ := ref.Parse(tt.ref) + err := client.DownloadRestResources(context.Background(), parsedRef, tt.dest) + + if tt.wantErr { + if err == nil { + t.Errorf("DownloadRestResources() expected error, got nil") + return + } + if tt.wantErrMsg != "" && !strings.Contains(err.Error(), tt.wantErrMsg) { + t.Errorf("DownloadRestResources() error = %v, want error containing %q", err, tt.wantErrMsg) + } + return + } + + if err != nil { + t.Errorf("DownloadRestResources() unexpected error: %v", err) + return + } + + // Verify expected files exist + for _, wantFile := range tt.wantFiles { + exists, err := afero.Exists(memFs, wantFile) + if err != nil { + t.Errorf("error checking file %s: %v", wantFile, err) + continue + } + if !exists { + t.Errorf("expected file %s does not exist", wantFile) + } + } + + // Verify elasticsearch.json content + if tt.dest != "" { + elasticsearchJSON, err := afero.ReadFile(memFs, tt.dest+"/elasticsearch.json") + if err != nil { + t.Errorf("cannot read elasticsearch.json: %v", err) + return + } + + var build Build + if err := json.Unmarshal(elasticsearchJSON, &build); err != nil { + t.Errorf("cannot unmarshal elasticsearch.json: %v", err) + return + } + + if build.Version == "" { + t.Error("elasticsearch.json has empty version") + } + if build.Projects.Elasticsearch.CommitHash == "" { + t.Error("elasticsearch.json has empty commit hash") + } + } + }) + } +} + +func TestManifest_ToBuild(t *testing.T) { + manifest := &Manifest{ + Branch: "main", + Version: "9.3.0-SNAPSHOT", + BuildID: "9.3.0-abc123", + StartTime: "2025-05-22T12:43:21+00:00", + Projects: struct { + Elasticsearch Project `json:"elasticsearch"` + }{ + Elasticsearch: Project{ + Branch: "main", + CommitHash: "abc123def456", + Packages: map[string]Package{ + "rest-resources.zip": { + URL: "https://example.com/rest-resources.zip", + Type: "zip", + }, + }, + }, + }, + } + + build := manifest.ToBuild() + + if build.Version != "9.3.0-SNAPSHOT" { + t.Errorf("Version = %q, want %q", build.Version, "9.3.0-SNAPSHOT") + } + + if build.BuildID != "9.3.0-abc123" { + t.Errorf("BuildID = %q, want %q", build.BuildID, "9.3.0-abc123") + } + + // Check start time is kept in RFC3339 format (matches original time.Time serialization) + if build.StartTime != "2025-05-22T12:43:21Z" { + t.Errorf("StartTime = %q, want %q", build.StartTime, "2025-05-22T12:43:21Z") + } + + if build.Projects.Elasticsearch.Branch != "main" { + t.Errorf("Branch = %q, want %q", build.Projects.Elasticsearch.Branch, "main") + } + + if build.Projects.Elasticsearch.CommitHash != "abc123def456" { + t.Errorf("CommitHash = %q, want %q", build.Projects.Elasticsearch.CommitHash, "abc123def456") + } + + if len(build.Projects.Elasticsearch.Packages) != 1 { + t.Errorf("Packages count = %d, want 1", len(build.Projects.Elasticsearch.Packages)) + } + + pkg, ok := build.Projects.Elasticsearch.Packages["rest-resources.zip"] + if !ok { + t.Error("rest-resources.zip package not found") + } else { + if pkg.URL != "https://example.com/rest-resources.zip" { + t.Errorf("Package URL = %q, want %q", pkg.URL, "https://example.com/rest-resources.zip") + } + if pkg.Type != "zip" { + t.Errorf("Package Type = %q, want %q", pkg.Type, "zip") + } + } +} diff --git a/internal/build/sdk/ref/ref.go b/internal/build/sdk/ref/ref.go new file mode 100644 index 0000000000..dbbd027a23 --- /dev/null +++ b/internal/build/sdk/ref/ref.go @@ -0,0 +1,56 @@ +package ref + +import ( + "errors" + "fmt" + + "github.com/elastic/go-elasticsearch/v9/internal/build/sdk/version" +) + +type Ref struct { + branch string + version *version.Version +} + +func (r Ref) IsEmpty() bool { + return r.version == nil && r.branch == "" +} + +func (r Ref) IsPreRelease() bool { + return (r.version != nil && r.version.IsPreRelease()) || r.branch != "" +} + +// Parse parses a reference string into a Ref. The reference can be a semantic +// version (e.g., "8.15.0", "9.0.0-SNAPSHOT") or a branch name (e.g., "main", "master"). +// An optional rename function can be provided to transform branch names during parsing. +func Parse(reference string, rename ...func(string) string) (Ref, error) { + if reference == "" { + return Ref{}, errors.New("empty reference") + } + + // Try to parse as a version first + if ver, err := version.Parse(reference); err == nil { + return Ref{version: &ver}, nil + } + + // Otherwise, treat as a branch name + branch := reference + if len(rename) > 0 && rename[0] != nil { + branch = rename[0](reference) + } + return Ref{branch: branch}, nil +} + +func (r Ref) TargetBranch() string { + if r.branch != "" { + return r.branch + } + return fmt.Sprintf("%d.%d", r.version.Major(), r.version.Minor()) +} + +func (r Ref) String() string { + if r.version != nil { + return r.version.String() + } + return r.branch +} diff --git a/internal/build/sdk/version/version.go b/internal/build/sdk/version/version.go new file mode 100644 index 0000000000..5b8cf29ed5 --- /dev/null +++ b/internal/build/sdk/version/version.go @@ -0,0 +1,135 @@ +package version + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" +) + +var ( + versionRexep = regexp.MustCompile(`^(?P\d+)\.(?P\d+)\.(?P\d+)(?:-(?P[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$`) +) + +// Version represents a semantic version. +type Version struct { + major, minor, patch int + preRelease string +} + +// Major returns the major version. +func (v Version) Major() int { + return v.major +} + +// Minor returns the minor version. +func (v Version) Minor() int { + return v.minor +} + +// Patch returns the patch version. +func (v Version) Patch() int { + return v.patch +} + +// PreRelease returns the pre-release version, if any. +func (v Version) PreRelease() string { + return v.preRelease +} + +// IsPreRelease returns true if the version is a pre-release version. e.g. 7.10.0-alpha1, or 7.10.0-SNAPSHOT +func (v Version) IsPreRelease() bool { + return v.PreRelease() != "" +} + +// WithMajor returns a new Version with the given major version. +func (v Version) WithMajor(major int) Version { + return Version{major: major, minor: v.minor, patch: v.patch, preRelease: v.preRelease} +} + +// WithMinor returns a new Version with the given minor version. +func (v Version) WithMinor(minor int) Version { + return Version{major: v.major, minor: minor, patch: v.patch, preRelease: v.preRelease} +} + +// WithPatch returns a new Version with the given patch version. +func (v Version) WithPatch(patch int) Version { + return Version{major: v.major, minor: v.minor, patch: patch, preRelease: v.preRelease} +} + +// WithPreRelease returns a new Version with the given pre-release version. +func (v Version) WithPreRelease(preRelease string) Version { + return Version{major: v.major, minor: v.minor, patch: v.patch, preRelease: preRelease} +} + +// String returns the version as a string. +func (v Version) String() string { + base := strings.Join( + []string{ + fmt.Sprint(v.major), + fmt.Sprint(v.minor), + fmt.Sprint(v.patch), + }, ".") + + if v.IsPreRelease() { + return fmt.Sprintf("%s-%s", base, v.preRelease) + } + return base +} + +// parseIntField extracts and parses an integer field from the regex match results. +func parseIntField(result map[string]string, field string) (int, error) { + if val, ok := result[field]; ok && val != "" { + return strconv.Atoi(val) + } + return 0, nil +} + +// Parse parses a version string into a Version struct. +func Parse(versionStr string) (Version, error) { + matches := versionRexep.FindStringSubmatch(versionStr) + if matches == nil { + return Version{}, errors.New("invalid version format") + } + + groupNames := versionRexep.SubexpNames() + result := make(map[string]string) + for i, name := range groupNames { + if i != 0 && name != "" { + result[name] = matches[i] + } + } + + major, err := parseIntField(result, "major") + if err != nil { + return Version{}, fmt.Errorf("invalid major version: %w", err) + } + + minor, err := parseIntField(result, "minor") + if err != nil { + return Version{}, fmt.Errorf("invalid minor version: %w", err) + } + + patch, err := parseIntField(result, "patch") + if err != nil { + return Version{}, fmt.Errorf("invalid patch version: %w", err) + } + + return Version{ + major: major, + minor: minor, + patch: patch, + preRelease: result["prerelease"], + }, nil +} + +// MustParse parses a version string into a Version struct. +// It panics if the version string is invalid. +func MustParse(version string) Version { + v, err := Parse(version) + if err != nil { + panic(err) + } + return v +} diff --git a/internal/build/sdk/version/version_test.go b/internal/build/sdk/version/version_test.go new file mode 100644 index 0000000000..6da492bb66 --- /dev/null +++ b/internal/build/sdk/version/version_test.go @@ -0,0 +1,64 @@ +package version + +import ( + "reflect" + "testing" +) + +func TestParse(t *testing.T) { + type args struct { + versionStr string + } + tests := []struct { + name string + args args + want Version + wantErr bool + }{ + { + name: "9.0.1-SNAPSHOT", + args: args{versionStr: "9.0.1-SNAPSHOT"}, + want: Version{9, 0, 1, "SNAPSHOT"}, + wantErr: false, + }, + { + name: "9.0.1-rc1", + args: args{versionStr: "9.0.1-rc1"}, + want: Version{9, 0, 1, "rc1"}, + wantErr: false, + }, + { + name: "9.0.1", + args: args{versionStr: "9.0.1"}, + want: Version{9, 0, 1, ""}, + wantErr: false, + }, + { + name: "9.0.1.0", + args: args{versionStr: "9.0.1.0"}, + wantErr: true, + }, + { + name: "9.0.1.0-SNAPSHOT", + args: args{versionStr: "9.0.1.0-SNAPSHOT"}, + wantErr: true, + }, + { + name: "9.0-SNAPSHOT", + args: args{versionStr: "9.0-SNAPSHOT"}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Parse(tt.args.versionStr) + if (err != nil) != tt.wantErr { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Parse() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/build/utils/chromatize.go b/internal/build/utils/chromatize.go index 389fcc3625..f01445dfef 100644 --- a/internal/build/utils/chromatize.go +++ b/internal/build/utils/chromatize.go @@ -28,7 +28,6 @@ import ( ) // Chromatize returns a syntax highlighted Go code. -// func Chromatize(r io.Reader) (io.Reader, error) { var b bytes.Buffer lexer := lexers.Get("go") diff --git a/internal/build/utils/debug.go b/internal/build/utils/debug.go index 1c2ec1d9ac..c6e1992d89 100644 --- a/internal/build/utils/debug.go +++ b/internal/build/utils/debug.go @@ -28,7 +28,6 @@ import ( ) // PrintSourceWithErr returns source code annotated with location of an error. -// func PrintSourceWithErr(out io.Reader, err error) { if IsTTY() { fmt.Fprint(os.Stderr, "\x1b[2m") diff --git a/internal/build/utils/strings.go b/internal/build/utils/strings.go index 7b9792ebe0..e423b32364 100644 --- a/internal/build/utils/strings.go +++ b/internal/build/utils/strings.go @@ -26,7 +26,6 @@ var ( ) // IDToUpper returns a string with all occurrences of "id" capitalized. -// func IDToUpper(s string) string { return reIDString.ReplaceAllLiteralString(s, "ID") } diff --git a/internal/build/utils/terminal.go b/internal/build/utils/terminal.go index 641bed76c6..f7add47b23 100644 --- a/internal/build/utils/terminal.go +++ b/internal/build/utils/terminal.go @@ -35,7 +35,6 @@ func init() { } // PrintErr prints an error to STDERR. -// func PrintErr(err error) { if isTTY { fmt.Fprint(os.Stderr, "\x1b[1;37;41m") @@ -48,13 +47,11 @@ func PrintErr(err error) { } // IsTTY returns true when os.Stderr is a terminal. -// func IsTTY() bool { return isTTY } // TerminalWidth returns the width of terminal, or zero. -// func TerminalWidth() int { if tWidth < 0 { return 0 From f670b8c85db49b71ad31de4eff3b1db8799d39a3 Mon Sep 17 00:00:00 2001 From: Matt Devy Date: Wed, 26 Nov 2025 18:23:54 +0000 Subject: [PATCH 2/4] chore: add license header --- internal/build/sdk/artifacts/client.go | 17 +++++++++++++++++ .../build/sdk/artifacts/latest_snapshot_get.go | 17 +++++++++++++++++ .../sdk/artifacts/latest_snapshot_get_test.go | 17 +++++++++++++++++ internal/build/sdk/artifacts/manifest_get.go | 17 +++++++++++++++++ .../build/sdk/artifacts/manifest_get_test.go | 17 +++++++++++++++++ internal/build/sdk/artifacts/releases_list.go | 17 +++++++++++++++++ .../build/sdk/artifacts/releases_list_test.go | 17 +++++++++++++++++ .../sdk/artifacts/rest_resources_download.go | 17 +++++++++++++++++ .../artifacts/rest_resources_download_test.go | 17 +++++++++++++++++ internal/build/sdk/ref/ref.go | 17 +++++++++++++++++ internal/build/sdk/version/version.go | 17 +++++++++++++++++ internal/build/sdk/version/version_test.go | 17 +++++++++++++++++ 12 files changed, 204 insertions(+) diff --git a/internal/build/sdk/artifacts/client.go b/internal/build/sdk/artifacts/client.go index ed60dd597b..67308b3da9 100644 --- a/internal/build/sdk/artifacts/client.go +++ b/internal/build/sdk/artifacts/client.go @@ -1,3 +1,20 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package artifacts import ( diff --git a/internal/build/sdk/artifacts/latest_snapshot_get.go b/internal/build/sdk/artifacts/latest_snapshot_get.go index 8adfc64ce6..eb23d40784 100644 --- a/internal/build/sdk/artifacts/latest_snapshot_get.go +++ b/internal/build/sdk/artifacts/latest_snapshot_get.go @@ -1,3 +1,20 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package artifacts import ( diff --git a/internal/build/sdk/artifacts/latest_snapshot_get_test.go b/internal/build/sdk/artifacts/latest_snapshot_get_test.go index b191defdcb..e0e57e3afa 100644 --- a/internal/build/sdk/artifacts/latest_snapshot_get_test.go +++ b/internal/build/sdk/artifacts/latest_snapshot_get_test.go @@ -1,3 +1,20 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package artifacts import ( diff --git a/internal/build/sdk/artifacts/manifest_get.go b/internal/build/sdk/artifacts/manifest_get.go index 7b9ec3f4ef..aa78c29e38 100644 --- a/internal/build/sdk/artifacts/manifest_get.go +++ b/internal/build/sdk/artifacts/manifest_get.go @@ -1,3 +1,20 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package artifacts import ( diff --git a/internal/build/sdk/artifacts/manifest_get_test.go b/internal/build/sdk/artifacts/manifest_get_test.go index c9309344b5..e6e617ddd7 100644 --- a/internal/build/sdk/artifacts/manifest_get_test.go +++ b/internal/build/sdk/artifacts/manifest_get_test.go @@ -1,3 +1,20 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package artifacts import ( diff --git a/internal/build/sdk/artifacts/releases_list.go b/internal/build/sdk/artifacts/releases_list.go index 9133170d8c..622658bc9b 100644 --- a/internal/build/sdk/artifacts/releases_list.go +++ b/internal/build/sdk/artifacts/releases_list.go @@ -1,3 +1,20 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package artifacts import ( diff --git a/internal/build/sdk/artifacts/releases_list_test.go b/internal/build/sdk/artifacts/releases_list_test.go index b8cd8e9c06..cf4987980e 100644 --- a/internal/build/sdk/artifacts/releases_list_test.go +++ b/internal/build/sdk/artifacts/releases_list_test.go @@ -1,3 +1,20 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package artifacts import ( diff --git a/internal/build/sdk/artifacts/rest_resources_download.go b/internal/build/sdk/artifacts/rest_resources_download.go index c45fdd6b3b..ac0550ba3d 100644 --- a/internal/build/sdk/artifacts/rest_resources_download.go +++ b/internal/build/sdk/artifacts/rest_resources_download.go @@ -1,3 +1,20 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package artifacts import ( diff --git a/internal/build/sdk/artifacts/rest_resources_download_test.go b/internal/build/sdk/artifacts/rest_resources_download_test.go index c7439a257d..3c632e6a9d 100644 --- a/internal/build/sdk/artifacts/rest_resources_download_test.go +++ b/internal/build/sdk/artifacts/rest_resources_download_test.go @@ -1,3 +1,20 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package artifacts import ( diff --git a/internal/build/sdk/ref/ref.go b/internal/build/sdk/ref/ref.go index dbbd027a23..e5437f354c 100644 --- a/internal/build/sdk/ref/ref.go +++ b/internal/build/sdk/ref/ref.go @@ -1,3 +1,20 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package ref import ( diff --git a/internal/build/sdk/version/version.go b/internal/build/sdk/version/version.go index 5b8cf29ed5..92773fc0c6 100644 --- a/internal/build/sdk/version/version.go +++ b/internal/build/sdk/version/version.go @@ -1,3 +1,20 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package version import ( diff --git a/internal/build/sdk/version/version_test.go b/internal/build/sdk/version/version_test.go index 6da492bb66..700794ea9d 100644 --- a/internal/build/sdk/version/version_test.go +++ b/internal/build/sdk/version/version_test.go @@ -1,3 +1,20 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package version import ( From f491e480d06568189df20db19f75e2604b48a61d Mon Sep 17 00:00:00 2001 From: Matt Devy Date: Wed, 26 Nov 2025 18:27:31 +0000 Subject: [PATCH 3/4] test: ref package --- internal/build/sdk/ref/ref_test.go | 324 +++++++++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 internal/build/sdk/ref/ref_test.go diff --git a/internal/build/sdk/ref/ref_test.go b/internal/build/sdk/ref/ref_test.go new file mode 100644 index 0000000000..c128af2c36 --- /dev/null +++ b/internal/build/sdk/ref/ref_test.go @@ -0,0 +1,324 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package ref + +import ( + "strings" + "testing" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + reference string + rename func(string) string + wantStr string + wantErr bool + }{ + { + name: "valid semantic version", + reference: "8.15.0", + wantStr: "8.15.0", + wantErr: false, + }, + { + name: "valid semantic version with snapshot", + reference: "9.0.0-SNAPSHOT", + wantStr: "9.0.0-SNAPSHOT", + wantErr: false, + }, + { + name: "valid semantic version with rc", + reference: "8.15.0-rc1", + wantStr: "8.15.0-rc1", + wantErr: false, + }, + { + name: "branch name main", + reference: "main", + wantStr: "main", + wantErr: false, + }, + { + name: "branch name master", + reference: "master", + wantStr: "master", + wantErr: false, + }, + { + name: "branch name with rename function", + reference: "main", + rename: func(s string) string { return "renamed-" + s }, + wantStr: "renamed-main", + wantErr: false, + }, + { + name: "version is not affected by rename function", + reference: "8.15.0", + rename: func(s string) string { return "renamed-" + s }, + wantStr: "8.15.0", + wantErr: false, + }, + { + name: "feature branch", + reference: "feature/new-api", + wantStr: "feature/new-api", + wantErr: false, + }, + { + name: "empty reference", + reference: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got Ref + var err error + if tt.rename != nil { + got, err = Parse(tt.reference, tt.rename) + } else { + got, err = Parse(tt.reference) + } + + if (err != nil) != tt.wantErr { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil { + return + } + if got.String() != tt.wantStr { + t.Errorf("Parse() got = %v, want %v", got.String(), tt.wantStr) + } + }) + } +} + +func TestRef_IsEmpty(t *testing.T) { + tests := []struct { + name string + ref Ref + want bool + }{ + { + name: "empty ref", + ref: Ref{}, + want: true, + }, + { + name: "ref with branch", + ref: mustParse("main"), + want: false, + }, + { + name: "ref with version", + ref: mustParse("8.15.0"), + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.ref.IsEmpty(); got != tt.want { + t.Errorf("Ref.IsEmpty() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRef_IsPreRelease(t *testing.T) { + tests := []struct { + name string + ref Ref + want bool + }{ + { + name: "stable version is not pre-release", + ref: mustParse("8.15.0"), + want: false, + }, + { + name: "snapshot version is pre-release", + ref: mustParse("9.0.0-SNAPSHOT"), + want: true, + }, + { + name: "rc version is pre-release", + ref: mustParse("8.15.0-rc1"), + want: true, + }, + { + name: "alpha version is pre-release", + ref: mustParse("8.15.0-alpha1"), + want: true, + }, + { + name: "branch is pre-release", + ref: mustParse("main"), + want: true, + }, + { + name: "feature branch is pre-release", + ref: mustParse("feature/new-api"), + want: true, + }, + { + name: "empty ref is not pre-release", + ref: Ref{}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.ref.IsPreRelease(); got != tt.want { + t.Errorf("Ref.IsPreRelease() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRef_TargetBranch(t *testing.T) { + tests := []struct { + name string + ref Ref + want string + }{ + { + name: "version returns major.minor", + ref: mustParse("8.15.0"), + want: "8.15", + }, + { + name: "snapshot version returns major.minor", + ref: mustParse("9.0.0-SNAPSHOT"), + want: "9.0", + }, + { + name: "branch returns branch name", + ref: mustParse("main"), + want: "main", + }, + { + name: "feature branch returns branch name", + ref: mustParse("feature/new-api"), + want: "feature/new-api", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.ref.TargetBranch(); got != tt.want { + t.Errorf("Ref.TargetBranch() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRef_String(t *testing.T) { + tests := []struct { + name string + ref Ref + want string + }{ + { + name: "version returns version string", + ref: mustParse("8.15.0"), + want: "8.15.0", + }, + { + name: "snapshot version returns full version string", + ref: mustParse("9.0.0-SNAPSHOT"), + want: "9.0.0-SNAPSHOT", + }, + { + name: "branch returns branch name", + ref: mustParse("main"), + want: "main", + }, + { + name: "empty ref returns empty string", + ref: Ref{}, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.ref.String(); got != tt.want { + t.Errorf("Ref.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParse_WithRenameFunction(t *testing.T) { + // Test that the rename function is applied correctly to branch names + uppercaseRename := func(s string) string { + return strings.ToUpper(s) + } + + tests := []struct { + name string + reference string + rename func(string) string + wantStr string + }{ + { + name: "rename main to MAIN", + reference: "main", + rename: uppercaseRename, + wantStr: "MAIN", + }, + { + name: "nil rename function does not modify branch", + reference: "main", + rename: nil, + wantStr: "main", + }, + { + name: "version is not renamed", + reference: "8.15.0", + rename: uppercaseRename, + wantStr: "8.15.0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Parse(tt.reference, tt.rename) + if err != nil { + t.Errorf("Parse() unexpected error = %v", err) + return + } + if got.String() != tt.wantStr { + t.Errorf("Parse() got = %v, want %v", got.String(), tt.wantStr) + } + }) + } +} + +func mustParse(reference string) Ref { + ref, err := Parse(reference) + if err != nil { + panic(err) + } + return ref +} From 2662ec15d323150cd46ff1b8cafa305672394048 Mon Sep 17 00:00:00 2001 From: Matt Devy Date: Wed, 26 Nov 2025 19:46:47 +0000 Subject: [PATCH 4/4] ci: update skip list --- .../cmd/generate/commands/gentests/skips.go | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/internal/build/cmd/generate/commands/gentests/skips.go b/internal/build/cmd/generate/commands/gentests/skips.go index 4689383be9..632a735843 100644 --- a/internal/build/cmd/generate/commands/gentests/skips.go +++ b/internal/build/cmd/generate/commands/gentests/skips.go @@ -19,8 +19,9 @@ package gentests import ( "fmt" - "gopkg.in/yaml.v2" "strings" + + "gopkg.in/yaml.v2" ) var skipTests map[string][]string @@ -80,8 +81,12 @@ var skipFiles = []string{ "update/100_synthetic_source.yml", "tsdb/160_nested_fields.yml", "tsdb/90_unsupported_operations.yml", + "cluster.component_template/.*.yml", + "indices.put_index_template/.*.yml", + "ml/forecast.yml", ".*inference/.*.yml", // incompatible inference tests ".*mustache/.*.yml", // incompatible mustache tests + "data_stream/240_data_stream_settings.yml", } // TODO: Comments into descriptions for `Skip()` @@ -466,12 +471,6 @@ data_streams/10_data_stream_resolvability.yml: data_stream/230_data_stream_options.yml: - Test partially resetting failure store options in template composition -data_stream/240_data_stream_settings.yml: - - Test single data stream - - Test dry run - - Test null out settings - - Test null out settings component templates only - # Error: constant 9223372036854775808 overflows int (https://play.golang.org/p/7pUdz-_Pdom) unsigned_long/10_basic.yml: unsigned_long/20_null_value.yml: @@ -662,4 +661,29 @@ search.vectors/41_knn_search_half_byte_quantized.yml: tsdb/25_id_generation.yml: - delete over _bulk +# Data stream mappings tests failing in Go test suite +data_stream/250_data_stream_mappings.yml: + - "Test single data stream" + - "Test mappings component templates only" + +# Data streams stats failing for multiple data streams scenario +data_stream/120_data_streams_stats.yml: + - "Multiple data stream" + +# Search vectors synthetic dense vectors failures (include and update cases) +search.vectors/240_source_synthetic_dense_vectors.yml: + - "include synthetic vectors" + - "Bulk partial update with synthetic vectors" + - "Partial update with synthetic vectors" + +# Search vectors synthetic sparse vectors failures (include and update cases) +search.vectors/250_source_synthetic_sparse_vectors.yml: + - "include synthetic vectors" + - "Bulk partial update with synthetic vectors" + - "Partial update with synthetic vectors" + +rank_vectors/rank_vectors_synthetic_vectors.yml: + - "include synthetic vectors" + - "Bulk partial update with synthetic vectors" + - "Partial update with synthetic vectors" `