diff --git a/cmd/lk/agent.go b/cmd/lk/agent.go index fe373a56..5e5a8cbf 100644 --- a/cmd/lk/agent.go +++ b/cmd/lk/agent.go @@ -110,6 +110,11 @@ var ( Name: "image-tar", Usage: "Pre-built image from an OCI tar file (e.g. ./image.tar). No Docker daemon required.", } + envFlag = &cli.StringSliceFlag{ + Name: "env", + Usage: "Deployment environment(s). For create/deploy, specifies the target environment (defaults to 'production'). For update-secrets, assigns environment(s) to the secret. Can be specified multiple times (e.g. --env staging --env production).", + Required: false, + } skipSDKCheckFlag = &cli.BoolFlag{ Name: "skip-sdk-check", @@ -182,6 +187,7 @@ var ( ignoreEmptySecretsFlag, silentFlag, regionFlag, + envFlag, skipSDKCheckFlag, agentPrebuiltImageFlag, agentPrebuiltImageTarFlag, @@ -228,6 +234,7 @@ var ( secretsMountFlag, silentFlag, regionFlag, + envFlag, ignoreEmptySecretsFlag, skipSDKCheckFlag, agentPrebuiltImageFlag, @@ -299,18 +306,20 @@ var ( Flags: []cli.Flag{ idFlag(false), logTypeFlag, + envFlag, }, ArgsUsage: "[working-dir]", }, { Name: "delete", - Usage: "Delete an agent", + Usage: "Delete an agent or a specific environment", Before: createAgentClient, Action: deleteAgent, Aliases: []string{"destroy"}, Flags: []cli.Flag{ silentFlag, idFlag(false), + envFlag, }, ArgsUsage: "[working-dir]", }, @@ -353,6 +362,7 @@ var ( secretsFileFlag, secretsMountFlag, ignoreEmptySecretsFlag, + envFlag, idFlag(false), &cli.BoolFlag{ Name: "overwrite", @@ -383,6 +393,13 @@ func createAgentClient(ctx context.Context, cmd *cli.Command) (context.Context, return createAgentClientWithOpts(ctx, cmd) } +func formatTime(t time.Time) string { + if t.IsZero() { + return "---" + } + return t.Format(time.RFC3339) +} + func createAgentClientWithOpts(ctx context.Context, cmd *cli.Command, opts ...loadOption) (context.Context, error) { var err error @@ -665,7 +682,7 @@ func createAgent(ctx context.Context, cmd *cli.Command) error { return err } else if viewLogs { fmt.Println("Tailing runtime logs...safe to exit at any time") - return agentsClient.StreamLogs(ctx, "deploy", lkConfig.Agent.ID, os.Stdout, resp.ServerRegions[0]) + return agentsClient.StreamLogs(ctx, "deploy", lkConfig.Agent.ID, "", os.Stdout, resp.ServerRegions[0]) } } return nil @@ -790,8 +807,14 @@ func deployAgent(ctx context.Context, cmd *cli.Command) error { } } + environment := "production" + if cmd.IsSet("env") { + environment = cmd.StringSlice("env")[0] + } + fmt.Printf("Using environment [%s]\n", util.Accented(environment)) + excludeFiles := []string{fmt.Sprintf("**/%s", config.LiveKitTOMLFile)} - if err := agentsClient.DeployAgent(buildContext, agentId, os.DirFS(workingDir), secrets, excludeFiles, os.Stderr); err != nil { + if err := agentsClient.DeployAgentV2(buildContext, agentId, os.DirFS(workingDir), secrets, environment, excludeFiles, os.Stderr); err != nil { if twerr, ok := err.(twirp.Error); ok { return fmt.Errorf("unable to deploy agent: %s", twerr.Msg()) } @@ -879,21 +902,27 @@ func getAgentStatus(ctx context.Context, cmd *cli.Command) error { logger.Errorw("error parsing mem req", err) } + version := regionalAgent.Version + if version == "" { + version = agent.Version + } rows = append(rows, []string{ agent.AgentId, - agent.Version, + regionalAgent.AgentName, + version, regionalAgent.Region, + regionalAgent.Environment, regionalAgent.Status, fmt.Sprintf("%s / %s", curCPU, regionalAgent.CpuLimit), fmt.Sprintf("%s / %s", curMem, memLimit), fmt.Sprintf("%d / %d / %d", regionalAgent.Replicas, regionalAgent.MinReplicas, regionalAgent.MaxReplicas), - agent.DeployedAt.AsTime().Format(time.RFC3339), + formatTime(agent.DeployedAt.AsTime()), }) } } t := util.CreateTable(). - Headers("ID", "Version", "Region", "Status", "CPU", "Mem", "Replicas", "Deployed At"). + Headers("ID", "Name", "Version", "Region", "Environment", "Status", "CPU", "Mem", "Replicas", "Deployed At"). Rows(rows...) fmt.Println(t) @@ -1015,7 +1044,11 @@ func getLogs(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("no agent deployments found") } - return agentsClient.StreamLogs(ctx, cmd.String("log-type"), agentID, os.Stdout, response.Agents[0].AgentDeployments[0].ServerRegion) + var agentEnvironment string + if envs := cmd.StringSlice("env"); len(envs) > 0 { + agentEnvironment = envs[0] + } + return agentsClient.StreamLogs(ctx, cmd.String("log-type"), agentID, agentEnvironment, os.Stdout, response.Agents[0].AgentDeployments[0].ServerRegion) } func deleteAgent(ctx context.Context, cmd *cli.Command) error { @@ -1025,12 +1058,26 @@ func deleteAgent(ctx context.Context, cmd *cli.Command) error { return err } + var environment string + if envs := cmd.StringSlice("env"); len(envs) > 0 { + environment = envs[0] + } + + confirmMsg := fmt.Sprintf("Are you sure you want to delete agent [%s]?", agentID) + deletingMsg := "Deleting agent [" + util.Accented(agentID) + "]" + deletedMsg := fmt.Sprintf("Deleted agent [%s]", util.Accented(agentID)) + if environment != "" { + confirmMsg = fmt.Sprintf("Are you sure you want to delete environment [%s] from agent [%s]?", environment, agentID) + deletingMsg = "Deleting environment [" + util.Accented(environment) + "] from agent [" + util.Accented(agentID) + "]" + deletedMsg = fmt.Sprintf("Deleted environment [%s] from agent [%s]", util.Accented(environment), util.Accented(agentID)) + } + if !silent && !SkipPrompts(cmd) { var confirmDelete bool if err := huh.NewForm( huh.NewGroup( huh.NewConfirm(). - Title(fmt.Sprintf("Are you sure you want to delete agent [%s]?", agentID)). + Title(confirmMsg). Value(&confirmDelete). Inline(false). WithTheme(util.Theme), @@ -1046,12 +1093,13 @@ func deleteAgent(ctx context.Context, cmd *cli.Command) error { var res *lkproto.DeleteAgentResponse err = util.Await( - "Deleting agent ["+util.Accented(agentID)+"]", + deletingMsg, ctx, func(ctx context.Context) error { var clientErr error res, clientErr = agentsClient.DeleteAgent(ctx, &lkproto.DeleteAgentRequest{ - AgentId: agentID, + AgentId: agentID, + Environment: environment, }) return clientErr }, @@ -1068,7 +1116,7 @@ func deleteAgent(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("failed to delete agent %s", res.Message) } - fmt.Printf("Deleted agent [%s]\n", util.Accented(agentID)) + fmt.Printf("%s\n", deletedMsg) return nil } @@ -1103,7 +1151,14 @@ func listAgentVersions(ctx context.Context, cmd *cli.Command) error { } } - headers := []string{"Version", "Current", "Status", "Created At", "Deployed At"} + flag := func(b bool) string { + if b { + return "✓" + } + return "---" + } + + headers := []string{"Version", "Prod", "Draining", "Active", "Status", "Created At", "Deployed At"} if showDigest { headers = append(headers, "Digest") } @@ -1112,10 +1167,12 @@ func listAgentVersions(ctx context.Context, cmd *cli.Command) error { for _, version := range versions.Versions { row := []string{ version.Version, - fmt.Sprintf("%t", version.Current), + flag(version.Current), + flag(version.Draining), + flag(version.Active), version.Status, - version.CreatedAt.AsTime().Format(time.RFC3339), - version.DeployedAt.AsTime().Format(time.RFC3339), + formatTime(version.CreatedAt.AsTime()), + formatTime(version.DeployedAt.AsTime()), } if showDigest { row = append(row, version.Attributes["image_digest"]) @@ -1167,21 +1224,32 @@ func listAgents(ctx context.Context, cmd *cli.Command) error { var rows [][]string for _, agent := range items { - var regions []string + regionSet := map[string]struct{}{} + envSet := map[string]struct{}{} for _, regionalAgent := range agent.AgentDeployments { - regions = append(regions, regionalAgent.Region) + regionSet[regionalAgent.Region] = struct{}{} + envSet[regionalAgent.Environment] = struct{}{} + } + regions := make([]string, 0, len(regionSet)) + for region := range regionSet { + regions = append(regions, region) + } + environments := make([]string, 0, len(envSet)) + for environment := range envSet { + environments = append(environments, environment) } rows = append(rows, []string{ agent.AgentId, agent.AgentName, strings.Join(regions, ","), + strings.Join(environments, ","), agent.Version, - agent.DeployedAt.AsTime().Format(time.RFC3339), + formatTime(agent.DeployedAt.AsTime()), }) } t := util.CreateTable(). - Headers("ID", "Dispatch Name", "Regions", "Version", "Deployed At"). + Headers("ID", "Dispatch Name", "Regions", "Environment", "Version", "Deployed At"). Rows(rows...) fmt.Println(t) @@ -1208,14 +1276,18 @@ func listAgentSecrets(ctx context.Context, cmd *cli.Command) error { // TODO (steveyoon): show secret.Kind.String() once cloud-agents is released table := util.CreateTable(). - Headers("Name", "Created At", "Updated At") + Headers("Name", "Environments", "Created At", "Updated At") for _, secret := range secrets.Secrets { // NOTE: Maybe these should be omitted on the server side? if slices.Contains(ignoredSecrets, secret.Name) { continue } - table.Row(secret.Name, secret.CreatedAt.AsTime().Format(time.RFC3339), secret.UpdatedAt.AsTime().Format(time.RFC3339)) + envs := strings.Join(secret.Environments, ", ") + if envs == "" { + envs = "(all)" + } + table.Row(secret.Name, envs, secret.CreatedAt.AsTime().Format(time.RFC3339), secret.UpdatedAt.AsTime().Format(time.RFC3339)) } fmt.Println(table) @@ -1233,6 +1305,13 @@ func updateAgentSecrets(ctx context.Context, cmd *cli.Command) error { return err } + if cmd.IsSet("env") { + envs := cmd.StringSlice("env") + for _, s := range secrets { + s.Environments = envs + } + } + var confirmOverwrite bool if cmd.Bool("overwrite") { confirmOverwrite = SkipPrompts(cmd) diff --git a/go.mod b/go.mod index 7c735317..2183aae3 100644 --- a/go.mod +++ b/go.mod @@ -15,8 +15,8 @@ require ( github.com/google/go-containerregistry v0.20.6 github.com/google/go-querystring v1.2.0 github.com/joho/godotenv v1.5.1 - github.com/livekit/protocol v1.45.4-0.20260417173102-5d4f88f73b7c - github.com/livekit/server-sdk-go/v2 v2.16.2 + github.com/livekit/protocol v1.45.5-0.20260423163244-347de5a2ef78 + github.com/livekit/server-sdk-go/v2 v2.16.3-0.20260423164341-e80807faf25c github.com/mattn/go-isatty v0.0.21 github.com/moby/patternmatcher v0.6.1 github.com/modelcontextprotocol/go-sdk v1.4.0 diff --git a/go.sum b/go.sum index 30b2933b..2579adbe 100644 --- a/go.sum +++ b/go.sum @@ -366,12 +366,12 @@ github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 h1:9x+U2HGLrSw5AT github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ= github.com/livekit/mediatransportutil v0.0.0-20260309115634-0e2e24b36ee8 h1:coWig9fKxdb/nwOaIoGUUAogso12GblAJh/9SA9hcxk= github.com/livekit/mediatransportutil v0.0.0-20260309115634-0e2e24b36ee8/go.mod h1:RCd46PT+6sEztld6XpkCrG1xskb0u3SqxIjy4G897Ss= -github.com/livekit/protocol v1.45.4-0.20260417173102-5d4f88f73b7c h1:TusqCkqnqw/9B6BVJhFjFI8D/1Zy6z4oBsk1ehbpZIc= -github.com/livekit/protocol v1.45.4-0.20260417173102-5d4f88f73b7c/go.mod h1:e6QdWDkfot+M2nRh0eitJUS0ZLuwvKCsfiz2pWWSG3s= +github.com/livekit/protocol v1.45.5-0.20260423163244-347de5a2ef78 h1:kT824Ziy89MSD2/UcvgGZWZ5iiEGepDAvu9phjYHiT0= +github.com/livekit/protocol v1.45.5-0.20260423163244-347de5a2ef78/go.mod h1:e6QdWDkfot+M2nRh0eitJUS0ZLuwvKCsfiz2pWWSG3s= github.com/livekit/psrpc v0.7.1 h1:ms37az0QTD3UXIWuUC5D/SkmKOlRMVRsI261eBWu/Vw= github.com/livekit/psrpc v0.7.1/go.mod h1:bZ4iHFQptTkbPnB0LasvRNu/OBYXEu1NA6O5BMFo9kk= -github.com/livekit/server-sdk-go/v2 v2.16.2 h1:eQe24cka3X+5zUivezyL72nwtAJTWFXgibeiyJ/Jm+Y= -github.com/livekit/server-sdk-go/v2 v2.16.2/go.mod h1:/HOUG9AXJeCbMCdtw0dr37AB+3xXUlj/OLeXS/0p7rA= +github.com/livekit/server-sdk-go/v2 v2.16.3-0.20260423164341-e80807faf25c h1:Ww0JxdBEMlFgiidHpXSQ94BYulmJJFZTDiqTcZa1Ztw= +github.com/livekit/server-sdk-go/v2 v2.16.3-0.20260423164341-e80807faf25c/go.mod h1:UrY6xgR0jLJqdqnh8XahX7IHzCRhZQCpvUvU8RvkEmY= github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magefile/mage v1.17.0 h1:dS4tkq997Ism03akafC8509iqDjeE7TNTexI25Y7sXM=