Skip to content

Commit bebff27

Browse files
authored
Move more of version check to Rego (#1513)
Just something little I wanted to try :) Signed-off-by: Anders Eknert <anders@styra.com>
1 parent d31b341 commit bebff27

6 files changed

Lines changed: 190 additions & 181 deletions

File tree

cmd/lint.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,11 +216,11 @@ func lint(args []string, params *lintCommandParams) (report.Report, error) {
216216
}
217217
}
218218

219-
var regalDir *os.File
220-
221-
var customRulesDir string
222-
223-
var configSearchPath string
219+
var (
220+
regalDir *os.File
221+
customRulesDir string
222+
configSearchPath string
223+
)
224224

225225
m := metrics.New()
226226
if params.metrics {

internal/update/update.go

Lines changed: 62 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ package update
33

44
import (
55
"context"
6+
"errors"
67
"fmt"
78
"io"
8-
"net/http"
9-
"net/url"
109
"os"
1110
"path/filepath"
1211
"strings"
@@ -26,35 +25,35 @@ var updateModule string
2625
const CheckVersionDisableEnvVar = "REGAL_DISABLE_VERSION_CHECK"
2726

2827
type Options struct {
29-
CurrentVersion string
30-
CurrentTime time.Time
31-
32-
StateDir string
33-
28+
CurrentTime time.Time
29+
CurrentVersion string
30+
StateDir string
3431
ReleaseServerHost string
3532
ReleaseServerPath string
36-
37-
CTAURLPrefix string
38-
39-
Debug bool
33+
CTAURLPrefix string
34+
Debug bool
4035
}
4136

4237
type latestVersionFileContents struct {
4338
CheckedAt time.Time `json:"checked_at"`
4439
LatestVersion string `json:"latest_version"`
4540
}
4641

42+
type decision struct {
43+
NeedsUpdate bool `json:"needs_update"`
44+
LatestVersion string `json:"latest_version"`
45+
CTA string `json:"cta"`
46+
}
47+
48+
var query = ast.MustParseBody("result := data.update.check")
49+
4750
func CheckAndWarn(opts Options, w io.Writer) {
48-
// this is a shortcut heuristic to avoid and version checking
49-
// when in dev/test etc.
51+
// this is a shortcut heuristic to avoid version checking when in dev/test etc.
5052
if !strings.HasPrefix(opts.CurrentVersion, "v") {
5153
return
5254
}
5355

54-
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
55-
defer cancel()
56-
57-
latestVersion, err := getLatestVersion(ctx, opts)
56+
latestVersion, err := getLatestCachedVersion(opts)
5857
if err != nil {
5958
if opts.Debug {
6059
w.Write([]byte(err.Error()))
@@ -65,10 +64,13 @@ func CheckAndWarn(opts Options, w io.Writer) {
6564

6665
regoArgs := []func(*rego.Rego){
6766
rego.Module("update.rego", updateModule),
68-
rego.Query(`data.update.needs_update`),
67+
rego.ParsedQuery(query),
6968
rego.ParsedInput(ast.NewObject(
7069
ast.Item(ast.StringTerm("current_version"), ast.StringTerm(opts.CurrentVersion)),
7170
ast.Item(ast.StringTerm("latest_version"), ast.StringTerm(latestVersion)),
71+
ast.Item(ast.StringTerm("cta_url_prefix"), ast.StringTerm(opts.CTAURLPrefix)),
72+
ast.Item(ast.StringTerm("release_server_host"), ast.StringTerm(opts.ReleaseServerHost)),
73+
ast.Item(ast.StringTerm("release_server_path"), ast.StringTerm(opts.ReleaseServerPath)),
7274
)),
7375
}
7476

@@ -81,45 +83,54 @@ func CheckAndWarn(opts Options, w io.Writer) {
8183
return
8284
}
8385

84-
if !rs.Allowed() {
86+
result, err := resultSetToDecision(rs)
87+
if err != nil {
8588
if opts.Debug {
86-
w.Write([]byte("Regal is up to date"))
89+
w.Write([]byte(err.Error()))
90+
}
91+
92+
return
93+
}
94+
95+
if result.NeedsUpdate {
96+
if err = saveLatestCachedVersion(opts, result.LatestVersion); err != nil && opts.Debug {
97+
w.Write([]byte(err.Error()))
8798
}
8899

100+
w.Write([]byte(result.CTA))
101+
89102
return
90103
}
91104

92-
ctaURLPrefix := "https://github.com/StyraInc/regal/releases/tag/"
93-
if opts.CTAURLPrefix != "" {
94-
ctaURLPrefix = opts.CTAURLPrefix
105+
if opts.Debug {
106+
w.Write([]byte("Regal is up to date"))
95107
}
108+
}
96109

97-
ctaURL := ctaURLPrefix + latestVersion
110+
func resultSetToDecision(rs rego.ResultSet) (decision, error) {
111+
if len(rs) == 0 || rs[0].Bindings["result"] == nil {
112+
return decision{}, errors.New("no result set")
113+
}
98114

99-
tmpl := `A new version of Regal is available (%s). You are running %s.
100-
See %s for the latest release.
101-
`
115+
var result decision
116+
if err := encoding.JSONRoundTrip(rs[0].Bindings["result"], &result); err != nil {
117+
return decision{}, fmt.Errorf("failed to decode result set: %w", err)
118+
}
102119

103-
fmt.Fprintf(w, tmpl, latestVersion, opts.CurrentVersion, ctaURL)
120+
return result, nil
104121
}
105122

106-
func getLatestVersion(ctx context.Context, opts Options) (string, error) {
123+
func getLatestCachedVersion(opts Options) (string, error) {
107124
if opts.StateDir != "" {
108125
// first, attempt to get the file from previous invocations to save on remote calls
109126
latestVersionFilePath := filepath.Join(opts.StateDir, "latest_version.json")
110127

111-
_, err := os.Stat(latestVersionFilePath)
112-
if err == nil {
113-
var preExistingState latestVersionFileContents
114-
115-
file, err := os.Open(latestVersionFilePath)
116-
if err != nil {
117-
return "", fmt.Errorf("failed to open file: %w", err)
118-
}
128+
if file, err := os.Open(latestVersionFilePath); err == nil {
129+
defer file.Close()
119130

120-
json := encoding.JSON()
131+
var preExistingState latestVersionFileContents
121132

122-
if err := json.NewDecoder(file).Decode(&preExistingState); err != nil {
133+
if err := encoding.JSON().NewDecoder(file).Decode(&preExistingState); err != nil {
123134
return "", fmt.Errorf("failed to decode existing version state file: %w", err)
124135
}
125136

@@ -129,60 +140,22 @@ func getLatestVersion(ctx context.Context, opts Options) (string, error) {
129140
}
130141
}
131142

132-
client := http.Client{}
143+
return "", nil
144+
}
133145

134-
releaseServerHost := "https://api.github.com"
135-
if opts.ReleaseServerHost != "" {
136-
releaseServerHost = strings.TrimSuffix(opts.ReleaseServerHost, "/")
146+
func saveLatestCachedVersion(opts Options, latestVersion string) error {
147+
if opts.StateDir != "" {
148+
content := latestVersionFileContents{LatestVersion: latestVersion, CheckedAt: opts.CurrentTime}
137149

138-
if !strings.HasPrefix(releaseServerHost, "http") {
139-
releaseServerHost = "https://" + releaseServerHost
150+
bs, err := encoding.JSON().MarshalIndent(content, "", " ")
151+
if err != nil {
152+
return fmt.Errorf("failed to marshal state file: %w", err)
140153
}
141-
}
142-
143-
releaseServerURL, err := url.Parse(releaseServerHost)
144-
if err != nil {
145-
return "", fmt.Errorf("failed to parse release server URL: %w", err)
146-
}
147-
148-
releaseServerPath := "/repos/styrainc/regal/releases/latest"
149-
if opts.ReleaseServerPath != "" {
150-
releaseServerPath = opts.ReleaseServerPath
151-
}
152-
153-
releaseServerURL.Path = releaseServerPath
154-
155-
req, err := http.NewRequestWithContext(ctx, http.MethodGet, releaseServerURL.String(), nil)
156-
if err != nil {
157-
return "", fmt.Errorf("failed to create request: %w", err)
158-
}
159154

160-
resp, err := client.Do(req)
161-
if err != nil {
162-
return "", fmt.Errorf("failed to make request: %w", err)
163-
}
164-
defer resp.Body.Close()
165-
166-
var responseData struct {
167-
TagName string `json:"tag_name"`
168-
}
169-
170-
json := encoding.JSON()
171-
if err = json.NewDecoder(resp.Body).Decode(&responseData); err != nil {
172-
return "", fmt.Errorf("failed to decode response: %w", err)
173-
}
174-
175-
stateBs, err := json.MarshalIndent(latestVersionFileContents{
176-
LatestVersion: responseData.TagName,
177-
CheckedAt: opts.CurrentTime,
178-
}, "", " ")
179-
if err != nil {
180-
return "", fmt.Errorf("failed to marshal state file: %w", err)
181-
}
182-
183-
if err = os.WriteFile(opts.StateDir+"/latest_version.json", stateBs, 0o600); err != nil {
184-
return "", fmt.Errorf("failed to write state file: %w", err)
155+
if err = os.WriteFile(opts.StateDir+"/latest_version.json", bs, 0o600); err != nil {
156+
return fmt.Errorf("failed to write state file: %w", err)
157+
}
185158
}
186159

187-
return responseData.TagName, nil
160+
return nil
188161
}

internal/update/update.rego

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,52 @@
22
# description: utility module to help determine if an update of Regal should be recommended
33
package update
44

5-
default needs_update := false
5+
default check["needs_update"] := false
66

77
# METADATA
88
# description: true if current version is behind latest version
99
# scope: document
10-
needs_update if {
10+
check["needs_update"] if {
1111
current_version := trim(input.current_version, "v")
1212

1313
semver.is_valid(current_version)
14-
semver.compare(current_version, trim(input.latest_version, "v")) == -1
14+
semver.compare(current_version, trim(check.latest_version, "v")) == -1
1515
}
16+
17+
# METADATA
18+
# description: the latest version, as determined by the release server
19+
# scope: document
20+
check["latest_version"] := input.latest_version if input.latest_version != ""
21+
22+
check["latest_version"] := response.body.tag_name if {
23+
input.latest_version == ""
24+
25+
response := http.send({"url": _release_server_url, "method": "GET"})
26+
}
27+
28+
# METADATA
29+
# description: the CTA message, if an update is available
30+
# scope: document
31+
check["cta"] := sprintf(
32+
"A new version of Regal is available (%s). You are running %s.\nSee %s for the latest release.\n",
33+
[check.latest_version, input.current_version, concat("", [_cta_url_prefix, check.latest_version])],
34+
) if {
35+
check.needs_update
36+
}
37+
38+
_release_server_url := concat("", [_release_server_host, _release_server_path])
39+
40+
default _release_server_host := "https://api.github.com"
41+
42+
_release_server_host := _ensure_http(trim_suffix(input.release_server_host, "/")) if input.release_server_host != ""
43+
44+
default _release_server_path := "/repos/styrainc/regal/releases/latest"
45+
46+
_release_server_path := input.release_server_path if input.release_server_path != ""
47+
48+
default _cta_url_prefix := "https://github.com/styrainc/regal/releases/tag/"
49+
50+
_cta_url_prefix := input.cta_url_prefix if input.cta_url_prefix != ""
51+
52+
_ensure_http(url) := url if startswith(url, "http")
53+
_ensure_http(url) := concat("", ["https://", url]) if not startswith(url, "http")

internal/update/update_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ func TestCheckAndWarn(t *testing.T) {
1616

1717
localReleasesServer := httptest.NewServer(
1818
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
19+
w.Header().Set("Content-Type", "application/json")
20+
1921
if _, err := w.Write([]byte(`{"tag_name": "v0.2.0"}`)); err != nil {
2022
t.Fatal(err)
2123
}

0 commit comments

Comments
 (0)