Skip to content
Merged
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
2 changes: 2 additions & 0 deletions autocomplete/fish_autocomplete
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,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 create' -f -l deployment -s d -r -d 'Agent deployment'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from create' -f -l image -r -d 'Pre-built image from the local Docker daemon (e.g. myimage:latest). Requires Docker.'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from create' -f -l image-tar -r -d 'Pre-built image from an OCI tar file (e.g. ./image.tar). No Docker daemon required.'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from create' -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 create' -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 create' -f -l help -s h -d 'show help'
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from create; 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 promote status update restart rollback logs tail delete destroy versions list secrets update-secrets private-link start dev console daemon simulate help h' -a 'dockerfile' -d 'Generate Dockerfile and .dockerignore for your project'
Expand Down
124 changes: 110 additions & 14 deletions cmd/lk/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ var (
skipSDKCheckFlag,
agentPrebuiltImageFlag,
agentPrebuiltImageTarFlag,
attributeFlag,
attributesFlag,
},
// NOTE: since secrets may contain commas, or indeed any special character we might want to treat as a flag separator,
// we disable it entirely here and require multiple --secrets flags to be used.
Expand Down Expand Up @@ -569,10 +571,8 @@ func createAgent(ctx context.Context, cmd *cli.Command) error {
if err := huh.NewForm(huh.NewGroup(util.Confirm().
Title(fmt.Sprintf("Use project [%s] (%s) to create agent deployment?", project.Name, project.URL)).
Value(&useProject).
Options(
huh.NewOption("Yes", true),
huh.NewOption("No, select another...", false),
).
Affirmative("Yes").
Negative("No, select another...").
WithTheme(util.Theme))).
Run(); err != nil {
return err
Expand Down Expand Up @@ -644,7 +644,11 @@ func createAgent(ctx context.Context, cmd *cli.Command) error {
return err
}
out.Statusf("Created agent with ID [%s]", util.Accented(agentID))
return deployPrebuiltImage(buildContext, agentID, imageRef, imageTar)
attrs, err := resolveDeployAttributes(buildContext, cmd)
if err != nil {
return err
}
return deployPrebuiltImage(buildContext, agentID, imageRef, imageTar, attrs)
}

projectType, err := agentfs.DetectProjectType(os.DirFS(workingDir))
Expand Down Expand Up @@ -799,7 +803,7 @@ func deployAgent(ctx context.Context, cmd *cli.Command) error {
buildContext, cancel := context.WithTimeout(ctx, buildTimeout)
defer cancel()

attrs, err := resolveAttributes(cmd)
attrs, err := resolveDeployAttributes(buildContext, cmd)
if err != nil {
return err
}
Expand Down Expand Up @@ -828,7 +832,7 @@ func deployAgent(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("failed to update agent secrets: %s", resp.Message)
}
}
if err := deployPrebuiltImage(buildContext, agentId, imageRef, imageTar); err != nil {
if err := deployPrebuiltImage(buildContext, agentId, imageRef, imageTar, attrs); err != nil {
return fmt.Errorf("unable to deploy prebuilt image: %w", err)
}
out.Status("Deployed agent")
Expand Down Expand Up @@ -893,7 +897,7 @@ func promoteAgent(ctx context.Context, cmd *cli.Command) error {

// deployPrebuiltImage pushes a locally-built image through the cloud-agents OCI proxy.
// Exactly one of imageRef (Docker daemon via the Docker API) or imageTar must be non-empty.
func deployPrebuiltImage(ctx context.Context, agentID, imageRef, imageTar string) error {
func deployPrebuiltImage(ctx context.Context, agentID, imageRef, imageTar string, attrs map[string]string) error {
target, err := agentsClient.GetPushTarget(ctx, agentID)
if err != nil {
return fmt.Errorf("failed to get push target: %w", err)
Expand All @@ -920,7 +924,7 @@ func deployPrebuiltImage(ctx context.Context, agentID, imageRef, imageTar string
proxyRef := fmt.Sprintf("%s/%s:%s", target.ProxyHost, target.Name, target.Tag)
out.Statusf("Pushing image [%s]", util.Accented(proxyRef))

rt := agentsClient.NewRegistryTransport()
rt := agentsClient.NewRegistryTransport(attrs)
if err := crane.Push(img, proxyRef,
crane.WithTransport(rt),
crane.WithAuth(authn.Anonymous),
Expand Down Expand Up @@ -960,9 +964,9 @@ func getAgentStatus(ctx context.Context, cmd *cli.Command) error {
for _, regionalAgent := range agent.AgentDeployments {
curCPU := "-"
curMem := "-"
agentName := "---"
deployment := "---"
lastScrapedAt := "---"
agentName := "--"
deployment := "--"
lastScrapedAt := "--"
if regionalAgent.LastScrapedAt != nil {
lastScrapedAt = formatTime(regionalAgent.LastScrapedAt.AsTime())
}
Expand Down Expand Up @@ -1230,6 +1234,98 @@ func resolveAttributes(cmd *cli.Command) (map[string]string, error) {
return attrs, nil
}

// Attribute keys used to tag a deployed agent version with the git metadata it
// was built from. Any of these supplied explicitly by the user takes precedence.
const (
gitCommitAttribute = "git_commit"
gitBranchAttribute = "git_branch"
gitTreeAttribute = "git_tree"
)

// resolveDeployAttributes resolves the user-supplied attributes and, when the
// working directory is inside a git repository, automatically tags the
// deployment with git metadata: git_commit (HEAD SHA) and git_branch (current
// branch). Any attribute the user supplied explicitly always wins. When the
// working tree has uncommitted changes the tagged commit may not reflect the
// deployed code, so the user is asked to confirm (skipped, with a warning,
// under --yes or a non-interactive terminal); if they proceed, a git_tree
// attribute is added with a content hash that uniquely identifies the dirty
// working state.
func resolveDeployAttributes(ctx context.Context, cmd *cli.Command) (map[string]string, error) {
attrs, err := resolveAttributes(cmd)
if err != nil {
return nil, err
}

if !util.IsGitRepository(ctx, workingDir) {
return attrs, nil
}

// setDefault assigns key=value only when value is non-empty and the user
// hasn't already supplied the key. Reports whether it assigned.
setDefault := func(key, value string) bool {
if value == "" {
return false
}
if _, ok := attrs[key]; ok {
return false
}
if attrs == nil {
attrs = map[string]string{}
}
attrs[key] = value
return true
}

commit, _ := util.GitHeadCommit(ctx, workingDir)
branch, _ := util.GitCurrentBranch(ctx, workingDir)

// Warn/confirm on a dirty tree before auto-tagging the commit, unless the
// user pinned the commit attribute themselves.
var treeHash string
_, commitPinned := attrs[gitCommitAttribute]
if commit != "" && !commitPinned {
if dirty, err := util.HasUncommittedChanges(ctx, workingDir); err == nil && dirty {
out.Warnf("Working tree has uncommitted changes; commit [%s] may not reflect the deployed code.", util.Accented(commit))
if !SkipPrompts(cmd) {
proceed := false
if err := huh.NewForm(
huh.NewGroup(
util.Confirm().
Title("Continue deploying anyway?").
Value(&proceed).
WithTheme(util.Theme),
),
).Run(); err != nil {
return nil, err
}
if !proceed {
return nil, errors.New("deployment cancelled")
}
}
// Proceeding with a dirty tree: capture a content hash so the exact
// working state remains identifiable despite the mismatched commit.
treeHash, _ = util.GitWorkingTreeHash(ctx, workingDir)
}
}

taggedCommit := setDefault(gitCommitAttribute, commit)
setDefault(gitBranchAttribute, branch)
setDefault(gitTreeAttribute, treeHash)

if taggedCommit {
if branch != "" {
out.Statusf("Tagging deployment with commit [%s] on branch [%s]", util.Accented(commit), util.Accented(branch))
} else {
out.Statusf("Tagging deployment with commit [%s]", util.Accented(commit))
}
if treeHash != "" {
out.Statusf("Working tree is dirty; tagged with tree hash [%s]", util.Accented(treeHash))
}
}
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 {
Expand Down Expand Up @@ -1293,7 +1389,7 @@ func listAgentVersions(ctx context.Context, cmd *cli.Command) error {
if b {
return "✓"
}
return "---"
return "--"
}
headers := []string{"Version", "Production", "Draining", "Active", "Status", "Attributes", "Created At", "Deployed At"}
if showDigest {
Expand Down Expand Up @@ -1565,7 +1661,7 @@ func selectAgent(ctx context.Context, cmd *cli.Command, excludeEmptyVersion bool

var agentNames []huh.Option[string]
for _, agent := range agents.Agents {
if excludeEmptyVersion && agent.Version == "---" {
if excludeEmptyVersion && agent.Version == "--" {
continue
}
var deployedStr string
Expand Down
6 changes: 2 additions & 4 deletions cmd/lk/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,8 @@ func requireProjectWithOpts(ctx context.Context, cmd *cli.Command, opts ...loadO
Title(fmt.Sprintf("Use project [%s]?", rp.project.Name)).
Description(rp.project.URL).
Value(&useDefault).
Options(
huh.NewOption("Yes", true),
huh.NewOption("No, select another...", false),
).
Affirmative("Yes").
Negative("No, select another...").
WithTheme(util.Theme))).
Run(); err != nil {
return ctx, fmt.Errorf("failed to confirm project: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion cmd/lk/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ func (s *templateStringValue) String() string {

func formatTime(t time.Time) string {
if t.IsZero() {
return "---"
return "--"
}
return t.Format(time.RFC3339)
}
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ require (
github.com/joho/godotenv v1.5.1
github.com/klauspost/compress v1.18.6
github.com/livekit/protocol v1.49.1-0.20260712215709-8847d7456816
github.com/livekit/server-sdk-go/v2 v2.17.0
github.com/livekit/server-sdk-go/v2 v2.18.1
github.com/mattn/go-isatty v0.0.22
github.com/moby/patternmatcher v0.6.1
github.com/modelcontextprotocol/go-sdk v1.6.1
github.com/pelletier/go-toml v1.9.5
github.com/pion/rtcp v1.2.16
github.com/pion/rtp v1.10.2
github.com/pion/webrtc/v4 v4.2.14
github.com/pion/webrtc/v4 v4.2.15
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/stretchr/testify v1.11.1
github.com/twitchtv/twirp v8.1.3+incompatible
Expand Down Expand Up @@ -193,11 +193,11 @@ require (
github.com/pion/mdns/v2 v2.1.0 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.10.0 // indirect
github.com/pion/sdp/v3 v3.0.18 // indirect
github.com/pion/sdp/v3 v3.0.19 // indirect
github.com/pion/srtp/v3 v3.0.11 // indirect
github.com/pion/stun/v3 v3.1.4 // indirect
github.com/pion/stun/v3 v3.1.5 // indirect
github.com/pion/transport/v4 v4.0.2 // indirect
github.com/pion/turn/v5 v5.0.8 // indirect
github.com/pion/turn/v5 v5.0.9 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
Expand Down
20 changes: 10 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -361,8 +361,8 @@ github.com/livekit/protocol v1.49.1-0.20260712215709-8847d7456816 h1:MDWDlH5dmcZ
github.com/livekit/protocol v1.49.1-0.20260712215709-8847d7456816/go.mod h1:jO+y05AU9Ec4JswDyuzKCZ4bhziOS0CzMqgnbj60Dzs=
github.com/livekit/psrpc v0.7.2 h1:6oZ+NODJ2pLyaT6VqDq1F4Qc/3TpDUSpyphj/P9MhQc=
github.com/livekit/psrpc v0.7.2/go.mod h1:rAI+m2+/cb4x9RXhLRtUx5ZwdfjjXOl4zi46IjEetaw=
github.com/livekit/server-sdk-go/v2 v2.17.0 h1:FzVQMoxHv0WIg164yGqSxLeV+h3aJomjAv1lFeR9MMw=
github.com/livekit/server-sdk-go/v2 v2.17.0/go.mod h1:5nzTfVBH2Jz+TW1SrfpqC7wrbcD1lT94KZCJ9hOMyvk=
github.com/livekit/server-sdk-go/v2 v2.18.1 h1:/u0JVII+ErGCivHnAGr6di0wl0NYsfcRvDzEtTAFovo=
github.com/livekit/server-sdk-go/v2 v2.18.1/go.mod h1:su0IvJNWTFCHVwmqpsFvxHfXWs9pn26ms+cKBR1jILU=
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.2 h1:fyXVu1eadI8Ap1HCCNgEhJ5McIWiYhLR8uol64ZZc40=
Expand Down Expand Up @@ -463,20 +463,20 @@ github.com/pion/rtp v1.10.2 h1:l+f6tTDcAH6xwepaAoW791ddhuYsJlqRATOzirO04Mo=
github.com/pion/rtp v1.10.2/go.mod h1:Au8fc6cEByy8RLTwKTQTEeQqDB/SJDxwL4mZuxYA5Pk=
github.com/pion/sctp v1.10.0 h1:qeoD6swF/2M5bYRcAGayqSbTKX3m4AW29CiQxG1+Pfg=
github.com/pion/sctp v1.10.0/go.mod h1:N20Dq6LY+JvJDAh9VVh1JELngb2rQ8dPgds5yBWiPgw=
github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI=
github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8=
github.com/pion/sdp/v3 v3.0.19 h1:1VMKs3gIkTQV5M3hNKfTAPrDXSNrYtOlmOD8+mSZUGQ=
github.com/pion/sdp/v3 v3.0.19/go.mod h1:dE5WOSlzXrtiE/iuZqe9n+AcEbOjtAd3k5m5NtlV/qU=
github.com/pion/srtp/v3 v3.0.11 h1:GiESUr54/K4UuPigfq/CvWUed80JenQAHXn0C2MQQIQ=
github.com/pion/srtp/v3 v3.0.11/go.mod h1:EeZOi/sd6glM1EXapg051gdNWO9yWT1YSsgQ4SlJkns=
github.com/pion/stun/v3 v3.1.4 h1:/7ZL0j0dmLroKOq4GfkyKQ6asByYqntwyHSp5sYLcGY=
github.com/pion/stun/v3 v3.1.4/go.mod h1:ET7PFiXo1nrD2ZNVpbEHDuT0kCPVXhKmyWdiePNMw/U=
github.com/pion/stun/v3 v3.1.5 h1:Y1FHlhaI6+4UoC5i/zQf4F7JvdZtB24/05oyy/GF1x8=
github.com/pion/stun/v3 v3.1.5/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs=
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
github.com/pion/turn/v5 v5.0.8 h1:pZUCtmwWCMkrRKqh/8pL3WoGADXBe0/lOPkN7oqFjK8=
github.com/pion/turn/v5 v5.0.8/go.mod h1:1VwvxElZaOdJU0liJ/WUSm/Tsh+n2OxS5ISSDxgOWxU=
github.com/pion/webrtc/v4 v4.2.14 h1:Q6zMs+fSDsYuhZcNlvFGBxCOMHVV9oYcDa6O9/HIGTc=
github.com/pion/webrtc/v4 v4.2.14/go.mod h1:87NVKP86+g4OMrRxWhjWfUjeXP4JrV6RTlUrIW+/Jak=
github.com/pion/turn/v5 v5.0.9 h1:zNeBfRyzGn7MPyUTvmvxeltLEjlFdSLPT1tlakoaOXM=
github.com/pion/turn/v5 v5.0.9/go.mod h1:u3XjBqy2Z4+NhCUpDoOSsNuQDrPLvKStlCGWk6sTQ1E=
github.com/pion/webrtc/v4 v4.2.15 h1:Ir/MauNFCfg+kgyBYPQLiGdVWFlzEcLxqtuzAkYkky0=
github.com/pion/webrtc/v4 v4.2.15/go.mod h1:CPTcyLfIzC4scOkQ4UY4pj6WvbUGhcNLIpK28cP5h6M=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
Expand Down
Loading
Loading