mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: show agent version in UI and CLI (#3709)
This commit adds the ability for agents to set their version upon start. This is then reported in the UI and CLI.
This commit is contained in:
+11
-1
@@ -20,6 +20,7 @@ import (
|
|||||||
"cdr.dev/slog/sloggers/sloghuman"
|
"cdr.dev/slog/sloggers/sloghuman"
|
||||||
"github.com/coder/coder/agent"
|
"github.com/coder/coder/agent"
|
||||||
"github.com/coder/coder/agent/reaper"
|
"github.com/coder/coder/agent/reaper"
|
||||||
|
"github.com/coder/coder/buildinfo"
|
||||||
"github.com/coder/coder/cli/cliflag"
|
"github.com/coder/coder/cli/cliflag"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
"github.com/coder/retry"
|
"github.com/coder/retry"
|
||||||
@@ -73,7 +74,12 @@ func workspaceAgent() *cobra.Command {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info(cmd.Context(), "starting agent", slog.F("url", coderURL), slog.F("auth", auth))
|
version := buildinfo.Version()
|
||||||
|
logger.Info(cmd.Context(), "starting agent",
|
||||||
|
slog.F("url", coderURL),
|
||||||
|
slog.F("auth", auth),
|
||||||
|
slog.F("version", version),
|
||||||
|
)
|
||||||
client := codersdk.New(coderURL)
|
client := codersdk.New(coderURL)
|
||||||
|
|
||||||
if pprofEnabled {
|
if pprofEnabled {
|
||||||
@@ -172,6 +178,10 @@ func workspaceAgent() *cobra.Command {
|
|||||||
return xerrors.Errorf("add executable to $PATH: %w", err)
|
return xerrors.Errorf("add executable to $PATH: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := client.PostWorkspaceAgentVersion(cmd.Context(), version); err != nil {
|
||||||
|
logger.Error(cmd.Context(), "post agent version: %w", slog.Error(err), slog.F("version", version))
|
||||||
|
}
|
||||||
|
|
||||||
closer := agent.New(client.ListenWorkspaceAgent, &agent.Options{
|
closer := agent.New(client.ListenWorkspaceAgent, &agent.Options{
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
EnvironmentVariables: map[string]string{
|
EnvironmentVariables: map[string]string{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/coder/coder/cli/clitest"
|
"github.com/coder/coder/cli/clitest"
|
||||||
@@ -59,6 +60,9 @@ func TestWorkspaceAgent(t *testing.T) {
|
|||||||
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
|
||||||
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
|
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
if assert.NotEmpty(t, resources) && assert.NotEmpty(t, resources[0].Agents) {
|
||||||
|
assert.NotEmpty(t, resources[0].Agents[0].Version)
|
||||||
|
}
|
||||||
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil)
|
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer dialer.Close()
|
defer dialer.Close()
|
||||||
@@ -114,6 +118,9 @@ func TestWorkspaceAgent(t *testing.T) {
|
|||||||
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
|
||||||
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
|
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
if assert.NotEmpty(t, resources) && assert.NotEmpty(t, resources[0].Agents) {
|
||||||
|
assert.NotEmpty(t, resources[0].Agents[0].Version)
|
||||||
|
}
|
||||||
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil)
|
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer dialer.Close()
|
defer dialer.Close()
|
||||||
@@ -169,6 +176,9 @@ func TestWorkspaceAgent(t *testing.T) {
|
|||||||
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
|
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
|
||||||
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
|
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
if assert.NotEmpty(t, resources) && assert.NotEmpty(t, resources[0].Agents) {
|
||||||
|
assert.NotEmpty(t, resources[0].Agents[0].Version)
|
||||||
|
}
|
||||||
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil)
|
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer dialer.Close()
|
defer dialer.Close()
|
||||||
|
|||||||
+38
-13
@@ -7,6 +7,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/jedib0t/go-pretty/v6/table"
|
"github.com/jedib0t/go-pretty/v6/table"
|
||||||
|
"golang.org/x/mod/semver"
|
||||||
|
|
||||||
"github.com/coder/coder/coderd/database"
|
"github.com/coder/coder/coderd/database"
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ type WorkspaceResourcesOptions struct {
|
|||||||
HideAgentState bool
|
HideAgentState bool
|
||||||
HideAccess bool
|
HideAccess bool
|
||||||
Title string
|
Title string
|
||||||
|
ServerVersion string
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorkspaceResources displays the connection status and tree-view of provided resources.
|
// WorkspaceResources displays the connection status and tree-view of provided resources.
|
||||||
@@ -48,6 +50,7 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
|
|||||||
row := table.Row{"Resource"}
|
row := table.Row{"Resource"}
|
||||||
if !options.HideAgentState {
|
if !options.HideAgentState {
|
||||||
row = append(row, "Status")
|
row = append(row, "Status")
|
||||||
|
row = append(row, "Version")
|
||||||
}
|
}
|
||||||
if !options.HideAccess {
|
if !options.HideAccess {
|
||||||
row = append(row, "Access")
|
row = append(row, "Access")
|
||||||
@@ -91,21 +94,12 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
|
|||||||
}
|
}
|
||||||
if !options.HideAgentState {
|
if !options.HideAgentState {
|
||||||
var agentStatus string
|
var agentStatus string
|
||||||
|
var agentVersion string
|
||||||
if !options.HideAgentState {
|
if !options.HideAgentState {
|
||||||
switch agent.Status {
|
agentStatus = renderAgentStatus(agent)
|
||||||
case codersdk.WorkspaceAgentConnecting:
|
agentVersion = renderAgentVersion(agent.Version, options.ServerVersion)
|
||||||
since := database.Now().Sub(agent.CreatedAt)
|
|
||||||
agentStatus = Styles.Warn.Render("⦾ connecting") + " " +
|
|
||||||
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
|
|
||||||
case codersdk.WorkspaceAgentDisconnected:
|
|
||||||
since := database.Now().Sub(*agent.DisconnectedAt)
|
|
||||||
agentStatus = Styles.Error.Render("⦾ disconnected") + " " +
|
|
||||||
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
|
|
||||||
case codersdk.WorkspaceAgentConnected:
|
|
||||||
agentStatus = Styles.Keyword.Render("⦿ connected")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
row = append(row, agentStatus)
|
row = append(row, agentStatus, agentVersion)
|
||||||
}
|
}
|
||||||
if !options.HideAccess {
|
if !options.HideAccess {
|
||||||
sshCommand := "coder ssh " + options.WorkspaceName
|
sshCommand := "coder ssh " + options.WorkspaceName
|
||||||
@@ -122,3 +116,34 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
|
|||||||
_, err := fmt.Fprintln(writer, tableWriter.Render())
|
_, err := fmt.Fprintln(writer, tableWriter.Render())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func renderAgentStatus(agent codersdk.WorkspaceAgent) string {
|
||||||
|
switch agent.Status {
|
||||||
|
case codersdk.WorkspaceAgentConnecting:
|
||||||
|
since := database.Now().Sub(agent.CreatedAt)
|
||||||
|
return Styles.Warn.Render("⦾ connecting") + " " +
|
||||||
|
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
|
||||||
|
case codersdk.WorkspaceAgentDisconnected:
|
||||||
|
since := database.Now().Sub(*agent.DisconnectedAt)
|
||||||
|
return Styles.Error.Render("⦾ disconnected") + " " +
|
||||||
|
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
|
||||||
|
case codersdk.WorkspaceAgentConnected:
|
||||||
|
return Styles.Keyword.Render("⦿ connected")
|
||||||
|
default:
|
||||||
|
return Styles.Warn.Render("○ unknown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderAgentVersion(agentVersion, serverVersion string) string {
|
||||||
|
if agentVersion == "" {
|
||||||
|
agentVersion = "(unknown)"
|
||||||
|
}
|
||||||
|
if !semver.IsValid(serverVersion) || !semver.IsValid(agentVersion) {
|
||||||
|
return Styles.Placeholder.Render(agentVersion)
|
||||||
|
}
|
||||||
|
outdated := semver.Compare(agentVersion, serverVersion) < 0
|
||||||
|
if outdated {
|
||||||
|
return Styles.Warn.Render(agentVersion + " (outdated)")
|
||||||
|
}
|
||||||
|
return Styles.Keyword.Render(agentVersion)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package cliui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRenderAgentVersion(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
agentVersion string
|
||||||
|
serverVersion string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "OK",
|
||||||
|
agentVersion: "v1.2.3",
|
||||||
|
serverVersion: "v1.2.3",
|
||||||
|
expected: "v1.2.3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Outdated",
|
||||||
|
agentVersion: "v1.2.3",
|
||||||
|
serverVersion: "v1.2.4",
|
||||||
|
expected: "v1.2.3 (outdated)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AgentUnknown",
|
||||||
|
agentVersion: "",
|
||||||
|
serverVersion: "v1.2.4",
|
||||||
|
expected: "(unknown)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ServerUnknown",
|
||||||
|
agentVersion: "v1.2.3",
|
||||||
|
serverVersion: "",
|
||||||
|
expected: "v1.2.3",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
actual := renderAgentVersion(testCase.agentVersion, testCase.serverVersion)
|
||||||
|
assert.Equal(t, testCase.expected, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,10 @@ func show() *cobra.Command {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
buildInfo, err := client.BuildInfo(cmd.Context())
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("get server version: %w", err)
|
||||||
|
}
|
||||||
workspace, err := namedWorkspace(cmd, client, args[0])
|
workspace, err := namedWorkspace(cmd, client, args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("get workspace: %w", err)
|
return xerrors.Errorf("get workspace: %w", err)
|
||||||
@@ -28,6 +32,7 @@ func show() *cobra.Command {
|
|||||||
}
|
}
|
||||||
return cliui.WorkspaceResources(cmd.OutOrStdout(), resources, cliui.WorkspaceResourcesOptions{
|
return cliui.WorkspaceResources(cmd.OutOrStdout(), resources, cliui.WorkspaceResourcesOptions{
|
||||||
WorkspaceName: workspace.Name,
|
WorkspaceName: workspace.Name,
|
||||||
|
ServerVersion: buildInfo.Version,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -333,6 +333,7 @@ func New(options *Options) *API {
|
|||||||
r.Route("/me", func(r chi.Router) {
|
r.Route("/me", func(r chi.Router) {
|
||||||
r.Use(httpmw.ExtractWorkspaceAgent(options.Database))
|
r.Use(httpmw.ExtractWorkspaceAgent(options.Database))
|
||||||
r.Get("/metadata", api.workspaceAgentMetadata)
|
r.Get("/metadata", api.workspaceAgentMetadata)
|
||||||
|
r.Post("/version", api.postWorkspaceAgentVersion)
|
||||||
r.Get("/listen", api.workspaceAgentListen)
|
r.Get("/listen", api.workspaceAgentListen)
|
||||||
r.Get("/gitsshkey", api.agentGitSSHKey)
|
r.Get("/gitsshkey", api.agentGitSSHKey)
|
||||||
r.Get("/turn", api.workspaceAgentTurn)
|
r.Get("/turn", api.workspaceAgentTurn)
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
|
|||||||
"GET:/api/v2/workspaceagents/me/metadata": {NoAuthorize: true},
|
"GET:/api/v2/workspaceagents/me/metadata": {NoAuthorize: true},
|
||||||
"GET:/api/v2/workspaceagents/me/turn": {NoAuthorize: true},
|
"GET:/api/v2/workspaceagents/me/turn": {NoAuthorize: true},
|
||||||
"GET:/api/v2/workspaceagents/me/derp": {NoAuthorize: true},
|
"GET:/api/v2/workspaceagents/me/derp": {NoAuthorize: true},
|
||||||
|
"POST:/api/v2/workspaceagents/me/version": {NoAuthorize: true},
|
||||||
"GET:/api/v2/workspaceagents/me/wireguardlisten": {NoAuthorize: true},
|
"GET:/api/v2/workspaceagents/me/wireguardlisten": {NoAuthorize: true},
|
||||||
"POST:/api/v2/workspaceagents/me/keys": {NoAuthorize: true},
|
"POST:/api/v2/workspaceagents/me/keys": {NoAuthorize: true},
|
||||||
"GET:/api/v2/workspaceagents/{workspaceagent}/iceservers": {NoAuthorize: true},
|
"GET:/api/v2/workspaceagents/{workspaceagent}/iceservers": {NoAuthorize: true},
|
||||||
|
|||||||
@@ -2047,6 +2047,22 @@ func (q *fakeQuerier) UpdateWorkspaceAgentKeysByID(_ context.Context, arg databa
|
|||||||
return sql.ErrNoRows
|
return sql.ErrNoRows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *fakeQuerier) UpdateWorkspaceAgentVersionByID(_ context.Context, arg database.UpdateWorkspaceAgentVersionByIDParams) error {
|
||||||
|
q.mutex.Lock()
|
||||||
|
defer q.mutex.Unlock()
|
||||||
|
|
||||||
|
for index, agent := range q.provisionerJobAgents {
|
||||||
|
if agent.ID != arg.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.Version = arg.Version
|
||||||
|
q.provisionerJobAgents[index] = agent
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return sql.ErrNoRows
|
||||||
|
}
|
||||||
|
|
||||||
func (q *fakeQuerier) UpdateProvisionerJobByID(_ context.Context, arg database.UpdateProvisionerJobByIDParams) error {
|
func (q *fakeQuerier) UpdateProvisionerJobByID(_ context.Context, arg database.UpdateProvisionerJobByIDParams) error {
|
||||||
q.mutex.Lock()
|
q.mutex.Lock()
|
||||||
defer q.mutex.Unlock()
|
defer q.mutex.Unlock()
|
||||||
|
|||||||
Generated
+4
-1
@@ -308,9 +308,12 @@ CREATE TABLE workspace_agents (
|
|||||||
directory character varying(4096) DEFAULT ''::character varying NOT NULL,
|
directory character varying(4096) DEFAULT ''::character varying NOT NULL,
|
||||||
wireguard_node_ipv6 inet DEFAULT '::'::inet NOT NULL,
|
wireguard_node_ipv6 inet DEFAULT '::'::inet NOT NULL,
|
||||||
wireguard_node_public_key character varying(128) DEFAULT 'nodekey:0000000000000000000000000000000000000000000000000000000000000000'::character varying NOT NULL,
|
wireguard_node_public_key character varying(128) DEFAULT 'nodekey:0000000000000000000000000000000000000000000000000000000000000000'::character varying NOT NULL,
|
||||||
wireguard_disco_public_key character varying(128) DEFAULT 'discokey:0000000000000000000000000000000000000000000000000000000000000000'::character varying NOT NULL
|
wireguard_disco_public_key character varying(128) DEFAULT 'discokey:0000000000000000000000000000000000000000000000000000000000000000'::character varying NOT NULL,
|
||||||
|
version text DEFAULT ''::text NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN workspace_agents.version IS 'Version tracks the version of the currently running workspace agent. Workspace agents register their version upon start.';
|
||||||
|
|
||||||
CREATE TABLE workspace_apps (
|
CREATE TABLE workspace_apps (
|
||||||
id uuid NOT NULL,
|
id uuid NOT NULL,
|
||||||
created_at timestamp with time zone NOT NULL,
|
created_at timestamp with time zone NOT NULL,
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE ONLY workspace_agents DROP COLUMN version;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE ONLY workspace_agents ADD COLUMN version text DEFAULT ''::text NOT NULL;
|
||||||
|
COMMENT ON COLUMN workspace_agents.version IS 'Version tracks the version of the currently running workspace agent. Workspace agents register their version upon start.';
|
||||||
@@ -536,6 +536,8 @@ type WorkspaceAgent struct {
|
|||||||
WireguardNodeIPv6 pqtype.Inet `db:"wireguard_node_ipv6" json:"wireguard_node_ipv6"`
|
WireguardNodeIPv6 pqtype.Inet `db:"wireguard_node_ipv6" json:"wireguard_node_ipv6"`
|
||||||
WireguardNodePublicKey dbtypes.NodePublic `db:"wireguard_node_public_key" json:"wireguard_node_public_key"`
|
WireguardNodePublicKey dbtypes.NodePublic `db:"wireguard_node_public_key" json:"wireguard_node_public_key"`
|
||||||
WireguardDiscoPublicKey dbtypes.DiscoPublic `db:"wireguard_disco_public_key" json:"wireguard_disco_public_key"`
|
WireguardDiscoPublicKey dbtypes.DiscoPublic `db:"wireguard_disco_public_key" json:"wireguard_disco_public_key"`
|
||||||
|
// Version tracks the version of the currently running workspace agent. Workspace agents register their version upon start.
|
||||||
|
Version string `db:"version" json:"version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkspaceApp struct {
|
type WorkspaceApp struct {
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ type querier interface {
|
|||||||
UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (Workspace, error)
|
UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (Workspace, error)
|
||||||
UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error
|
UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error
|
||||||
UpdateWorkspaceAgentKeysByID(ctx context.Context, arg UpdateWorkspaceAgentKeysByIDParams) error
|
UpdateWorkspaceAgentKeysByID(ctx context.Context, arg UpdateWorkspaceAgentKeysByIDParams) error
|
||||||
|
UpdateWorkspaceAgentVersionByID(ctx context.Context, arg UpdateWorkspaceAgentVersionByIDParams) error
|
||||||
UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error
|
UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error
|
||||||
UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) error
|
UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) error
|
||||||
UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error
|
UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error
|
||||||
|
|||||||
@@ -3158,7 +3158,7 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP
|
|||||||
|
|
||||||
const getWorkspaceAgentByAuthToken = `-- name: GetWorkspaceAgentByAuthToken :one
|
const getWorkspaceAgentByAuthToken = `-- name: GetWorkspaceAgentByAuthToken :one
|
||||||
SELECT
|
SELECT
|
||||||
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, wireguard_node_ipv6, wireguard_node_public_key, wireguard_disco_public_key
|
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, wireguard_node_ipv6, wireguard_node_public_key, wireguard_disco_public_key, version
|
||||||
FROM
|
FROM
|
||||||
workspace_agents
|
workspace_agents
|
||||||
WHERE
|
WHERE
|
||||||
@@ -3191,13 +3191,14 @@ func (q *sqlQuerier) GetWorkspaceAgentByAuthToken(ctx context.Context, authToken
|
|||||||
&i.WireguardNodeIPv6,
|
&i.WireguardNodeIPv6,
|
||||||
&i.WireguardNodePublicKey,
|
&i.WireguardNodePublicKey,
|
||||||
&i.WireguardDiscoPublicKey,
|
&i.WireguardDiscoPublicKey,
|
||||||
|
&i.Version,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getWorkspaceAgentByID = `-- name: GetWorkspaceAgentByID :one
|
const getWorkspaceAgentByID = `-- name: GetWorkspaceAgentByID :one
|
||||||
SELECT
|
SELECT
|
||||||
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, wireguard_node_ipv6, wireguard_node_public_key, wireguard_disco_public_key
|
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, wireguard_node_ipv6, wireguard_node_public_key, wireguard_disco_public_key, version
|
||||||
FROM
|
FROM
|
||||||
workspace_agents
|
workspace_agents
|
||||||
WHERE
|
WHERE
|
||||||
@@ -3228,13 +3229,14 @@ func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (W
|
|||||||
&i.WireguardNodeIPv6,
|
&i.WireguardNodeIPv6,
|
||||||
&i.WireguardNodePublicKey,
|
&i.WireguardNodePublicKey,
|
||||||
&i.WireguardDiscoPublicKey,
|
&i.WireguardDiscoPublicKey,
|
||||||
|
&i.Version,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getWorkspaceAgentByInstanceID = `-- name: GetWorkspaceAgentByInstanceID :one
|
const getWorkspaceAgentByInstanceID = `-- name: GetWorkspaceAgentByInstanceID :one
|
||||||
SELECT
|
SELECT
|
||||||
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, wireguard_node_ipv6, wireguard_node_public_key, wireguard_disco_public_key
|
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, wireguard_node_ipv6, wireguard_node_public_key, wireguard_disco_public_key, version
|
||||||
FROM
|
FROM
|
||||||
workspace_agents
|
workspace_agents
|
||||||
WHERE
|
WHERE
|
||||||
@@ -3267,13 +3269,14 @@ func (q *sqlQuerier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInst
|
|||||||
&i.WireguardNodeIPv6,
|
&i.WireguardNodeIPv6,
|
||||||
&i.WireguardNodePublicKey,
|
&i.WireguardNodePublicKey,
|
||||||
&i.WireguardDiscoPublicKey,
|
&i.WireguardDiscoPublicKey,
|
||||||
|
&i.Version,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many
|
const getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many
|
||||||
SELECT
|
SELECT
|
||||||
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, wireguard_node_ipv6, wireguard_node_public_key, wireguard_disco_public_key
|
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, wireguard_node_ipv6, wireguard_node_public_key, wireguard_disco_public_key, version
|
||||||
FROM
|
FROM
|
||||||
workspace_agents
|
workspace_agents
|
||||||
WHERE
|
WHERE
|
||||||
@@ -3310,6 +3313,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []
|
|||||||
&i.WireguardNodeIPv6,
|
&i.WireguardNodeIPv6,
|
||||||
&i.WireguardNodePublicKey,
|
&i.WireguardNodePublicKey,
|
||||||
&i.WireguardDiscoPublicKey,
|
&i.WireguardDiscoPublicKey,
|
||||||
|
&i.Version,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -3325,7 +3329,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getWorkspaceAgentsCreatedAfter = `-- name: GetWorkspaceAgentsCreatedAfter :many
|
const getWorkspaceAgentsCreatedAfter = `-- name: GetWorkspaceAgentsCreatedAfter :many
|
||||||
SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, wireguard_node_ipv6, wireguard_node_public_key, wireguard_disco_public_key FROM workspace_agents WHERE created_at > $1
|
SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, wireguard_node_ipv6, wireguard_node_public_key, wireguard_disco_public_key, version FROM workspace_agents WHERE created_at > $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) {
|
func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) {
|
||||||
@@ -3358,6 +3362,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created
|
|||||||
&i.WireguardNodeIPv6,
|
&i.WireguardNodeIPv6,
|
||||||
&i.WireguardNodePublicKey,
|
&i.WireguardNodePublicKey,
|
||||||
&i.WireguardDiscoPublicKey,
|
&i.WireguardDiscoPublicKey,
|
||||||
|
&i.Version,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -3394,7 +3399,7 @@ INSERT INTO
|
|||||||
wireguard_disco_public_key
|
wireguard_disco_public_key
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, wireguard_node_ipv6, wireguard_node_public_key, wireguard_disco_public_key
|
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, wireguard_node_ipv6, wireguard_node_public_key, wireguard_disco_public_key, version
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertWorkspaceAgentParams struct {
|
type InsertWorkspaceAgentParams struct {
|
||||||
@@ -3459,6 +3464,7 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa
|
|||||||
&i.WireguardNodeIPv6,
|
&i.WireguardNodeIPv6,
|
||||||
&i.WireguardNodePublicKey,
|
&i.WireguardNodePublicKey,
|
||||||
&i.WireguardDiscoPublicKey,
|
&i.WireguardDiscoPublicKey,
|
||||||
|
&i.Version,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -3522,6 +3528,25 @@ func (q *sqlQuerier) UpdateWorkspaceAgentKeysByID(ctx context.Context, arg Updat
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateWorkspaceAgentVersionByID = `-- name: UpdateWorkspaceAgentVersionByID :exec
|
||||||
|
UPDATE
|
||||||
|
workspace_agents
|
||||||
|
SET
|
||||||
|
version = $2
|
||||||
|
WHERE
|
||||||
|
id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateWorkspaceAgentVersionByIDParams struct {
|
||||||
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
|
Version string `db:"version" json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *sqlQuerier) UpdateWorkspaceAgentVersionByID(ctx context.Context, arg UpdateWorkspaceAgentVersionByIDParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, updateWorkspaceAgentVersionByID, arg.ID, arg.Version)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const getWorkspaceAppByAgentIDAndName = `-- name: GetWorkspaceAppByAgentIDAndName :one
|
const getWorkspaceAppByAgentIDAndName = `-- name: GetWorkspaceAppByAgentIDAndName :one
|
||||||
SELECT id, created_at, agent_id, name, icon, command, url, relative_path FROM workspace_apps WHERE agent_id = $1 AND name = $2
|
SELECT id, created_at, agent_id, name, icon, command, url, relative_path FROM workspace_apps WHERE agent_id = $1 AND name = $2
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -81,3 +81,11 @@ SET
|
|||||||
updated_at = $4
|
updated_at = $4
|
||||||
WHERE
|
WHERE
|
||||||
id = $1;
|
id = $1;
|
||||||
|
|
||||||
|
-- name: UpdateWorkspaceAgentVersionByID :exec
|
||||||
|
UPDATE
|
||||||
|
workspace_agents
|
||||||
|
SET
|
||||||
|
version = $2
|
||||||
|
WHERE
|
||||||
|
id = $1;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/hashicorp/yamux"
|
"github.com/hashicorp/yamux"
|
||||||
"github.com/tabbed/pqtype"
|
"github.com/tabbed/pqtype"
|
||||||
|
"golang.org/x/mod/semver"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
"nhooyr.io/websocket"
|
"nhooyr.io/websocket"
|
||||||
@@ -152,6 +153,46 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *API) postWorkspaceAgentVersion(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
workspaceAgent := httpmw.WorkspaceAgent(r)
|
||||||
|
apiAgent, err := convertWorkspaceAgent(workspaceAgent, nil, api.AgentInactiveDisconnectTimeout)
|
||||||
|
if err != nil {
|
||||||
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Internal error reading workspace agent.",
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req codersdk.PostWorkspaceAgentVersionRequest
|
||||||
|
if !httpapi.Read(rw, r, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.Logger.Info(r.Context(), "post workspace agent version", slog.F("agent_id", apiAgent.ID), slog.F("agent_version", req.Version))
|
||||||
|
|
||||||
|
if !semver.IsValid(req.Version) {
|
||||||
|
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: "Invalid workspace agent version provided.",
|
||||||
|
Detail: fmt.Sprintf("invalid semver version: %q", req.Version),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := api.Database.UpdateWorkspaceAgentVersionByID(r.Context(), database.UpdateWorkspaceAgentVersionByIDParams{
|
||||||
|
ID: apiAgent.ID,
|
||||||
|
Version: req.Version,
|
||||||
|
}); err != nil {
|
||||||
|
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Error setting agent version",
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpapi.Write(rw, http.StatusOK, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func (api *API) workspaceAgentListen(rw http.ResponseWriter, r *http.Request) {
|
func (api *API) workspaceAgentListen(rw http.ResponseWriter, r *http.Request) {
|
||||||
api.websocketWaitMutex.Lock()
|
api.websocketWaitMutex.Lock()
|
||||||
api.websocketWaitGroup.Add(1)
|
api.websocketWaitGroup.Add(1)
|
||||||
@@ -707,6 +748,7 @@ func convertWorkspaceAgent(dbAgent database.WorkspaceAgent, apps []codersdk.Work
|
|||||||
IPv6: inetToNetaddr(dbAgent.WireguardNodeIPv6),
|
IPv6: inetToNetaddr(dbAgent.WireguardNodeIPv6),
|
||||||
WireguardPublicKey: key.NodePublic(dbAgent.WireguardNodePublicKey),
|
WireguardPublicKey: key.NodePublic(dbAgent.WireguardNodePublicKey),
|
||||||
DiscoPublicKey: key.DiscoPublic(dbAgent.WireguardDiscoPublicKey),
|
DiscoPublicKey: key.DiscoPublic(dbAgent.WireguardDiscoPublicKey),
|
||||||
|
Version: dbAgent.Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
if dbAgent.FirstConnectedAt.Valid {
|
if dbAgent.FirstConnectedAt.Valid {
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ type WorkspaceAgentAuthenticateResponse struct {
|
|||||||
SessionToken string `json:"session_token"`
|
SessionToken string `json:"session_token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PostWorkspaceAgentVersionRequest struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
// AuthWorkspaceGoogleInstanceIdentity uses the Google Compute Engine Metadata API to
|
// AuthWorkspaceGoogleInstanceIdentity uses the Google Compute Engine Metadata API to
|
||||||
// fetch a signed JWT, and exchange it for a session token for a workspace agent.
|
// fetch a signed JWT, and exchange it for a session token for a workspace agent.
|
||||||
//
|
//
|
||||||
@@ -240,6 +244,8 @@ func (c *Client) ListenWorkspaceAgent(ctx context.Context, logger slog.Logger) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return agent.Metadata{}, nil, xerrors.Errorf("listen peerbroker: %w", err)
|
return agent.Metadata{}, nil, xerrors.Errorf("listen peerbroker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch updated agent metadata
|
||||||
res, err = c.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/metadata", nil)
|
res, err = c.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/metadata", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return agent.Metadata{}, nil, err
|
return agent.Metadata{}, nil, err
|
||||||
@@ -429,6 +435,19 @@ func (c *Client) WorkspaceAgent(ctx context.Context, id uuid.UUID) (WorkspaceAge
|
|||||||
return workspaceAgent, json.NewDecoder(res.Body).Decode(&workspaceAgent)
|
return workspaceAgent, json.NewDecoder(res.Body).Decode(&workspaceAgent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) PostWorkspaceAgentVersion(ctx context.Context, version string) error {
|
||||||
|
// Phone home and tell the mothership what version we're on.
|
||||||
|
versionReq := PostWorkspaceAgentVersionRequest{Version: version}
|
||||||
|
res, err := c.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/me/version", versionReq)
|
||||||
|
if err != nil {
|
||||||
|
return readBodyAsError(res)
|
||||||
|
}
|
||||||
|
// Discord the response
|
||||||
|
_, _ = io.Copy(io.Discard, res.Body)
|
||||||
|
_ = res.Body.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// WorkspaceAgentReconnectingPTY spawns a PTY that reconnects using the token provided.
|
// WorkspaceAgentReconnectingPTY spawns a PTY that reconnects using the token provided.
|
||||||
// It communicates using `agent.ReconnectingPTYRequest` marshaled as JSON.
|
// It communicates using `agent.ReconnectingPTYRequest` marshaled as JSON.
|
||||||
// Responses are PTY output that can be rendered.
|
// Responses are PTY output that can be rendered.
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ type WorkspaceAgent struct {
|
|||||||
WireguardPublicKey key.NodePublic `json:"wireguard_public_key"`
|
WireguardPublicKey key.NodePublic `json:"wireguard_public_key"`
|
||||||
DiscoPublicKey key.DiscoPublic `json:"disco_public_key"`
|
DiscoPublicKey key.DiscoPublic `json:"disco_public_key"`
|
||||||
IPv6 netaddr.IPPrefix `json:"ipv6"`
|
IPv6 netaddr.IPPrefix `json:"ipv6"`
|
||||||
|
Version string `json:"version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkspaceAgentResourceMetadata struct {
|
type WorkspaceAgentResourceMetadata struct {
|
||||||
|
|||||||
@@ -81,6 +81,7 @@
|
|||||||
"@types/react": "18.0.15",
|
"@types/react": "18.0.15",
|
||||||
"@types/react-dom": "18.0.6",
|
"@types/react-dom": "18.0.6",
|
||||||
"@types/react-helmet": "6.1.5",
|
"@types/react-helmet": "6.1.5",
|
||||||
|
"@types/semver": "^7.3.12",
|
||||||
"@types/superagent": "4.1.15",
|
"@types/superagent": "4.1.15",
|
||||||
"@types/uuid": "8.3.4",
|
"@types/uuid": "8.3.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.31.0",
|
"@typescript-eslint/eslint-plugin": "5.31.0",
|
||||||
@@ -112,6 +113,7 @@
|
|||||||
"prettier": "2.7.1",
|
"prettier": "2.7.1",
|
||||||
"prettier-plugin-organize-imports": "3.0.0",
|
"prettier-plugin-organize-imports": "3.0.0",
|
||||||
"react-hot-loader": "4.13.0",
|
"react-hot-loader": "4.13.0",
|
||||||
|
"semver": "^7.3.7",
|
||||||
"sql-formatter": "8.2.0",
|
"sql-formatter": "8.2.0",
|
||||||
"style-loader": "3.3.1",
|
"style-loader": "3.3.1",
|
||||||
"ts-jest": "27.1.4",
|
"ts-jest": "27.1.4",
|
||||||
|
|||||||
@@ -249,6 +249,11 @@ export interface ParameterSchema {
|
|||||||
readonly validation_contains?: string[]
|
readonly validation_contains?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From codersdk/workspaceagents.go
|
||||||
|
export interface PostWorkspaceAgentVersionRequest {
|
||||||
|
readonly version: string
|
||||||
|
}
|
||||||
|
|
||||||
// From codersdk/provisionerdaemons.go
|
// From codersdk/provisionerdaemons.go
|
||||||
export interface ProvisionerDaemon {
|
export interface ProvisionerDaemon {
|
||||||
readonly id: string
|
readonly id: string
|
||||||
@@ -479,6 +484,7 @@ export interface WorkspaceAgent {
|
|||||||
// Named type "inet.af/netaddr.IPPrefix" unknown, using "any"
|
// Named type "inet.af/netaddr.IPPrefix" unknown, using "any"
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
readonly ipv6: any
|
readonly ipv6: any
|
||||||
|
readonly version: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// From codersdk/workspaceagents.go
|
// From codersdk/workspaceagents.go
|
||||||
|
|||||||
@@ -9,14 +9,15 @@ import useTheme from "@material-ui/styles/useTheme"
|
|||||||
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
|
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
|
||||||
import { TableCellDataPrimary } from "components/TableCellData/TableCellData"
|
import { TableCellDataPrimary } from "components/TableCellData/TableCellData"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { getDisplayAgentStatus } from "util/workspace"
|
import { getDisplayAgentStatus, getDisplayVersionStatus } from "util/workspace"
|
||||||
import { Workspace, WorkspaceResource } from "../../api/typesGenerated"
|
import { BuildInfoResponse, Workspace, WorkspaceResource } from "../../api/typesGenerated"
|
||||||
import { AppLink } from "../AppLink/AppLink"
|
import { AppLink } from "../AppLink/AppLink"
|
||||||
import { SSHButton } from "../SSHButton/SSHButton"
|
import { SSHButton } from "../SSHButton/SSHButton"
|
||||||
import { Stack } from "../Stack/Stack"
|
import { Stack } from "../Stack/Stack"
|
||||||
import { TableHeaderRow } from "../TableHeaders/TableHeaders"
|
import { TableHeaderRow } from "../TableHeaders/TableHeaders"
|
||||||
import { TerminalLink } from "../TerminalLink/TerminalLink"
|
import { TerminalLink } from "../TerminalLink/TerminalLink"
|
||||||
import { AgentHelpTooltip } from "../Tooltips/AgentHelpTooltip"
|
import { AgentHelpTooltip } from "../Tooltips/AgentHelpTooltip"
|
||||||
|
import { AgentOutdatedTooltip } from "../Tooltips/AgentOutdatedTooltip"
|
||||||
import { ResourcesHelpTooltip } from "../Tooltips/ResourcesHelpTooltip"
|
import { ResourcesHelpTooltip } from "../Tooltips/ResourcesHelpTooltip"
|
||||||
import { ResourceAvatarData } from "./ResourceAvatarData"
|
import { ResourceAvatarData } from "./ResourceAvatarData"
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ const Language = {
|
|||||||
agentsLabel: "Agents",
|
agentsLabel: "Agents",
|
||||||
agentLabel: "Agent",
|
agentLabel: "Agent",
|
||||||
statusLabel: "status: ",
|
statusLabel: "status: ",
|
||||||
|
versionLabel: "version: ",
|
||||||
osLabel: "os: ",
|
osLabel: "os: ",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ interface ResourcesProps {
|
|||||||
getResourcesError?: Error | unknown
|
getResourcesError?: Error | unknown
|
||||||
workspace: Workspace
|
workspace: Workspace
|
||||||
canUpdateWorkspace: boolean
|
canUpdateWorkspace: boolean
|
||||||
|
buildInfo?: BuildInfoResponse | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
|
export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
|
||||||
@@ -41,9 +44,11 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
|
|||||||
getResourcesError,
|
getResourcesError,
|
||||||
workspace,
|
workspace,
|
||||||
canUpdateWorkspace,
|
canUpdateWorkspace,
|
||||||
|
buildInfo,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const theme: Theme = useTheme()
|
const theme: Theme = useTheme()
|
||||||
|
const serverVersion = buildInfo?.version || ""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div aria-label={Language.resources} className={styles.wrapper}>
|
<div aria-label={Language.resources} className={styles.wrapper}>
|
||||||
@@ -90,6 +95,10 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { displayVersion, outdated } = getDisplayVersionStatus(
|
||||||
|
agent.version,
|
||||||
|
serverVersion,
|
||||||
|
)
|
||||||
const agentStatus = getDisplayAgentStatus(theme, agent)
|
const agentStatus = getDisplayAgentStatus(theme, agent)
|
||||||
return (
|
return (
|
||||||
<TableRow key={`${resource.id}-${agent.id}`}>
|
<TableRow key={`${resource.id}-${agent.id}`}>
|
||||||
@@ -114,6 +123,11 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
|
|||||||
<strong>{Language.osLabel}</strong>
|
<strong>{Language.osLabel}</strong>
|
||||||
<span className={styles.operatingSystem}>{agent.operating_system}</span>
|
<span className={styles.operatingSystem}>{agent.operating_system}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.dataRow}>
|
||||||
|
<strong>{Language.versionLabel}</strong>
|
||||||
|
<span className={styles.agentVersion}>{displayVersion}</span>
|
||||||
|
<AgentOutdatedTooltip outdated={outdated} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -188,6 +202,10 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
textTransform: "capitalize",
|
textTransform: "capitalize",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
agentVersion: {
|
||||||
|
display: "block",
|
||||||
|
},
|
||||||
|
|
||||||
accessLinks: {
|
accessLinks: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: theme.spacing(0.5),
|
gap: theme.spacing(0.5),
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { FC } from "react"
|
||||||
|
import { HelpTooltip, HelpTooltipText, HelpTooltipTitle } from "./HelpTooltip"
|
||||||
|
|
||||||
|
export const Language = {
|
||||||
|
label: "Agent Outdated",
|
||||||
|
text: "This agent is an older version than the Coder server. This can happen after you update Coder with running workspaces. To fix this, you can stop and start the workspace.",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TooltipProps {
|
||||||
|
outdated: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AgentOutdatedTooltip: FC<React.PropsWithChildren<TooltipProps>> = ({ outdated }) => {
|
||||||
|
if (!outdated) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<HelpTooltip size="small">
|
||||||
|
<HelpTooltipTitle>{Language.label}</HelpTooltipTitle>
|
||||||
|
<HelpTooltipText>{Language.text}</HelpTooltipText>
|
||||||
|
</HelpTooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@ Started.args = {
|
|||||||
builds: [Mocks.MockWorkspaceBuild],
|
builds: [Mocks.MockWorkspaceBuild],
|
||||||
canUpdateWorkspace: true,
|
canUpdateWorkspace: true,
|
||||||
workspaceErrors: {},
|
workspaceErrors: {},
|
||||||
|
buildInfo: Mocks.MockBuildInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WithoutUpdateAccess = Template.bind({})
|
export const WithoutUpdateAccess = Template.bind({})
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export interface WorkspaceProps {
|
|||||||
builds?: TypesGen.WorkspaceBuild[]
|
builds?: TypesGen.WorkspaceBuild[]
|
||||||
canUpdateWorkspace: boolean
|
canUpdateWorkspace: boolean
|
||||||
workspaceErrors: Partial<Record<WorkspaceErrors, Error | unknown>>
|
workspaceErrors: Partial<Record<WorkspaceErrors, Error | unknown>>
|
||||||
|
buildInfo?: TypesGen.BuildInfoResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,6 +63,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||||||
builds,
|
builds,
|
||||||
canUpdateWorkspace,
|
canUpdateWorkspace,
|
||||||
workspaceErrors,
|
workspaceErrors,
|
||||||
|
buildInfo,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -128,6 +130,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||||||
getResourcesError={workspaceErrors[WorkspaceErrors.GET_RESOURCES_ERROR]}
|
getResourcesError={workspaceErrors[WorkspaceErrors.GET_RESOURCES_ERROR]}
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
canUpdateWorkspace={canUpdateWorkspace}
|
canUpdateWorkspace={canUpdateWorkspace}
|
||||||
|
buildInfo={buildInfo}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ describe("WorkspacePage", () => {
|
|||||||
const agent1Status = await screen.findAllByText(
|
const agent1Status = await screen.findAllByText(
|
||||||
DisplayAgentStatusLanguage[MockWorkspaceAgent.status],
|
DisplayAgentStatusLanguage[MockWorkspaceAgent.status],
|
||||||
)
|
)
|
||||||
expect(agent1Status.length).toEqual(2)
|
expect(agent1Status.length).toEqual(4)
|
||||||
const agent2Status = await screen.findAllByText(
|
const agent2Status = await screen.findAllByText(
|
||||||
DisplayAgentStatusLanguage[MockWorkspaceAgentDisconnected.status],
|
DisplayAgentStatusLanguage[MockWorkspaceAgentDisconnected.status],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { makeStyles } from "@material-ui/core/styles"
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
import { useMachine, useSelector } from "@xstate/react"
|
import { useActor, useMachine, useSelector } from "@xstate/react"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import minMax from "dayjs/plugin/minMax"
|
import minMax from "dayjs/plugin/minMax"
|
||||||
import { FC, useContext, useEffect } from "react"
|
import { FC, useContext, useEffect } from "react"
|
||||||
@@ -51,6 +51,7 @@ export const WorkspacePage: FC = () => {
|
|||||||
const canUpdateWorkspace = !!permissions?.updateWorkspace
|
const canUpdateWorkspace = !!permissions?.updateWorkspace
|
||||||
|
|
||||||
const [bannerState, bannerSend] = useMachine(workspaceScheduleBannerMachine)
|
const [bannerState, bannerSend] = useMachine(workspaceScheduleBannerMachine)
|
||||||
|
const [buildInfoState] = useActor(xServices.buildInfoXService)
|
||||||
|
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
@@ -133,6 +134,7 @@ export const WorkspacePage: FC = () => {
|
|||||||
[WorkspaceErrors.BUILD_ERROR]: buildError,
|
[WorkspaceErrors.BUILD_ERROR]: buildError,
|
||||||
[WorkspaceErrors.CANCELLATION_ERROR]: cancellationError,
|
[WorkspaceErrors.CANCELLATION_ERROR]: cancellationError,
|
||||||
}}
|
}}
|
||||||
|
buildInfo={buildInfoState.context.buildInfo}
|
||||||
/>
|
/>
|
||||||
<DeleteWorkspaceDialog
|
<DeleteWorkspaceDialog
|
||||||
isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })}
|
isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })}
|
||||||
|
|||||||
@@ -305,6 +305,7 @@ export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = {
|
|||||||
wireguard_public_key: "",
|
wireguard_public_key: "",
|
||||||
disco_public_key: "",
|
disco_public_key: "",
|
||||||
ipv6: "",
|
ipv6: "",
|
||||||
|
version: MockBuildInfo.version,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = {
|
export const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = {
|
||||||
@@ -312,10 +313,19 @@ export const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = {
|
|||||||
id: "test-workspace-agent-2",
|
id: "test-workspace-agent-2",
|
||||||
name: "another-workspace-agent",
|
name: "another-workspace-agent",
|
||||||
status: "disconnected",
|
status: "disconnected",
|
||||||
|
version: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MockWorkspaceAgentOutdated: TypesGen.WorkspaceAgent = {
|
||||||
|
...MockWorkspaceAgent,
|
||||||
|
id: "test-workspace-agent-3",
|
||||||
|
name: "an-outdated-workspace-agent",
|
||||||
|
version: "v99.999.9998+abcdef",
|
||||||
|
operating_system: "Windows",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MockWorkspaceResource: TypesGen.WorkspaceResource = {
|
export const MockWorkspaceResource: TypesGen.WorkspaceResource = {
|
||||||
agents: [MockWorkspaceAgent, MockWorkspaceAgentDisconnected],
|
agents: [MockWorkspaceAgent, MockWorkspaceAgentDisconnected, MockWorkspaceAgentOutdated],
|
||||||
created_at: "",
|
created_at: "",
|
||||||
id: "test-workspace-resource",
|
id: "test-workspace-resource",
|
||||||
job_id: "",
|
job_id: "",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as TypesGen from "../api/typesGenerated"
|
|||||||
import * as Mocks from "../testHelpers/entities"
|
import * as Mocks from "../testHelpers/entities"
|
||||||
import {
|
import {
|
||||||
defaultWorkspaceExtension,
|
defaultWorkspaceExtension,
|
||||||
|
getDisplayVersionStatus,
|
||||||
getDisplayWorkspaceBuildInitiatedBy,
|
getDisplayWorkspaceBuildInitiatedBy,
|
||||||
isWorkspaceDeleted,
|
isWorkspaceDeleted,
|
||||||
isWorkspaceOn,
|
isWorkspaceOn,
|
||||||
@@ -128,4 +129,23 @@ describe("util > workspace", () => {
|
|||||||
expect(getDisplayWorkspaceBuildInitiatedBy(build)).toEqual(initiatedBy)
|
expect(getDisplayWorkspaceBuildInitiatedBy(build)).toEqual(initiatedBy)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("getDisplayVersionStatus", () => {
|
||||||
|
it.each<[string, string, string, boolean]>([
|
||||||
|
["", "", "(unknown)", false],
|
||||||
|
["", "v1.2.3", "(unknown)", false],
|
||||||
|
["v1.2.3", "", "v1.2.3", false],
|
||||||
|
["v1.2.3", "v1.2.3", "v1.2.3", false],
|
||||||
|
["v1.2.3", "v1.2.4", "v1.2.3 (outdated)", true],
|
||||||
|
["v1.2.4", "v1.2.3", "v1.2.4", false],
|
||||||
|
["foo", "bar", "foo", false],
|
||||||
|
])(
|
||||||
|
`getDisplayVersionStatus(theme, %p, %p) returns (%p, %p)`,
|
||||||
|
(agentVersion, serverVersion, expectedVersion, expectedOutdated) => {
|
||||||
|
const { displayVersion, outdated } = getDisplayVersionStatus(agentVersion, serverVersion)
|
||||||
|
expect(displayVersion).toEqual(expectedVersion)
|
||||||
|
expect(expectedOutdated).toEqual(outdated)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import dayjs from "dayjs"
|
|||||||
import duration from "dayjs/plugin/duration"
|
import duration from "dayjs/plugin/duration"
|
||||||
import minMax from "dayjs/plugin/minMax"
|
import minMax from "dayjs/plugin/minMax"
|
||||||
import utc from "dayjs/plugin/utc"
|
import utc from "dayjs/plugin/utc"
|
||||||
|
import semver from "semver"
|
||||||
import { WorkspaceBuildTransition } from "../api/types"
|
import { WorkspaceBuildTransition } from "../api/types"
|
||||||
import * as TypesGen from "../api/typesGenerated"
|
import * as TypesGen from "../api/typesGenerated"
|
||||||
|
|
||||||
@@ -95,6 +96,11 @@ export const DisplayWorkspaceBuildStatusLanguage = {
|
|||||||
failed: "Failed",
|
failed: "Failed",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DisplayAgentVersionLanguage = {
|
||||||
|
unknown: "unknown",
|
||||||
|
outdated: "outdated",
|
||||||
|
}
|
||||||
|
|
||||||
export const getDisplayWorkspaceBuildStatus = (
|
export const getDisplayWorkspaceBuildStatus = (
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
build: TypesGen.WorkspaceBuild,
|
build: TypesGen.WorkspaceBuild,
|
||||||
@@ -211,6 +217,28 @@ export const getDisplayAgentStatus = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getDisplayVersionStatus = (
|
||||||
|
agentVersion: string,
|
||||||
|
serverVersion: string,
|
||||||
|
): { displayVersion: string; outdated: boolean } => {
|
||||||
|
if (!semver.valid(serverVersion) || !semver.valid(agentVersion)) {
|
||||||
|
return {
|
||||||
|
displayVersion: `${agentVersion}` || `(${DisplayAgentVersionLanguage.unknown})`,
|
||||||
|
outdated: false,
|
||||||
|
}
|
||||||
|
} else if (semver.lt(agentVersion, serverVersion)) {
|
||||||
|
return {
|
||||||
|
displayVersion: `${agentVersion} (${DisplayAgentVersionLanguage.outdated})`,
|
||||||
|
outdated: true,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
displayVersion: agentVersion,
|
||||||
|
outdated: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const isWorkspaceOn = (workspace: TypesGen.Workspace): boolean => {
|
export const isWorkspaceOn = (workspace: TypesGen.Workspace): boolean => {
|
||||||
const transition = workspace.latest_build.transition
|
const transition = workspace.latest_build.transition
|
||||||
const status = workspace.latest_build.job.status
|
const status = workspace.latest_build.job.status
|
||||||
|
|||||||
@@ -3330,6 +3330,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
|
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
|
||||||
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
|
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
|
||||||
|
|
||||||
|
"@types/semver@^7.3.12":
|
||||||
|
version "7.3.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.12.tgz#920447fdd78d76b19de0438b7f60df3c4a80bf1c"
|
||||||
|
integrity sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==
|
||||||
|
|
||||||
"@types/serve-index@^1.9.1":
|
"@types/serve-index@^1.9.1":
|
||||||
version "1.9.1"
|
version "1.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278"
|
resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278"
|
||||||
|
|||||||
Reference in New Issue
Block a user