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
8 changes: 8 additions & 0 deletions autocomplete/fish_autocomplete
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcomma
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from config' -f -l help -s h -d 'show help'
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command'
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and not __fish_seen_subcommand_from init create dockerfile config deploy status update restart rollback logs tail delete destroy versions list secrets update-secrets private-link start dev console daemon simulate help h' -a 'deploy' -d 'Deploy a new version of the agent'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from deploy' -f -l attributes -r -d '`JSON` literal or file path containing an object of string key-value pairs. Use "-" to read from stdin.'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from deploy' -f -l attribute -r -d '`KEY=VALUE` attribute pair, may be repeated. Merged with --attributes, taking precedence on conflicting keys.'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from deploy' -f -l secrets -r -d 'KEY=VALUE comma separated secrets. These will be injected as environment variables into the agent. These take precedence over secrets-file.'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from deploy' -l secrets-file -r -d '`FILE` containing secret KEY=VALUE pairs, one per line. These will be injected as environment variables into the agent.'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from deploy' -f -l secret-mount -r -d 'Local path to a secret file to be mounted on agent environment'
Expand All @@ -91,6 +93,7 @@ complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcomma
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from deploy; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command'
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and not __fish_seen_subcommand_from init create dockerfile config deploy status update restart rollback logs tail delete destroy versions list secrets update-secrets private-link start dev console daemon simulate help h' -a 'status' -d 'Get the status of an agent'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from status' -f -l id -r -d '`ID` of the agent. If unset, and the livekit.toml file is present, will use the id found there.'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from status' -f -l json -s j -d 'Output as JSON'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from status' -f -l help -s h -d 'show help'
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from status; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command'
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and not __fish_seen_subcommand_from init create dockerfile config deploy status update restart rollback logs tail delete destroy versions list secrets update-secrets private-link start dev console daemon simulate help h' -a 'update' -d 'Update an agent metadata and secrets. This will restart the agent.'
Expand Down Expand Up @@ -120,14 +123,19 @@ complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcomma
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from delete destroy; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command'
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and not __fish_seen_subcommand_from init create dockerfile config deploy status update restart rollback logs tail delete destroy versions list secrets update-secrets private-link start dev console daemon simulate help h' -a 'versions' -d 'List versions of an agent'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from versions' -f -l id -r -d '`ID` of the agent. If unset, and the livekit.toml file is present, will use the id found there.'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from versions' -f -l attribute -r -d '`KEY=VALUE` attribute pair, may be repeated. Merged with --attributes, taking precedence on conflicting keys.'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from versions' -f -l attributes -r -d '`JSON` literal or file path containing an object of string key-value pairs. Use "-" to read from stdin.'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from versions' -f -l json -s j -d 'Output as JSON'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from versions' -f -l help -s h -d 'show help'
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from versions; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command'
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and not __fish_seen_subcommand_from init create dockerfile config deploy status update restart rollback logs tail delete destroy versions list secrets update-secrets private-link start dev console daemon simulate help h' -a 'list' -d 'List all LiveKit Cloud Agents'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from list' -f -l id -r -d '`IDs` of agent(s)'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from list' -f -l json -s j -d 'Output as JSON'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from list' -f -l help -s h -d 'show help'
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command'
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and not __fish_seen_subcommand_from init create dockerfile config deploy status update restart rollback logs tail delete destroy versions list secrets update-secrets private-link start dev console daemon simulate help h' -a 'secrets' -d 'List secrets for an agent'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from secrets' -f -l id -r -d '`ID` of the agent. If unset, and the livekit.toml file is present, will use the id found there.'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from secrets' -f -l json -s j -d 'Output as JSON'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from secrets' -f -l help -s h -d 'show help'
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from secrets; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command'
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and not __fish_seen_subcommand_from init create dockerfile config deploy status update restart rollback logs tail delete destroy versions list secrets update-secrets private-link start dev console daemon simulate help h' -a 'update-secrets' -d 'Update secrets for an agent, will cause a re-start of the agent.'
Expand Down
124 changes: 113 additions & 11 deletions cmd/lk/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ package main

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"maps"
"os"
"path/filepath"
"regexp"
Expand Down Expand Up @@ -62,6 +64,18 @@ var (
Required: false,
}

