Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 100 additions & 21 deletions cmd/lk/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Copy link
Copy Markdown

@shawnyang-coder shawnyang-coder Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why only create/deploy/update-secrets? What about other rpc like status/update?

Can an agent (identified by unique agent_id) be in both Staging and Prod ? or only one env at any time?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

status/update shows agent details including all environments. Yes, multiple environments (prod, staging) can exists within a single agent (agent_id).


skipSDKCheckFlag = &cli.BoolFlag{
Name: "skip-sdk-check",
Expand Down Expand Up @@ -182,6 +187,7 @@ var (
ignoreEmptySecretsFlag,
silentFlag,
regionFlag,
envFlag,
skipSDKCheckFlag,
agentPrebuiltImageFlag,
agentPrebuiltImageTarFlag,
Expand Down Expand Up @@ -228,6 +234,7 @@ var (
secretsMountFlag,
silentFlag,
regionFlag,
envFlag,
ignoreEmptySecretsFlag,
skipSDKCheckFlag,
agentPrebuiltImageFlag,
Expand Down Expand Up @@ -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]",
},
Expand Down Expand Up @@ -353,6 +362,7 @@ var (
secretsFileFlag,
secretsMountFlag,
ignoreEmptySecretsFlag,
envFlag,
idFlag(false),
&cli.BoolFlag{
Name: "overwrite",
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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())
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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),
Expand All @@ -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
},
Expand All @@ -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
}

Expand Down Expand Up @@ -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")
}
Expand All @@ -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"])
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
Loading