@@ -3,10 +3,9 @@ package update
33
44import (
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
2625const CheckVersionDisableEnvVar = "REGAL_DISABLE_VERSION_CHECK"
2726
2827type 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
4237type 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+
4750func 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}
0 commit comments