attributesFlag = &cli.StringFlag{
Name: "attributes",
Usage: "`JSON` literal or file path containing an object of string key-value pairs. Use \"-\" to read from stdin.",
Required: false,
}

attributeFlag = &cli.StringSliceFlag{
Name: "attribute",
Usage: "`KEY=VALUE` attribute pair, may be repeated. Merged with --attributes, taking precedence on conflicting keys.",
Required: false,
}

secretsFileFlag = &cli.StringFlag{
Name: "secrets-file",
Usage: "`FILE` containing secret KEY=VALUE pairs, one per line. These will be injected as environment variables into the agent.",
Expand Down Expand Up @@ -221,6 +235,8 @@ var (
Before: createAgentClient,
Action: deployAgent,
Flags: []cli.Flag{
attributesFlag,
attributeFlag,
secretsFlag,
secretsFileFlag,
secretsMountFlag,
Expand All @@ -242,6 +258,7 @@ var (
Action: getAgentStatus,
Flags: []cli.Flag{
idFlag(false),
jsonFlag,
},
ArgsUsage: "[working-dir]",
},
Expand Down Expand Up @@ -317,6 +334,9 @@ var (
Action: listAgentVersions,
Flags: []cli.Flag{
idFlag(false),
attributeFlag,
attributesFlag,
jsonFlag,
},
ArgsUsage: "[working-dir]",
},
Expand All @@ -327,6 +347,7 @@ var (
Before: createAgentClient,
Flags: []cli.Flag{
idSliceFlag,
jsonFlag,
},
},
{
Expand All @@ -336,6 +357,7 @@ var (
Action: listAgentSecrets,
Flags: []cli.Flag{
idFlag(false),
jsonFlag,
},
ArgsUsage: "[working-dir]",
},
Expand Down Expand Up @@ -755,6 +777,11 @@ func deployAgent(ctx context.Context, cmd *cli.Command) error {
buildContext, cancel := context.WithTimeout(ctx, buildTimeout)
defer cancel()

attrs, err := resolveAttributes(cmd)
if err != nil {
return err
}

secrets, err := requireSecrets(ctx, cmd, false, true)
if err != nil {
return err
Expand Down Expand Up @@ -806,7 +833,7 @@ func deployAgent(ctx context.Context, cmd *cli.Command) error {
}

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.DeployAgent(buildContext, agentId, os.DirFS(workingDir), secrets, attrs, 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 @@ -876,6 +903,11 @@ func getAgentStatus(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("no agents found")
}

if cmd.Bool("json") {
util.PrintJSON(res)
return nil
}

var rows [][]string
for _, agent := range res.Agents {
for _, regionalAgent := range agent.AgentDeployments {
Expand Down Expand Up @@ -1085,6 +1117,39 @@ func deleteAgent(ctx context.Context, cmd *cli.Command) error {
return nil
}

// resolveAttributes merges attribute inputs from the --attributes JSON flag
// (literal, file path, or "-" for stdin) and the repeatable --attribute
// key=value flag. The key=value pairs take precedence over the JSON object on
// conflicting keys. Returns nil when neither flag is set.
func resolveAttributes(cmd *cli.Command) (map[string]string, error) {
attrs := map[string]string{}
if cmd.IsSet(attributesFlag.Name) {
if _, err := ReadJSONFileOrLiteral(cmd.String(attributesFlag.Name), &attrs); err != nil {
return nil, err
}
}
pairs, err := parseKeyValuePairs(cmd, attributeFlag.Name)
if err != nil {
return nil, err
}
maps.Copy(attrs, pairs)
if len(attrs) == 0 {
return nil, nil
}
return attrs, nil
}

// attributesMatch reports whether attrs contains every key-value pair in
// filter. Extra keys in attrs are allowed, so the match is inclusive.
func attributesMatch(attrs, filter map[string]string) bool {
for k, want := range filter {
if got, ok := attrs[k]; !ok || got != want {
return false
}
}
return true
}

func listAgentVersions(ctx context.Context, cmd *cli.Command) error {
agentID, err := getAgentID(ctx, cmd, workingDir, tomlFilename, false)
if err != nil {
Expand All @@ -1103,11 +1168,28 @@ func listAgentVersions(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("unable to list agent versions: %w", err)
}

// Filter to versions containing all requested attributes. Extra attributes
// on a version are allowed; the filter is inclusive, not exclusive.
attrFilter, err := resolveAttributes(cmd)
if err != nil {
return err
}
if len(attrFilter) > 0 {
versions.Versions = slices.DeleteFunc(versions.Versions, func(v *lkproto.AgentVersion) bool {
return !attributesMatch(v.Attributes, attrFilter)
})
}

// Sort versions by created date descending
slices.SortFunc(versions.Versions, func(a, b *lkproto.AgentVersion) int {
return b.CreatedAt.AsTime().Compare(a.CreatedAt.AsTime())
})

if cmd.Bool("json") {
util.PrintJSON(versions)
return nil
}

showDigest := false
for _, v := range versions.Versions {
if v.Attributes["image_digest"] != "" {
Expand All @@ -1116,17 +1198,22 @@ func listAgentVersions(ctx context.Context, cmd *cli.Command) error {
}
}

headers := []string{"Version", "Current", "Status", "Created At", "Deployed At"}
headers := []string{"Version", "Current", "Status", "Attributes", "Created At", "Deployed At"}
if showDigest {
headers = append(headers, "Digest")
}
table := util.CreateTable().Headers(headers...)

for _, version := range versions.Versions {
attrs, err := json.Marshal(version.Attributes)
if err != nil || len(version.Attributes) == 0 {
attrs = []byte("--")
}
row := []string{
version.Version,
fmt.Sprintf("%t", version.Current),
version.Status,
string(attrs),
version.CreatedAt.AsTime().Format(time.RFC3339),
version.DeployedAt.AsTime().Format(time.RFC3339),
}
Expand Down Expand Up @@ -1169,15 +1256,20 @@ func listAgents(ctx context.Context, cmd *cli.Command) error {
items = agents.Agents
}

slices.SortFunc(items, func(a, b *lkproto.AgentInfo) int {
return b.DeployedAt.AsTime().Compare(a.DeployedAt.AsTime())
})

if cmd.Bool("json") {
util.PrintJSON(&lkproto.ListAgentsResponse{Agents: items})
return nil
}

if len(items) == 0 {
out.Status("No agents found")
return nil
}

slices.SortFunc(items, func(a, b *lkproto.AgentInfo) int {
return b.DeployedAt.AsTime().Compare(a.DeployedAt.AsTime())
})

var rows [][]string
for _, agent := range items {
var regions []string
Expand Down Expand Up @@ -1219,15 +1311,25 @@ func listAgentSecrets(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("unable to list agent secrets: %w", err)
}

// TODO (steveyoon): show secret.Kind.String() once cloud-agents is released
table := util.CreateTable().
Headers("Name", "Created At", "Updated At")

// NOTE: Maybe these should be omitted on the server side?
visible := make([]*lkproto.AgentSecret, 0, len(secrets.Secrets))
for _, secret := range secrets.Secrets {
// NOTE: Maybe these should be omitted on the server side?
if slices.Contains(ignoredSecrets, secret.Name) {
continue
}
visible = append(visible, secret)
}

if cmd.Bool("json") {
util.PrintJSON(&lkproto.ListAgentSecretsResponse{Secrets: visible})
return nil
}

// TODO (steveyoon): show secret.Kind.String() once cloud-agents is released
table := util.CreateTable().
Headers("Name", "Created At", "Updated At")

for _, secret := range visible {
table.Row(secret.Name, secret.CreatedAt.AsTime().Format(time.RFC3339), secret.UpdatedAt.AsTime().Format(time.RFC3339))
}

Expand Down
Loading
Loading