10000 feat: Add support for checking for updates · coder/coder@23a2811 · GitHub
[go: up one dir, main page]

Skip to content

Commit 23a2811

Browse files
committed
feat: Add support for checking for updates
1 parent ec4b397 commit 23a2811

File tree

16 files changed

+654
-9
lines changed

16 files changed

+654
-9
lines changed

buildinfo/buildinfo.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ func VersionsMatch(v1, v2 string) bool {
6868
return semver.MajorMinor(v1) == semver.MajorMinor(v2)
6969
}
7070

71+
// IsDev returns true if this is a development build.
72+
func IsDev() bool {
73+
return strings.HasPrefix(Version(), develPrefix)
74+
}
75+
7176
// ExternalURL returns a URL referencing the current Coder version.
7277
// For production builds, this will link directly to a release.
7378
// For development builds, this will link to a commit.

cli/deployment/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/spf13/viper"
1515
"golang.org/x/xerrors"
1616

17+
"github.com/coder/coder/buildinfo"
1718
"github.com/coder/coder/cli/cliui"
1819
"github.com/coder/coder/cli/config"
1920
"github.com/coder/coder/codersdk"
@@ -325,6 +326,12 @@ func newConfig() *codersdk.DeploymentConfig {
325326
Hidden: true,
326327
Default: 10 * time.Minute,
327328
},
329+
UpdateCheck: &codersdk.DeploymentConfigField[bool]{
330+
Name: "Update Check",
331+
Usage: "Periodically check for new releases of Coder and inform the owner.",
332+
Flag: "update-check",
333+
Default: flag.Lookup("test.v") == nil && !buildinfo.IsDev(),
334+
},
328335
AuditLogging: &codersdk.DeploymentConfigField[bool]{
329336
Name: "Audit Logging",
330337
Usage: "Specifies whether audit logging is enabled.",

cli/deployment/config_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func TestConfig(t *testing.T) {
3838
"CODER_TELEMETRY": "false",
3939
"CODER_TELEMETRY_TRACE": "false",
4040
"CODER_WILDCARD_ACCESS_URL": "something-wildcard.com",
41+
"CODER_UPDATE_CHECK": "false",
4142
},
4243
Valid: func(config *codersdk.DeploymentConfig) {
4344
require.Equal(t, config.Address.Value, "0.0.0.0:8443")
@@ -53,6 +54,7 @@ func TestConfig(t *testing.T) {
5354
require.Equal(t, config.Telemetry.Enable.Value, false)
5455
require.Equal(t, config.Telemetry.Trace.Value, false)
5556
require.Equal(t, config.WildcardAccessURL.Value, "something-wildcard.com")
57+
require.Equal(t, config.UpdateCheck.Value, false)
5658
},
5759
}, {
5860
Name: "DERP",

cli/server.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import (
6262
"github.com/coder/coder/coderd/prometheusmetrics"
6363
"github.com/coder/coder/coderd/telemetry"
6464
"github.com/coder/coder/coderd/tracing"
65+
"github.com/coder/coder/coderd/updatecheck"
6566
"github.com/coder/coder/codersdk"
6667
"github.com/coder/coder/cryptorand"
6768
"github.com/coder/coder/provisioner/echo"
@@ -363,6 +364,25 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
363364
options.TLSCertificates = tlsConfig.Certificates
364365
}
365366

367+
if cfg.UpdateCheck.Value {
368+
options.UpdateCheckOptions = &updatecheck.Options{
369+
// Avoid spamming GitHub API checking for updates.
370+
Interval: 24 * time.Hour,
371+
// Inform server admins of new versions.
372+
Notify: func(r updatecheck.Result) {
373+
if semver.Compare(r.Version, buildinfo.Version()) > 0 {
374+
options.Logger.Info(
375+
context.Background(),
376+
"new version of coder available",
377+
slog.F("new_version", r.Version),
378+
slog.F("url", r.URL),
379+
slog.F("upgrade_instructions", "https://coder.com/docs/coder-oss/latest/admin/upgrade"),
380+
)
381+
}
382+
},
383+
}
384+
}
385+
366386
if cfg.OAuth2.Github.ClientSecret.Value != "" {
367387
options.GithubOAuth2Config, err = configureGithubOAuth2(accessURLParsed,
368388
cfg.OAuth2.Github.ClientID.Value,

coderd/coderd.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
"github.com/coder/coder/coderd/rbac"
4040
"github.com/coder/coder/coderd/telemetry"
4141
"github.com/coder/coder/coderd/tracing"
42+
"github.com/coder/coder/coderd/updatecheck"
4243
"github.com/coder/coder/coderd/workspacequota"
4344
"github.com/coder/coder/coderd/wsconncache"
4445
"github.com/coder/coder/codersdk"
@@ -97,6 +98,7 @@ type Options struct {
9798
AgentStatsRefreshInterval time.Duration
9899
Experimental bool
99100
DeploymentConfig *codersdk.DeploymentConfig
101+
UpdateCheckOptions *updatecheck.Options // Set non-nil to enable update checking.
100102
}
101103

102104
// New constructs a Coder API handler.
@@ -123,12 +125,6 @@ func New(options *Options) *API {
123125
if options.APIRateLimit == 0 {
124126
options.APIRateLimit = 512
125127
}
126-
if options.AgentStatsRefreshInterval == 0 {
127-
options.AgentStatsRefreshInterval = 10 * time.Minute
128-
}
129-
if options.MetricsCacheRefreshInterval == 0 {
130-
options.MetricsCacheRefreshInterval = time.Hour
131-
}
132128
if options.Authorizer == nil {
133129
options.Authorizer = rbac.NewAuthorizer()
134130
}
@@ -176,6 +172,13 @@ func New(options *Options) *API {
176172
Auditor: atomic.Pointer[audit.Auditor]{},
177173
WorkspaceQuotaEnforcer: atomic.Pointer[workspacequota.Enforcer]{},
178174
}
175+
if options.UpdateCheckOptions != nil {
176+
api.updateChecker = updatecheck.New(
177+
options.Database,
178+
options.Logger.Named("update_checker"),
179+
*options.UpdateCheckOptions,
180+
)
181+
}
179182
api.Auditor.Store(&options.Auditor)
180183
api.WorkspaceQuotaEnforcer.Store(&options.WorkspaceQuotaEnforcer)
181184
api.workspaceAgentCache = wsconncache.New(api.dialWorkspaceAgentTailnet, 0)
@@ -302,6 +305,9 @@ func New(options *Options) *API {
302305
})
303306
})
304307
})
308+
r.Route("/updatecheck", func(r chi.Router) {
309+
r.Get("/", api.updateCheck)
310+
})
305311
r.Route("/config", func(r chi.Router) {
306312
r.Use(apiKeyMiddleware)
307313
r.Get("/deployment", api.deploymentConfig)
@@ -591,6 +597,7 @@ type API struct {
591597
RootHandler chi.Router
592598

593599
metricsCache *metricscache.Cache
600+
updateChecker *updatecheck.Checker
594601
siteHandler http.Handler
595602
websocketWaitMutex sync.Mutex
596603
websocketWaitGroup sync.WaitGroup
@@ -604,6 +611,9 @@ func (api *API) Close() error {
604611
api.websocketWaitMutex.Unlock()
605612

606613
api.metricsCache.Close()
614+
if api.updateChecker != nil {
615+
api.updateChecker.Close()
616+
}
607617
coordinator := api.TailnetCoordinator.Load()
608618
if coordinator != nil {
609619
_ = (*coordinator).Close()

coderd/coderdtest/coderdtest.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import (
6161
"github.com/coder/coder/coderd/httpmw"
6262
"github.com/coder/coder/coderd/rbac"
6363
"github.com/coder/coder/coderd/telemetry"
64+
"github.com/coder/coder/coderd/updatecheck"
6465
"github.com/coder/coder/coderd/util/ptr"
6566
"github.com/coder/coder/codersdk"
6667
"github.com/coder/coder/cryptorand"
@@ -97,6 +98,9 @@ type Options struct {
9798
AgentStatsRefreshInterval time.Duration
9899
DeploymentConfig *codersdk.DeploymentConfig
99100

101+
// Set update check options to enable update check.
102+
UpdateCheckOptions *updatecheck.Options
103+
100104
// Overriding the database is heavily discouraged.
101105
// It should only be used in cases where multiple Coder
102106
// test instances are running against the same database.
@@ -275,6 +279,7 @@ func NewOptions(t *testing.T, options *Options) (func(http.Handler), context.Can
275279
MetricsCacheRefreshInterval: options.MetricsCacheRefreshInterval,
276280
AgentStatsRefreshInterval: options.AgentStatsRefreshInterval,
277281
D F438 eploymentConfig: options.DeploymentConfig,
282+
UpdateCheckOptions: options.UpdateCheckOptions,
278283
}
279284
}
280285

coderd/database/databasefake/databasefake.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,10 @@ type data struct {
111111
licenses []database.License
112112
replicas []database.Replica
113113

114-
deploymentID string
115-
derpMeshKey string
116-
lastLicenseID int32
114+
deploymentID string
115+
derpMeshKey string
116+
lastUpdateCheck []byte
117+
lastLicenseID int32
117118
}
118119

119120
func (*fakeQuerier) Ping(_ context.Context) (time.Duration, error) {
@@ -154,6 +155,7 @@ func (q *fakeQuerier) AcquireProvisionerJob(_ context.Context, arg database.Acqu
154155
}
155156
return database.ProvisionerJob{}, sql.ErrNoRows
156157
}
158+
157159
func (*fakeQuerier) DeleteOldAgentStats(_ context.Context) error {
158160
// no-op
159161
return nil
@@ -3108,6 +3110,24 @@ func (q *fakeQuerier) GetDERPMeshKey(_ context.Context) (string, error) {
31083110
return q.derpMeshKey, nil
31093111
}
31103112

3113+
func (q *fakeQuerier) InsertOrUpdateLastUpdateCheck(_ context.Context, data string) error {
3114+
q.mutex.RLock()
3115+
defer q.mutex.RUnlock()
3116+
3117+
q.lastUpdateCheck = []byte(data)
3118+
return nil
3119+
}
3120+
3121+
func (q *fakeQuerier) GetLastUpdateCheck(_ context.Context) (string, error) {
3122+
q.mutex.RLock()
3123+
defer q.mutex.RUnlock()
3124+
3125+
if q.lastUpdateCheck == nil {
3126+
return "", sql.ErrNoRows
3127+
}
3128+
return string(q.lastUpdateCheck), nil
3129+
}
3130+
31113131
func (q *fakeQuerier) InsertLicense(
31123132
_ context.Context, arg database.InsertLicenseParams,
31133133
) (database.License, error) {

coderd/database/querier.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/siteconfig.sql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,10 @@ INSERT INTO site_configs (key, value) VALUES ('derp_mesh_key', $1);
99

1010
-- name: GetDERPMeshKey :one
1111
SELECT value FROM site_configs WHERE key = 'derp_mesh_key';
12+
13+
-- name: InsertOrUpdateLastUpdateCheck :exec
14+
INSERT INTO site_configs (key, value) VALUES ('last_update_check', $1)
15+
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'last_update_check';
16+
17+
-- name: GetLastUpdateCheck :one
18+
SELECT value FROM site_configs WHERE key = 'last_update_check';

0 commit comments

Comments
 (0)
0