Skip to content

Commit 92174f4

Browse files
authored
Merge pull request #162 from dgageot/refactor-cagent-build
Refactor cagent build
2 parents 7b712cd + aa0cb3c commit 92174f4

8 files changed

Lines changed: 262 additions & 205 deletions

File tree

cmd/root/build.go

Lines changed: 9 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,19 @@
11
package root
22

33
import (
4-
"fmt"
5-
"os"
6-
"os/exec"
7-
"path/filepath"
8-
"strings"
9-
104
"github.com/spf13/cobra"
115

126
"github.com/docker/cagent/internal/telemetry"
13-
"github.com/docker/cagent/pkg/config"
14-
latest "github.com/docker/cagent/pkg/config/v2"
15-
"github.com/docker/cagent/pkg/model/provider"
7+
"github.com/docker/cagent/pkg/oci"
168
)
179

1810
var push bool
1911

2012
func NewBuildCmd() *cobra.Command {
2113
cmd := &cobra.Command{
22-
Use: "build <agent-file> <image-name>",
23-
Args: cobra.ExactArgs(2),
14+
Use: "build <agent-file> [docker-image-name]",
15+
Short: "Build a Docker image for the agent",
16+
Args: cobra.MinimumNArgs(1),
2417
RunE: runBuildCommand,
2518
Hidden: true,
2619
}
@@ -33,128 +26,11 @@ func NewBuildCmd() *cobra.Command {
3326
func runBuildCommand(cmd *cobra.Command, args []string) error {
3427
telemetry.TrackCommand("build", args)
3528

36-
fileName := filepath.Base(args[0])
37-
parentDir := filepath.Dir(args[0])
38-
39-
cfg, err := config.LoadConfigSecure(fileName, parentDir)
40-
if err != nil {
41-
return err
42-
}
43-
44-
secrets := gatherRequiredEnv(cfg)
45-
mcpServers := gatherMCPServers(cfg)
46-
47-
tmp, err := os.MkdirTemp("", "build")
48-
if err != nil {
49-
return err
50-
}
51-
defer os.RemoveAll(tmp)
52-
53-
// TODO(dga): set the right entrypoint.
54-
err = os.WriteFile(filepath.Join(tmp, "Dockerfile"), fmt.Appendf(nil, `# syntax=docker/dockerfile:1
55-
FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1
56-
57-
RUN adduser -D cagent
58-
ADD https://github.com/docker/cagent/releases/download/v1.0.9/cagent-linux-arm64 /cagent
59-
RUN chmod +x /cagent
60-
COPY agent.yaml /
61-
RUN chmod 666 /agent.yaml
62-
USER cagent
63-
ENTRYPOINT ["/cagent", "run", "--debug", "--tui=false", "/agent.yaml", "get my username on github"]
64-
65-
LABEL com.docker.agent.packaging.version="v0.0.1"
66-
LABEL com.docker.agent.runtime="cagent"
67-
LABEL org.opencontainers.image.description="%s"
68-
LABEL org.opencontainers.image.licenses="%s"
69-
LABEL com.docker.agent.mcp-servers="%s"
70-
LABEL com.docker.agent.secrets="%s"
71-
`, cfg.Agents["root"].Description, cfg.Metadata.License, strings.Join(mcpServers, ","), strings.Join(secrets, ",")), 0o700)
72-
if err != nil {
73-
return err
74-
}
75-
76-
agentYaml, err := os.ReadFile(args[0])
77-
if err != nil {
78-
return err
79-
}
80-
81-
err = os.WriteFile(filepath.Join(tmp, "agent.yaml"), agentYaml, 0o700)
82-
if err != nil {
83-
return err
84-
}
85-
86-
buildArgs := []string{"build", "-t", args[1]}
87-
if push {
88-
buildArgs = append(buildArgs, "--push")
89-
}
90-
buildArgs = append(buildArgs, tmp)
91-
buildCmd := exec.CommandContext(cmd.Context(), "docker", buildArgs...)
92-
buildCmd.Stdout = os.Stdout
93-
buildCmd.Stderr = os.Stderr
94-
95-
return buildCmd.Run()
96-
}
97-
98-
func gatherRequiredEnv(cfg *latest.Config) []string {
99-
requiredEnv := map[string]bool{}
100-
101-
for name := range cfg.Models {
102-
model := cfg.Models[name]
103-
// Use the token environment variable from the alias if available
104-
if alias, exists := provider.ProviderAliases[model.Provider]; exists {
105-
if alias.TokenEnvVar != "" {
106-
requiredEnv[alias.TokenEnvVar] = true
107-
}
108-
} else {
109-
// Fallback to hardcoded mappings for unknown providers
110-
switch model.Provider {
111-
case "openai":
112-
requiredEnv["OPENAI_API_KEY"] = true
113-
case "anthropic":
114-
requiredEnv["ANTHROPIC_API_KEY"] = true
115-
case "google":
116-
requiredEnv["GOOGLE_API_KEY"] = true
117-
}
118-
}
119-
}
120-
121-
for _, agent := range cfg.Agents {
122-
model := agent.Model
123-
switch {
124-
case strings.HasPrefix(model, "openai/"):
125-
requiredEnv["OPENAI_API_KEY"] = true
126-
case strings.HasPrefix(model, "anthropic/"):
127-
requiredEnv["ANTHROPIC_API_KEY"] = true
128-
case strings.HasPrefix(model, "google/"):
129-
requiredEnv["GOOGLE_API_KEY"] = true
130-
}
131-
}
132-
133-
var requiredEnvList []string
134-
for e := range requiredEnv {
135-
requiredEnvList = append(requiredEnvList, e)
136-
}
137-
138-
return requiredEnvList
139-
}
140-
141-
func gatherMCPServers(cfg *latest.Config) []string {
142-
requiredServers := map[string]bool{}
143-
144-
for _, agent := range cfg.Agents {
145-
for i := range agent.Toolsets {
146-
toolSet := agent.Toolsets[i]
147-
148-
if toolSet.Type == "mcp" && toolSet.Ref != "" {
149-
requiredServers[toolSet.Ref] = true
150-
}
151-
}
152-
}
153-
154-
var requiredServersList []string
155-
for e := range requiredServers {
156-
requiredServersList = append(requiredServersList, e)
29+
agentFilePath := args[0]
30+
dockerImageName := ""
31+
if len(args) > 1 {
32+
dockerImageName = args[1]
15733
}
15834

159-
return requiredServersList
35+
return oci.BuildDockerImage(cmd.Context(), agentFilePath, dockerImageName, push)
16036
}

pkg/config/mcp.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package config
2+
3+
import (
4+
"sort"
5+
6+
latest "github.com/docker/cagent/pkg/config/v2"
7+
)
8+
9+
func GatherMCPServerReferences(cfg *latest.Config) []string {
10+
servers := map[string]bool{}
11+
12+
for _, agent := range cfg.Agents {
13+
for i := range agent.Toolsets {
14+
toolSet := agent.Toolsets[i]
15+
16+
if toolSet.Type == "mcp" && toolSet.Ref != "" {
17+
servers[toolSet.Ref] = true
18+
}
19+
}
20+
}
21+
22+
var list []string
23+
for e := range servers {
24+
list = append(list, e)
25+
}
26+
sort.Strings(list)
27+
28+
return list
29+
}

pkg/gateway/catalog.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func RequiredEnvVars(ctx context.Context, serverName, catalogURL string) ([]Secr
3030
return server.Secrets, nil
3131
}
3232

33+
// TODO(dga): cache the catalog.
3334
func readCatalog(ctx context.Context, url string) (Catalog, error) {
3435
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
3536
if err != nil {

pkg/oci/Dockerfile.template

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# syntax=docker/dockerfile:1
2+
FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1
3+
4+
RUN adduser -D cagent
5+
ADD https://github.com/docker/cagent/releases/download/v1.0.9/cagent-linux-arm64 /cagent
6+
RUN chmod +x /cagent
7+
RUN cat <<EOF > /agent.yaml
8+
{{ .AgentConfig }}
9+
EOF
10+
RUN chmod +r /agent.yaml && mkdir /data && chmod 777 -R /data
11+
USER cagent
12+
ENTRYPOINT ["/cagent"]
13+
CMD ["api", "--session-db", "/data/session.db", "/agent.yaml"]
14+
15+
LABEL com.docker.agent.packaging.version="v0.0.1"
16+
LABEL com.docker.agent.runtime="cagent"
17+
LABEL org.opencontainers.image.description="{{ .Description }}"
18+
LABEL org.opencontainers.image.licenses="{{ .Licenses }}"
19+
LABEL com.docker.agent.mcp-servers="{{ .McpServers }}"
20+
LABEL com.docker.agent.secrets="{{ .Secrets }}"

pkg/oci/build.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package oci
2+
3+
import (
4+
"bytes"
5+
"context"
6+
_ "embed"
7+
"log/slog"
8+
"os"
9+
"os/exec"
10+
"path/filepath"
11+
"strings"
12+
"text/template"
13+
14+
"github.com/docker/cagent/pkg/config"
15+
"github.com/docker/cagent/pkg/secrets"
16+
)
17+
18+
//go:embed Dockerfile.template
19+
var dockerfileTemplate string
20+
21+
func BuildDockerImage(ctx context.Context, agentFilePath, dockerImageName string, push bool) error {
22+
agentYaml, err := os.ReadFile(agentFilePath)
23+
if err != nil {
24+
return err
25+
}
26+
27+
fileName := filepath.Base(agentFilePath)
28+
parentDir := filepath.Dir(agentFilePath)
29+
cfg, err := config.LoadConfigSecure(fileName, parentDir)
30+
if err != nil {
31+
return err
32+
}
33+
34+
// Analyze the config to find which secrets are needed
35+
modelSecrets := secrets.GatherEnvVarsForModels(cfg)
36+
mcpServers := config.GatherMCPServerReferences(cfg)
37+
38+
// Generate the Dockerfile
39+
var dockerfileBuf bytes.Buffer
40+
41+
tpl := template.Must(template.New("Dockerfile").Parse(dockerfileTemplate))
42+
if err := tpl.Execute(&dockerfileBuf, map[string]any{
43+
"AgentConfig": string(agentYaml),
44+
"Description": cfg.Agents["root"].Description,
45+
"Licenses": cfg.Metadata.License,
46+
"McpServers": strings.Join(mcpServers, ","),
47+
"Secrets": strings.Join(modelSecrets, ","),
48+
}); err != nil {
49+
return err
50+
}
51+
52+
dockerfile := dockerfileBuf.String()
53+
slog.Debug("Generated Dockerfile", "dockerfile", dockerfile)
54+
55+
// Run docker build
56+
buildArgs := []string{"build"}
57+
if dockerImageName != "" {
58+
buildArgs = append(buildArgs, "-t", dockerImageName)
59+
if push {
60+
buildArgs = append(buildArgs, "--push")
61+
}
62+
}
63+
buildArgs = append(buildArgs, "-")
64+
65+
buildCmd := exec.CommandContext(ctx, "docker", buildArgs...)
66+
buildCmd.Stdin = strings.NewReader(dockerfile)
67+
buildCmd.Stdout = os.Stdout
68+
buildCmd.Stderr = os.Stderr
69+
slog.Debug("running docker build", "args", buildArgs)
70+
71+
return buildCmd.Run()
72+
}

0 commit comments

Comments
 (0)