Skip to content

Commit a8eff21

Browse files
committed
Merge branch 'main' into reland-activity-and-autostop-changes
2 parents b7e3c99 + 5a41385 commit a8eff21

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+896
-302
lines changed

agent/agent.go

Lines changed: 40 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ type Options struct {
9090

9191
type Client interface {
9292
ConnectRPC(ctx context.Context) (drpc.Conn, error)
93-
PostMetadata(ctx context.Context, req agentsdk.PostMetadataRequest) error
9493
RewriteDERPMap(derpMap *tailcfg.DERPMap)
9594
}
9695

@@ -298,7 +297,6 @@ func (a *agent) init() {
298297
// may be happening, but regardless after the intermittent
299298
// failure, you'll want the agent to reconnect.
300299
func (a *agent) runLoop() {
301-
go a.reportMetadataUntilGracefulShutdown()
302300
go a.manageProcessPriorityUntilGracefulShutdown()
303301

304302
// need to keep retrying up to the hardCtx so that we can send graceful shutdown-related
@@ -405,9 +403,7 @@ func (t *trySingleflight) Do(key string, fn func()) {
405403
fn()
406404
}
407405

408-
func (a *agent) reportMetadataUntilGracefulShutdown() {
409-
// metadata reporting can cease as soon as we start gracefully shutting down.
410-
ctx := a.gracefulCtx
406+
func (a *agent) reportMetadata(ctx context.Context, conn drpc.Conn) error {
411407
tickerDone := make(chan struct{})
412408
collectDone := make(chan struct{})
413409
ctx, cancel := context.WithCancel(ctx)
@@ -567,51 +563,55 @@ func (a *agent) reportMetadataUntilGracefulShutdown() {
567563
var (
568564
updatedMetadata = make(map[string]*codersdk.WorkspaceAgentMetadataResult)
569565
reportTimeout = 30 * time.Second
570-
reportSemaphore = make(chan struct{}, 1)
566+
reportError = make(chan error, 1)
567+
reportInFlight = false
568+
aAPI = proto.NewDRPCAgentClient(conn)
571569
)
572-
reportSemaphore <- struct{}{}
573570

574571
for {
575572
select {
576573
case <-ctx.Done():
577-
return
574+
return ctx.Err()
578575
case mr := <-metadataResults:
579576
// This can overwrite unsent values, but that's fine because
580577
// we're only interested about up-to-date values.
581578
updatedMetadata[mr.key] = mr.result
582579
continue
580+
case err := <-reportError:
581+
a.logger.Debug(ctx, "batch update metadata complete", slog.Error(err))
582+
if err != nil {
583+
return xerrors.Errorf("failed to report metadata: %w", err)
584+
}
585+
reportInFlight = false
583586
case <-report:
584-
if len(updatedMetadata) > 0 {
585-
select {
586-
case <-reportSemaphore:
587-
default:
588-
// If there's already a report in flight, don't send
589-
// another one, wait for next tick instead.
590-
continue
591-
}
592-
593-
metadata := make([]agentsdk.Metadata, 0, len(updatedMetadata))
594-
for key, result := range updatedMetadata {
595-
metadata = append(metadata, agentsdk.Metadata{
596-
Key: key,
597-
WorkspaceAgentMetadataResult: *result,
598-
})
599-
delete(updatedMetadata, key)
600-
}
587+
if len(updatedMetadata) == 0 {
588+
continue
589+
}
590+
if reportInFlight {
591+
// If there's already a report in flight, don't send
592+
// another one, wait for next tick instead.
593+
a.logger.Debug(ctx, "skipped metadata report tick because report is in flight")
594+
continue
595+
}
596+
metadata := make([]*proto.Metadata, 0, len(updatedMetadata))
597+
for key, result := range updatedMetadata {
598+
pr := agentsdk.ProtoFromMetadataResult(*result)
599+
metadata = append(metadata, &proto.Metadata{
600+
Key: key,
601+
Result: pr,
602+
})
603+
delete(updatedMetadata, key)
604+
}
601605

602-
go func() {
603-
ctx, cancel := context.WithTimeout(ctx, reportTimeout)
604-
defer func() {
605-
cancel()
606-
reportSemaphore <- struct{}{}
607-
}()
606+
reportInFlight = true
607+
go func() {
608+
a.logger.Debug(ctx, "batch updating metadata")
609+
ctx, cancel := context.WithTimeout(ctx, reportTimeout)
610+
defer cancel()
608611

609-
err := a.client.PostMetadata(ctx, agentsdk.PostMetadataRequest{Metadata: metadata})
610-
if err != nil {
611-
a.logger.Error(ctx, "agent failed to report metadata", slog.Error(err))
612-
}
613-
}()
614-
}
612+
_, err := aAPI.BatchUpdateMetadata(ctx, &proto.BatchUpdateMetadataRequest{Metadata: metadata})
613+
reportError <- err
614+
}()
615615
}
616616
}
617617
}
@@ -783,6 +783,9 @@ func (a *agent) run() (retErr error) {
783783
// lifecycle reporting has to be via gracefulShutdownBehaviorRemain
784784
connMan.start("report lifecycle", gracefulShutdownBehaviorRemain, a.reportLifecycle)
785785

786+
// metadata reporting can cease as soon as we start gracefully shutting down
787+
connMan.start("report metadata", gracefulShutdownBehaviorStop, a.reportMetadata)
788+
786789
// channels to sync goroutines below
787790
// handle manifest
788791
// |

agent/agenttest/client.go

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ type Client struct {
8282
t testing.TB
8383
logger slog.Logger
8484
agentID uuid.UUID
85-
metadata map[string]agentsdk.Metadata
8685
coordinator tailnet.Coordinator
8786
server *drpcserver.Server
8887
fakeAgentAPI *FakeAgentAPI
@@ -131,22 +130,7 @@ func (c *Client) GetStartup() <-chan *agentproto.Startup {
131130
}
132131

133132
func (c *Client) GetMetadata() map[string]agentsdk.Metadata {
134-
c.mu.Lock()
135-
defer c.mu.Unlock()
136-
return maps.Clone(c.metadata)
137-
}
138-
139-
func (c *Client) PostMetadata(ctx context.Context, req agentsdk.PostMetadataRequest) error {
140-
c.mu.Lock()
141-
defer c.mu.Unlock()
142-
if c.metadata == nil {
143-
c.metadata = make(map[string]agentsdk.Metadata)
144-
}
145-
for _, md := range req.Metadata {
146-
c.metadata[md.Key] = md
147-
c.logger.Debug(ctx, "post metadata", slog.F("key", md.Key), slog.F("md", md))
148-
}
149-
return nil
133+
return c.fakeAgentAPI.GetMetadata()
150134
}
151135

152136
func (c *Client) GetStartupLogs() []agentsdk.Log {
@@ -186,6 +170,7 @@ type FakeAgentAPI struct {
186170
appHealthCh chan *agentproto.BatchUpdateAppHealthRequest
187171
logsCh chan<- *agentproto.BatchCreateLogsRequest
188172
lifecycleStates []codersdk.WorkspaceAgentLifecycle
173+
metadata map[string]agentsdk.Metadata
189174

190175
getServiceBannerFunc func() (codersdk.ServiceBannerConfig, error)
191176
}
@@ -254,9 +239,24 @@ func (f *FakeAgentAPI) UpdateStartup(_ context.Context, req *agentproto.UpdateSt
254239
return req.GetStartup(), nil
255240
}
256241

257-
func (*FakeAgentAPI) BatchUpdateMetadata(context.Context, *agentproto.BatchUpdateMetadataRequest) (*agentproto.BatchUpdateMetadataResponse, error) {
258-
// TODO implement me
259-
panic("implement me")
242+
func (f *FakeAgentAPI) GetMetadata() map[string]agentsdk.Metadata {
243+
f.Lock()
244+
defer f.Unlock()
245+
return maps.Clone(f.metadata)
246+
}
247+
248+
func (f *FakeAgentAPI) BatchUpdateMetadata(ctx context.Context, req *agentproto.BatchUpdateMetadataRequest) (*agentproto.BatchUpdateMetadataResponse, error) {
249+
f.Lock()
250+
defer f.Unlock()
251+
if f.metadata == nil {
252+
f.metadata = make(map[string]agentsdk.Metadata)
253+
}
254+
for _, md := range req.Metadata {
255+
smd := agentsdk.MetadataFromProto(md)
256+
f.metadata[md.Key] = smd
257+
f.logger.Debug(ctx, "post metadata", slog.F("key", md.Key), slog.F("md", md))
258+
}
259+
return &agentproto.BatchUpdateMetadataResponse{}, nil
260260
}
261261

262262
func (f *FakeAgentAPI) SetLogsChannel(ch chan<- *agentproto.BatchCreateLogsRequest) {

cli/config/file.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ func (r Root) PostgresPort() File {
7070
// File provides convenience methods for interacting with *os.File.
7171
type File string
7272

73+
func (f File) Exists() bool {
74+
if f == "" {
75+
return false
76+
}
77+
_, err := os.Stat(string(f))
78+
return err == nil
79+
}
80+
7381
// Delete deletes the file.
7482
func (f File) Delete() error {
7583
if f == "" {

cli/create.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func (r *RootCmd) create() *clibase.Cmd {
4343
),
4444
Middleware: clibase.Chain(r.InitClient(client)),
4545
Handler: func(inv *clibase.Invocation) error {
46-
organization, err := CurrentOrganization(inv, client)
46+
organization, err := CurrentOrganization(r, inv, client)
4747
if err != nil {
4848
return err
4949
}

cli/organization.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/coder/coder/v2/cli/clibase"
8+
"github.com/coder/coder/v2/cli/cliui"
9+
"github.com/coder/coder/v2/codersdk"
10+
)
11+
12+
func (r *RootCmd) organizations() *clibase.Cmd {
13+
cmd := &clibase.Cmd{
14+
Annotations: workspaceCommand,
15+
Use: "organizations [subcommand]",
16+
Short: "Organization related commands",
17+
Aliases: []string{"organization", "org", "orgs"},
18+
Hidden: true, // Hidden until these commands are complete.
19+
Handler: func(inv *clibase.Invocation) error {
20+
return inv.Command.HelpHandler(inv)
21+
},
22+
Children: []*clibase.Cmd{
23+
r.currentOrganization(),
24+
},
25+
}
26+
27+
cmd.Options = clibase.OptionSet{}
28+
return cmd
29+
}
30+
31+
func (r *RootCmd) currentOrganization() *clibase.Cmd {
32+
var (
33+
stringFormat func(orgs []codersdk.Organization) (string, error)
34+
client = new(codersdk.Client)
35+
formatter = cliui.NewOutputFormatter(
36+
cliui.ChangeFormatterData(cliui.TextFormat(), func(data any) (any, error) {
37+
typed, ok := data.([]codersdk.Organization)
38+
if !ok {
39+
// This should never happen
40+
return "", fmt.Errorf("expected []Organization, got %T", data)
41+
}
42+
return stringFormat(typed)
43+
}),
44+
cliui.TableFormat([]codersdk.Organization{}, []string{"id", "name", "default"}),
45+
cliui.JSONFormat(),
46+
)
47+
onlyID = false
48+
)
49+
cmd := &clibase.Cmd{
50+
Use: "show [current|me|uuid]",
51+
Short: "Show the organization, if no argument is given, the organization currently in use will be shown.",
52+
Middleware: clibase.Chain(
53+
r.InitClient(client),
54+
clibase.RequireRangeArgs(0, 1),
55+
),
56+
Options: clibase.OptionSet{
57+
{
58+
Name: "only-id",
59+
Description: "Only print the organization ID.",
60+
Required: false,
61+
Flag: "only-id",
62+
Value: clibase.BoolOf(&onlyID),
63+
},
64+
},
65+
Handler: func(inv *clibase.Invocation) error {
66+
orgArg := "current"
67+
if len(inv.Args) >= 1 {
68+
orgArg = inv.Args[0]
69+
}
70+
71+
var orgs []codersdk.Organization
72+
var err error
73+
switch strings.ToLower(orgArg) {
74+
case "current":
75+
stringFormat = func(orgs []codersdk.Organization) (string, error) {
76+
if len(orgs) != 1 {
77+
return "", fmt.Errorf("expected 1 organization, got %d", len(orgs))
78+
}
79+
return fmt.Sprintf("Current CLI Organization: %s (%s)\n", orgs[0].Name, orgs[0].ID.String()), nil
80+
}
81+
org, err := CurrentOrganization(r, inv, client)
82+
if err != nil {
83+
return err
84+
}
85+
orgs = []codersdk.Organization{org}
86+
case "me":
87+
stringFormat = func(orgs []codersdk.Organization) (string, error) {
88+
var str strings.Builder
89+
_, _ = fmt.Fprint(&str, "Organizations you are a member of:\n")
90+
for _, org := range orgs {
91+
_, _ = fmt.Fprintf(&str, "\t%s (%s)\n", org.Name, org.ID.String())
92+
}
93+
return str.String(), nil
94+
}
95+
orgs, err = client.OrganizationsByUser(inv.Context(), codersdk.Me)
96+
if err != nil {
97+
return err
98+
}
99+
default:
100+
stringFormat = func(orgs []codersdk.Organization) (string, error) {
101+
if len(orgs) != 1 {
102+
return "", fmt.Errorf("expected 1 organization, got %d", len(orgs))
103+
}
104+
return fmt.Sprintf("Organization: %s (%s)\n", orgs[0].Name, orgs[0].ID.String()), nil
105+
}
106+
// This works for a uuid or a name
107+
org, err := client.OrganizationByName(inv.Context(), orgArg)
108+
if err != nil {
109+
return err
110+
}
111+
orgs = []codersdk.Organization{org}
112+
}
113+
114+
if onlyID {
115+
for _, org := range orgs {
116+
_, _ = fmt.Fprintf(inv.Stdout, "%s\n", org.ID)
117+
}
118+
} else {
119+
out, err := formatter.Format(inv.Context(), orgs)
120+
if err != nil {
121+
return err
122+
}
123+
_, _ = fmt.Fprint(inv.Stdout, out)
124+
}
125+
return nil
126+
},
127+
}
128+
formatter.AttachOptions(&cmd.Options)
129+
130+
return cmd
131+
}

cli/organization_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package cli_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"github.com/coder/coder/v2/cli/clitest"
9+
"github.com/coder/coder/v2/coderd/coderdtest"
10+
"github.com/coder/coder/v2/coderd/rbac"
11+
"github.com/coder/coder/v2/codersdk"
12+
"github.com/coder/coder/v2/pty/ptytest"
13+
"github.com/coder/coder/v2/testutil"
14+
)
15+
16+
func TestCurrentOrganization(t *testing.T) {
17+
t.Parallel()
18+
19+
t.Run("OnlyID", func(t *testing.T) {
20+
t.Parallel()
21+
ownerClient := coderdtest.New(t, nil)
22+
first := coderdtest.CreateFirstUser(t, ownerClient)
23+
// Owner is required to make orgs
24+
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, first.OrganizationID, rbac.RoleOwner())
25+
26+
ctx := testutil.Context(t, testutil.WaitMedium)
27+
orgs := []string{"foo", "bar"}
28+
for _, orgName := range orgs {
29+
_, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
30+
Name: orgName,
31+
})
32+
require.NoError(t, err)
33+
}
34+
35+
inv, root := clitest.New(t, "organizations", "show", "--only-id")
36+
clitest.SetupConfig(t, client, root)
37+
pty := ptytest.New(t).Attach(inv)
38+
errC := make(chan error)
39+
go func() {
40+
errC <- inv.Run()
41+
}()
42+
require.NoError(t, <-errC)
43+
pty.ExpectMatch(first.OrganizationID.String())
44+
})
45+
}

0 commit comments

Comments
 (0)