Skip to content

Commit feaa8f7

Browse files
authored
Avoid requesting latest github release in every command (#1417)
Two different mechanisms have been added to avoid requesting to Github for every command the latest elastic-package release published. A new environment variable ELASTIC_PACKAGE_CHECK_UPDATE_DISABLED to disable completely this check, and also a cache to store the latest Github release published for 30 minutes. This file is going to be stored under ~/.elastic-package folder.
1 parent f1468cd commit feaa8f7

File tree

5 files changed

+207
-13
lines changed

5 files changed

+207
-13
lines changed

.buildkite/pipeline.trigger.integration.tests.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,12 @@ echo " - label: \":go: Running integration test: test-profiles-command\""
107107
echo " command: ./.buildkite/scripts/integration_tests.sh -t test-profiles-command"
108108
echo " agents:"
109109
echo " provider: \"gcp\""
110+
111+
echo " - label: \":go: Running integration test: test-check-update-version\""
112+
echo " command: ./.buildkite/scripts/integration_tests.sh -t test-check-update-version"
113+
echo " env:"
114+
echo " DEFAULT_VERSION_TAG: v0.80.0"
115+
echo " agents:"
116+
echo " image: \"${LINUX_AGENT_IMAGE}\""
117+
echo " cpu: \"8\""
118+
echo " memory: \"4G\""

Makefile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ CODE_COVERAGE_REPORT_NAME_UNIT = $(CODE_COVERAGE_REPORT_FOLDER)/coverage-unit-re
33
VERSION_IMPORT_PATH = github.com/elastic/elastic-package/internal/version
44
VERSION_COMMIT_HASH = `git describe --always --long --dirty`
55
VERSION_BUILD_TIME = `date +%s`
6-
VERSION_TAG = `(git describe --exact-match --tags 2>/dev/null || echo '') | tr -d '\n'`
6+
DEFAULT_VERSION_TAG ?=
7+
VERSION_TAG = `(git describe --exact-match --tags 2>/dev/null || echo '$(DEFAULT_VERSION_TAG)') | tr -d '\n'`
78
VERSION_LDFLAGS = -X $(VERSION_IMPORT_PATH).CommitHash=$(VERSION_COMMIT_HASH) -X $(VERSION_IMPORT_PATH).BuildTime=$(VERSION_BUILD_TIME) -X $(VERSION_IMPORT_PATH).Tag=$(VERSION_TAG)
89

910
.PHONY: build
@@ -101,7 +102,10 @@ test-install-zip-shellinit:
101102
test-profiles-command:
102103
./scripts/test-profiles-command.sh
103104

104-
test: test-go test-stack-command test-check-packages test-profiles-command test-build-zip
105+
test-check-update-version:
106+
./scripts/test-check-update-version.sh
107+
108+
test: test-go test-stack-command test-check-packages test-profiles-command test-build-zip test-check-update-version
105109

106110
check-git-clean:
107111
git update-index --really-refresh

internal/github/client.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,32 @@
55
package github
66

77
import (
8+
"context"
9+
"fmt"
810
"net/http"
911

1012
"github.com/google/go-github/v32/github"
1113
)
1214

15+
type Client struct {
16+
client *github.Client
17+
}
18+
1319
// UnauthorizedClient function returns unauthorized instance of Github API client.
14-
func UnauthorizedClient() *github.Client {
15-
return github.NewClient(new(http.Client))
20+
func UnauthorizedClient() *Client {
21+
githubClient := github.NewClient(new(http.Client))
22+
return &Client{githubClient}
23+
}
24+
25+
func (c *Client) LatestRelease(ctx context.Context, repositoryOwner, repositoryName string) (*github.RepositoryRelease, error) {
26+
release, _, err := c.client.Repositories.GetLatestRelease(ctx, repositoryOwner, repositoryName)
27+
if err != nil {
28+
return nil, fmt.Errorf("can't check latest release: %w", err)
29+
}
30+
31+
if release.TagName == nil || *release.TagName == "" {
32+
return nil, fmt.Errorf("release tag is empty")
33+
}
34+
35+
return release, nil
1636
}

internal/version/check_update.go

Lines changed: 108 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,37 @@ package version
66

77
import (
88
"context"
9+
"encoding/json"
10+
"fmt"
11+
"os"
12+
"path/filepath"
13+
"strings"
14+
"time"
915

1016
"github.com/Masterminds/semver/v3"
1117

18+
"github.com/elastic/elastic-package/internal/configuration/locations"
19+
"github.com/elastic/elastic-package/internal/environment"
1220
"github.com/elastic/elastic-package/internal/github"
1321
"github.com/elastic/elastic-package/internal/logger"
1422
)
1523

1624
const (
1725
repositoryOwner = "elastic"
1826
repositoryName = "elastic-package"
27+
28+
latestVersionFile = "latestVersion"
29+
defaultCacheDuration = 30 * time.Minute
1930
)
2031

32+
var checkUpdatedDisabledEnv = environment.WithElasticPackagePrefix("CHECK_UPDATE_DISABLED")
33+
34+
type versionLatest struct {
35+
TagName string `json:"tag"`
36+
HtmlURL string `json:"html_url"`
37+
Timestamp time.Time `json:"timestamp"`
38+
}
39+
2140
// CheckUpdate function checks using Github Release API if newer version is available.
2241
func CheckUpdate() {
2342
if Tag == "" {
@@ -26,16 +45,39 @@ func CheckUpdate() {
2645
return
2746
}
2847

29-
githubClient := github.UnauthorizedClient()
30-
release, _, err := githubClient.Repositories.GetLatestRelease(context.TODO(), repositoryOwner, repositoryName)
31-
if err != nil {
32-
logger.Debugf("Error: can't check latest release, %v", err)
48+
v, ok := os.LookupEnv(checkUpdatedDisabledEnv)
49+
if ok && strings.ToLower(v) != "false" {
50+
logger.Debug("Disabled checking updates")
3351
return
3452
}
3553

36-
if release.TagName == nil || *release.TagName == "" {
37-
logger.Debugf("Error: release tag is empty")
38-
return
54+
expired := true
55+
latestVersion, err := loadCacheLatestVersion()
56+
switch {
57+
case err != nil:
58+
logger.Debug("failed to load latest version from cache: %v", err)
59+
default:
60+
expired = checkCachedLatestVersion(latestVersion, defaultCacheDuration)
61+
}
62+
63+
var release *versionLatest
64+
switch {
65+
case !expired:
66+
logger.Debugf("latest version (cached): %s", latestVersion)
67+
release = latestVersion
68+
default:
69+
logger.Debugf("checking latest release in Github")
70+
githubClient := github.UnauthorizedClient()
71+
githubRelease, err := githubClient.LatestRelease(context.TODO(), repositoryOwner, repositoryName)
72+
if err != nil {
73+
logger.Debugf("Error: %v", err)
74+
return
75+
}
76+
release = &versionLatest{
77+
TagName: *githubRelease.TagName,
78+
HtmlURL: *githubRelease.HTMLURL,
79+
Timestamp: time.Now(),
80+
}
3981
}
4082

4183
currentVersion, err := semver.NewVersion(Tag[1:]) // strip "v" prefix
@@ -44,13 +86,70 @@ func CheckUpdate() {
4486
return
4587
}
4688

47-
releaseVersion, err := semver.NewVersion((*release.TagName)[1:]) // strip "v" prefix
89+
releaseVersion, err := semver.NewVersion(release.TagName[1:]) // strip "v" prefix
4890
if err != nil {
4991
logger.Debugf("Error: can't parse current version tag, %v", err)
5092
return
5193
}
5294

5395
if currentVersion.LessThan(releaseVersion) {
54-
logger.Infof("New version is available - %s. Download from: %s", *release.TagName, *release.HTMLURL)
96+
logger.Infof("New version is available - %s. Download from: %s", release.TagName, release.HtmlURL)
97+
}
98+
99+
// if version cached is not expired, do not write contents into file
100+
if !expired {
101+
return
102+
}
103+
104+
if err := writeLatestReleaseToCache(release); err != nil {
105+
logger.Debugf("failed to write latest versoin to cache file: %v", err)
106+
}
107+
}
108+
109+
func writeLatestReleaseToCache(release *versionLatest) error {
110+
elasticPackagePath, err := locations.NewLocationManager()
111+
if err != nil {
112+
return fmt.Errorf("failed locating the configuration directory: %w", err)
113+
}
114+
115+
latestVersionPath := filepath.Join(elasticPackagePath.RootDir(), latestVersionFile)
116+
117+
contents, err := json.Marshal(release)
118+
if err != nil {
119+
return fmt.Errorf("failed to encode file %s: %w", latestVersionPath, err)
120+
}
121+
err = os.WriteFile(latestVersionPath, contents, 0644)
122+
if err != nil {
123+
return fmt.Errorf("writing file failed (path: %s): %w", latestVersionPath, err)
124+
}
125+
126+
return nil
127+
}
128+
129+
func loadCacheLatestVersion() (*versionLatest, error) {
130+
elasticPackagePath, err := locations.NewLocationManager()
131+
if err != nil {
132+
return nil, fmt.Errorf("failed locating the configuration directory: %w", err)
55133
}
134+
135+
latestVersionPath := filepath.Join(elasticPackagePath.RootDir(), latestVersionFile)
136+
contents, err := os.ReadFile(latestVersionPath)
137+
if err != nil {
138+
logger.Warnf("reading version file failed: %w", err.Error())
139+
return nil, fmt.Errorf("reading version file failed: %w", err)
140+
}
141+
142+
var infoVersion versionLatest
143+
err = json.Unmarshal(contents, &infoVersion)
144+
if err != nil {
145+
return nil, fmt.Errorf("failed to decode file %s: %w", latestVersionPath, err)
146+
}
147+
148+
return &infoVersion, nil
149+
}
150+
151+
func checkCachedLatestVersion(latest *versionLatest, expiration time.Duration) bool {
152+
exprirationTime := time.Now().Add(-expiration)
153+
154+
return latest.Timestamp.Before(exprirationTime)
56155
}

scripts/test-check-update-version.sh

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/bin/bash
2+
3+
set -euxo pipefail
4+
5+
# Last time data modification (seconds since Epoch)
6+
timeLatestDataModification() {
7+
local filePath="$1"
8+
stat --format="%Y" ${filePath}
9+
}
10+
11+
latestVersionFilePath="${HOME}/.elastic-package/latestVersion"
12+
rm -rf ${latestVersionFilePath}
13+
14+
# First usage needs to write the cache file
15+
elastic-package version
16+
17+
if [ ! -f ${latestVersionFilePath} ]; then
18+
echo "Error: Cache file with latest release info not written"
19+
exit 1
20+
fi
21+
22+
LATEST_MODIFICATION_SINCE_EPOCH=$(timeLatestDataModification ${latestVersionFilePath})
23+
24+
# Second elastic-package usage should not update the file
25+
elastic-package version
26+
27+
if [ ${LATEST_MODIFICATION_SINCE_EPOCH} != $(timeLatestDataModification ${latestVersionFilePath}) ]; then
28+
echo "Error: Cache file with latest release info updated - not used cached value"
29+
exit 1
30+
fi
31+
32+
# If latest data modification is older than the expiration time, it should be updated
33+
# Forced change latest data modification of cache file
34+
cat <<EOF > ${latestVersionFilePath}
35+
{
36+
"tag":"v0.85.0",
37+
"html_url":"https://github.com/elastic/elastic-package/releases/tag/v0.85.0",
38+
"timestamp":"2023-08-28T17:10:31.735505212+02:00"
39+
}
40+
EOF
41+
LATEST_MODIFICATION_SINCE_EPOCH=$(timeLatestDataModification ${latestVersionFilePath})
42+
43+
# Precision of stat is in seconds, need to wait at least 1 second
44+
sleep 1
45+
46+
elastic-package version
47+
48+
if [ ${LATEST_MODIFICATION_SINCE_EPOCH} == $(timeLatestDataModification ${latestVersionFilePath}) ]; then
49+
echo "Error: Cache file with latest release info not updated and timestamp is older than the expiration time"
50+
exit 1
51+
fi
52+
53+
# If environment variable is defined, cache file should not be written
54+
export ELASTIC_PACKAGE_CHECK_UPDATE_DISABLED=true
55+
rm -rf ${latestVersionFilePath}
56+
57+
elastic-package version
58+
59+
if [ -f ${latestVersionFilePath} ]; then
60+
echo "Error: Cache file with latest release info written and ELASTIC_PACKAGE_CHECK_UPDATE_DISABLED is defined"
61+
exit 1
62+
fi

0 commit comments

Comments
 (0)