-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Check Regal version at start-up (#824)
* Check Regal version at start-up This adds functionality to the regal LS and lint commands what will check the current version against the latest release in GH. In order to keep the number of requests down, the version is cached to disk in the .regal dir. The task runs in another goroutine to keep things fast too. Config has been updated with a features key, and REGAL_FEATURES_REMOTE_CHECK_VERSION can also be set to disable to functionality. Sharing for review, if we think it looks good, I'll add some docs and we can get this in. Signed-off-by: Charlie Egan <charlie@styra.com> * Share logic for sharing of defaulting config Signed-off-by: Charlie Egan <charlie@styra.com> * Rename var to disable the check version feature Signed-off-by: Charlie Egan <charlie@styra.com> * Use global config dir for version state Signed-off-by: Charlie Egan <charlie@styra.com> * Use shorter var name Signed-off-by: Charlie Egan <charlie@styra.com> * Extract check to func * Add docs Signed-off-by: Charlie Egan <charlie@styra.com> * Set lang in docs Signed-off-by: Charlie Egan <charlie@styra.com> --------- Signed-off-by: Charlie Egan <charlie@styra.com>
- Loading branch information
1 parent
b6588dc
commit 90b2bcc
Showing
12 changed files
with
608 additions
and
111 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,6 @@ | ||
features: | ||
remote: | ||
check-version: true | ||
rules: | ||
bugs: | ||
constant-condition: | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# Remote Features | ||
|
||
This page outlines the features of Regal that need internet access to function. | ||
|
||
## Checking for Updates | ||
|
||
Regal will check for updates on startup. If a new version is available, | ||
Regal will notify you by writing a message in stderr. | ||
|
||
An example of such a message is: | ||
|
||
```txt | ||
A new version of Regal is available (v0.23.1). You are running v0.23.0. | ||
See https://github.com/StyraInc/regal/releases/tag/v0.23.1 for the latest release. | ||
``` | ||
|
||
This message is based on the local version set in the Regal binary, and **no | ||
user data is sent** to GitHub where the releases are hosted. | ||
|
||
This same function will also write to the file at: `$HOME/.config/regal/latest_version.json`, | ||
this is used as a cache of the latest version to avoid consuming excessive | ||
GitHub API rate limits when using Regal. | ||
|
||
This functionality can be disabled in two ways: | ||
|
||
* Using `.regal/config.yaml`: set `features.remote.check-version` to `false`. | ||
* Using an environment variable: set `REGAL_DISABLE_CHECK_VERSION` to `true`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
//nolint:errcheck | ||
package update | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
"time" | ||
|
||
"github.com/open-policy-agent/opa/rego" | ||
|
||
_ "embed" | ||
) | ||
|
||
//go:embed update.rego | ||
var updateModule string | ||
|
||
const CheckVersionDisableEnvVar = "REGAL_DISABLE_VERSION_CHECK" | ||
|
||
type Options struct { | ||
CurrentVersion string | ||
CurrentTime time.Time | ||
|
||
StateDir string | ||
|
||
ReleaseServerHost string | ||
ReleaseServerPath string | ||
|
||
CTAURLPrefix string | ||
|
||
Debug bool | ||
} | ||
|
||
type latestVersionFileContents struct { | ||
LatestVersion string `json:"latest_version"` | ||
CheckedAt time.Time `json:"checked_at"` | ||
} | ||
|
||
func CheckAndWarn(opts Options, w io.Writer) { | ||
// this is a shortcut heuristic to avoid and version checking | ||
// when in dev/test etc. | ||
if !strings.HasPrefix(opts.CurrentVersion, "v") { | ||
return | ||
} | ||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
defer cancel() | ||
|
||
latestVersion, err := getLatestVersion(ctx, opts) | ||
if err != nil { | ||
if opts.Debug { | ||
w.Write([]byte(err.Error())) | ||
} | ||
|
||
return | ||
} | ||
|
||
regoArgs := []func(*rego.Rego){ | ||
rego.Module("update.rego", updateModule), | ||
rego.Query(`data.update.needs_update`), | ||
rego.Input(map[string]interface{}{ | ||
"current_version": opts.CurrentVersion, | ||
"latest_version": latestVersion, | ||
}), | ||
} | ||
|
||
rs, err := rego.New(regoArgs...).Eval(context.Background()) | ||
if err != nil { | ||
if opts.Debug { | ||
w.Write([]byte(err.Error())) | ||
} | ||
|
||
return | ||
} | ||
|
||
if !rs.Allowed() { | ||
if opts.Debug { | ||
w.Write([]byte("Regal is up to date")) | ||
} | ||
|
||
return | ||
} | ||
|
||
ctaURLPrefix := "https://github.com/StyraInc/regal/releases/tag/" | ||
if opts.CTAURLPrefix != "" { | ||
ctaURLPrefix = opts.CTAURLPrefix | ||
} | ||
|
||
ctaURL := ctaURLPrefix + latestVersion | ||
|
||
tmpl := `A new version of Regal is available (%s). You are running %s. | ||
See %s for the latest release. | ||
` | ||
|
||
w.Write([]byte(fmt.Sprintf(tmpl, latestVersion, opts.CurrentVersion, ctaURL))) | ||
} | ||
|
||
func getLatestVersion(ctx context.Context, opts Options) (string, error) { | ||
if opts.StateDir != "" { | ||
// first, attempt to get the file from previous invocations to save on remote calls | ||
latestVersionFilePath := filepath.Join(opts.StateDir, "latest_version.json") | ||
|
||
_, err := os.Stat(latestVersionFilePath) | ||
if err == nil { | ||
var preExistingState latestVersionFileContents | ||
|
||
file, err := os.Open(latestVersionFilePath) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to open file: %w", err) | ||
} | ||
|
||
err = json.NewDecoder(file).Decode(&preExistingState) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to decode existing version state file: %w", err) | ||
} | ||
|
||
if opts.CurrentTime.Sub(preExistingState.CheckedAt) < 3*24*time.Hour { | ||
return preExistingState.LatestVersion, nil | ||
} | ||
} | ||
} | ||
|
||
client := http.Client{} | ||
|
||
releaseServerHost := "https://api.github.com" | ||
if opts.ReleaseServerHost != "" { | ||
releaseServerHost = strings.TrimSuffix(opts.ReleaseServerHost, "/") | ||
|
||
if !strings.HasPrefix(releaseServerHost, "http") { | ||
releaseServerHost = "https://" + releaseServerHost | ||
} | ||
} | ||
|
||
releaseServerURL, err := url.Parse(releaseServerHost) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to parse release server URL: %w", err) | ||
} | ||
|
||
releaseServerPath := "/repos/styrainc/regal/releases/latest" | ||
if opts.ReleaseServerPath != "" { | ||
releaseServerPath = opts.ReleaseServerPath | ||
} | ||
|
||
releaseServerURL.Path = releaseServerPath | ||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, releaseServerURL.String(), nil) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to create request: %w", err) | ||
} | ||
|
||
resp, err := client.Do(req) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to make request: %w", err) | ||
} | ||
defer resp.Body.Close() | ||
|
||
var responseData struct { | ||
TagName string `json:"tag_name"` | ||
} | ||
|
||
err = json.NewDecoder(resp.Body).Decode(&responseData) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to decode response: %w", err) | ||
} | ||
|
||
stateBs, err := json.MarshalIndent(latestVersionFileContents{ | ||
LatestVersion: responseData.TagName, | ||
CheckedAt: opts.CurrentTime, | ||
}, "", " ") | ||
if err != nil { | ||
return "", fmt.Errorf("failed to marshal state file: %w", err) | ||
} | ||
|
||
err = os.WriteFile(opts.StateDir+"/latest_version.json", stateBs, 0o600) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to write state file: %w", err) | ||
} | ||
|
||
return responseData.TagName, nil | ||
} |
Oops, something went wrong.