Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 108 additions & 44 deletions pkg/plugins/resources/temurin/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httputil"
"net/url"
"slices"

"github.com/sirupsen/logrus"
"github.com/updatecli/updatecli/pkg/core/httpclient"
Expand All @@ -23,8 +24,15 @@ const signaturesEndpoint = "/signature/version"
const parseVersionEndpoint = "/version"
const releaseNamesEndpoint = "/info/release_names"

func (t Temurin) baseURL() string {
if t.apiURL != "" {
return t.apiURL
}
return temurinApiUrl
}

func (t Temurin) apiPerformHttpReq(endpoint string, webClient httpclient.HTTPClient) (body []byte, locationHeader string, err error) {
url := temurinApiUrl + endpoint
url := t.baseURL() + endpoint

req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
Expand Down Expand Up @@ -69,18 +77,34 @@ func (t Temurin) apiGetRedirectLocation(endpoint string) (redirectLocation strin
return redirectLocation, err
}

func (t Temurin) apiGetLastFeatureRelease() (result int, err error) {
apiInfoReleases, err := t.apiGetInfoReleases()
func (t Temurin) apiGetLastFeatureRelease() (result int, fallbacks []int, err error) {
infoReleases, err := t.apiGetInfoReleases()
if err != nil {
return result, err
return result, nil, err
}

result = apiInfoReleases.MostRecentLTS
var candidates []int
if t.spec.ReleaseLine == "feature" {
result = apiInfoReleases.MostRecentFeatureRelease
result = infoReleases.MostRecentFeatureRelease
candidates = slices.Clone(infoReleases.AvailableReleases)
} else {
result = infoReleases.MostRecentLTS
candidates = slices.Clone(infoReleases.AvailableLTSReleases)
}
slices.Sort(candidates)

return result, err
if len(candidates) == 0 {
logrus.Debugf("[temurin] API returned no available releases for release line %q", t.spec.ReleaseLine)
}

// Build fallbacks: all candidates except the primary, highest version first.
for i := len(candidates) - 1; i >= 0; i-- {
if candidates[i] != result {
fallbacks = append(fallbacks, candidates[i])
}
}

return result, fallbacks, nil
}

func (t Temurin) apiGetInfoReleases() (result *apiInfoReleases, err error) {
Expand Down Expand Up @@ -151,17 +175,51 @@ func (t Temurin) apiParseVersion(version string) (result parsedVersion, err erro
return result, nil
}

func (t Temurin) apiGetReleaseNames() (result []string, err error) {
var versionRange string
// apiQueryReleaseNamesForRange queries the release_names endpoint for an arbitrary semver range string.
func (t Temurin) apiQueryReleaseNamesForRange(versionRange string) ([]string, error) {
apiEndpoint := fmt.Sprintf(
"%s?heap_size=normal&image_type=%s&page=0&page_size=10&project=%s&release_type=%s&architecture=%s&os=%s&semver=true&sort_method=DEFAULT&sort_order=DESC&vendor=eclipse&version=%s",
releaseNamesEndpoint,
url.QueryEscape(t.spec.ImageType),
url.QueryEscape(t.spec.Project),
url.QueryEscape(t.spec.ReleaseType),
url.QueryEscape(t.spec.Architecture),
url.QueryEscape(t.spec.OperatingSystem),
url.QueryEscape(versionRange),
)

logrus.Debugf("[temurin] using API endpoint %q", apiEndpoint)

body, err := t.apiGetBody(apiEndpoint)
if err != nil {
return nil, err
}

var apiResult releaseInformation
if err := json.Unmarshal(body, &apiResult); err != nil {
logrus.Debugf("[temurin] Failed decoding the response: %q\n", err)
return nil, fmt.Errorf("[temurin] No release found matching provided criteria. Use '--debug' to get details")
}

return apiResult.Releases, nil
}

// apiQueryReleaseNames queries the release_names endpoint for a specific feature version.
// The version range covers all patch releases within that major version: (N.0.0, N+1.0.0].
func (t Temurin) apiQueryReleaseNames(featureVersion int) ([]string, error) {
versionRange := fmt.Sprintf("(%d.0.0, %d.0.0]", featureVersion, featureVersion+1)
return t.apiQueryReleaseNamesForRange(versionRange)
}

// If user specified a custom version, we have to normalize and validate it
func (t Temurin) apiGetReleaseNames() (result []string, err error) {
// If user specified a custom version, normalize and validate it first.
if t.spec.SpecificVersion != "" {
parsedVersion, err := t.apiParseVersion(t.spec.SpecificVersion)
if err != nil {
return []string{}, err
}

versionRange = fmt.Sprintf("(%d.%d.%d, %d.%d.%d]",
versionRange := fmt.Sprintf("(%d.%d.%d, %d.%d.%d]",
parsedVersion.Major,
parsedVersion.Minor,
parsedVersion.Security,
Expand All @@ -170,47 +228,53 @@ func (t Temurin) apiGetReleaseNames() (result []string, err error) {
parsedVersion.Security+1,
)

} else {
featureVersion := t.spec.FeatureVersion
if featureVersion == 0 {
featureVersion, err = t.apiGetLastFeatureRelease()
if err != nil {
return []string{}, err
}
}

versionRange = fmt.Sprintf("(%d.0.0, %d.0.0]", featureVersion, featureVersion+1)
return t.apiQueryReleaseNamesForRange(versionRange)
}

apiEndpoint := fmt.Sprintf(
"%s?heap_size=normal&image_type=%s&page=0&page_size=10&project=%s&release_type=%s&architecture=%s&os=%s&semver=true&sort_method=DEFAULT&sort_order=DESC&vendor=eclipse&version=%s",
releaseNamesEndpoint,
t.spec.ImageType,
t.spec.Project,
t.spec.ReleaseType,
t.spec.Architecture,
t.spec.OperatingSystem,
// Mandatory URL encoding otherwise empty responses or HTTP errors
url.QueryEscape(versionRange),
)
featureVersion := t.spec.FeatureVersion
var fallbacks []int
if featureVersion == 0 {
featureVersion, fallbacks, err = t.apiGetLastFeatureRelease()
if err != nil {
return []string{}, err
}
}

logrus.Debugf("[temurin] using API endpoint %q", apiEndpoint)
// Try the primary feature version, then fall back to older releases if the
// API returns no results (e.g. a newly announced major with no GA builds yet).
const maxAttempts = 3
candidates := append([]int{featureVersion}, fallbacks...)
if len(candidates) > maxAttempts {
candidates = candidates[:maxAttempts]
}

body, err := t.apiGetBody(apiEndpoint)
if err != nil {
logrus.Errorf("something went wrong while getting Temurin API latest release information %q.", err)
return result, err
var firstErr error
for i, version := range candidates {
releases, queryErr := t.apiQueryReleaseNames(version)
if queryErr != nil {
if firstErr == nil {
firstErr = queryErr
logrus.Debugf("[temurin] feature version %d returned an error, trying fallback: %v", version, queryErr)
}
logrus.Debugf("[temurin] falling back from feature version %d after error: %v", version, queryErr)
continue
}
if len(releases) == 0 {
logrus.Debugf("[temurin] no releases found for feature version %d, trying fallback", version)
continue
}
if i > 0 {
logrus.Debugf("[temurin] using fallback feature version %d", version)
}
return releases, nil
}

var apiResult releaseInformation
err = json.Unmarshal(body, &apiResult)
if err != nil {
logrus.Debugf("[temurin] Failed decoding the response: %q\n", err)
return result, fmt.Errorf("[temurin] No release found matching provided criteria. Use '--debug' to get details")
if firstErr != nil {
return []string{}, firstErr
}

// Return only the most recent, e.g. the first one (sort is DESC in the URL)
return apiResult.Releases, nil
logrus.Debugf("[temurin] exhausted all %d candidate feature versions with no matching releases", len(candidates))
return []string{}, nil
}

func (t Temurin) apiGetInstallerUrl(releaseName string) (result string, err error) {
Expand Down
Loading
Loading