Skip to content

Commit 14f7ba5

Browse files
committed
Add generate dockerfile command
1 parent 8011f4d commit 14f7ba5

File tree

2 files changed

+150
-24
lines changed

2 files changed

+150
-24
lines changed

cmd/lk/agent.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"errors"
2020
"fmt"
2121
"os"
22+
"path/filepath"
2223
"regexp"
2324
"slices"
2425
"strings"
@@ -111,6 +112,22 @@ var (
111112
DisableSliceFlagSeparator: true,
112113
ArgsUsage: "[working-dir]",
113114
},
115+
{
116+
Name: "dockerfile",
117+
Usage: "Generate Dockerfile and .dockerignore for your project",
118+
Before: createAgentClient,
119+
Action: generateAgentDockerfile,
120+
Flags: []cli.Flag{
121+
silentFlag,
122+
&cli.BoolFlag{
123+
Name: "overwrite",
124+
Usage: "Overwrite existing Dockerfile and/or .dockerignore if they exist",
125+
Required: false,
126+
Value: false,
127+
},
128+
},
129+
ArgsUsage: "[working-dir]",
130+
},
114131
{
115132
Name: "config",
116133
Usage: fmt.Sprintf("Creates a %s in the working directory for an existing agent.", config.LiveKitTOMLFile),
@@ -378,6 +395,7 @@ func createAgent(ctx context.Context, cmd *cli.Command) error {
378395
}
379396

380397
projectType, err := agentfs.DetectProjectType(workingDir)
398+
fmt.Printf("Detected project type [%s]\n", util.Accented(string(projectType)))
381399
if err != nil {
382400
return fmt.Errorf("unable to determine project type: %w, please use a supported project type, or create your own Dockerfile in the current directory", err)
383401
}
@@ -1213,3 +1231,69 @@ func requireConfig(workingDir, tomlFilename string) (bool, error) {
12131231
lkConfig, exists, err = config.LoadTOMLFile(workingDir, tomlFilename)
12141232
return exists, err
12151233
}
1234+
1235+
func generateAgentDockerfile(ctx context.Context, cmd *cli.Command) error {
1236+
if cmd.NArg() > 0 {
1237+
workingDir = cmd.Args().First()
1238+
}
1239+
1240+
if stat, err := os.Stat(workingDir); err != nil || !stat.IsDir() {
1241+
return fmt.Errorf("invalid working directory: %s", workingDir)
1242+
}
1243+
1244+
settingsMap, err := getClientSettings(ctx, cmd.Bool("silent"))
1245+
if err != nil {
1246+
return err
1247+
}
1248+
1249+
projectType, err := agentfs.DetectProjectType(workingDir)
1250+
fmt.Printf("Detected project type [%s]\n", util.Accented(string(projectType)))
1251+
if err != nil {
1252+
return fmt.Errorf("unable to determine project type: %w, please use a supported project type, or create your own Dockerfile in the current directory", err)
1253+
}
1254+
1255+
dockerfilePath := filepath.Join(workingDir, "Dockerfile")
1256+
dockerignorePath := filepath.Join(workingDir, ".dockerignore")
1257+
overwrite := cmd.Bool("overwrite")
1258+
1259+
writeDockerfile := true
1260+
writeDockerignore := true
1261+
if !overwrite {
1262+
if _, err := os.Stat(dockerfilePath); err == nil {
1263+
fmt.Println("Dockerfile already exists; skipping. Use --overwrite to replace.")
1264+
writeDockerfile = false
1265+
}
1266+
if _, err := os.Stat(dockerignorePath); err == nil {
1267+
fmt.Println(".dockerignore already exists; skipping. Use --overwrite to replace.")
1268+
writeDockerignore = false
1269+
}
1270+
}
1271+
1272+
if !writeDockerfile && !writeDockerignore {
1273+
fmt.Println("No files to write.")
1274+
return nil
1275+
}
1276+
1277+
// Generate contents without writing
1278+
dockerfileContent, dockerignoreContent, err := agentfs.GenerateDockerArtifacts(workingDir, projectType, settingsMap)
1279+
if err != nil {
1280+
return err
1281+
}
1282+
1283+
if writeDockerfile {
1284+
if err := os.WriteFile(dockerfilePath, dockerfileContent, 0644); err != nil {
1285+
return err
1286+
}
1287+
1288+
fmt.Printf("Wrote %s\n", util.Accented("Dockerfile"))
1289+
}
1290+
1291+
if writeDockerignore {
1292+
if err := os.WriteFile(dockerignorePath, dockerignoreContent, 0644); err != nil {
1293+
return err
1294+
}
1295+
fmt.Printf("Wrote %s\n", util.Accented(".dockerignore"))
1296+
}
1297+
1298+
return nil
1299+
}

pkg/agentfs/docker.go

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"os"
2222
"path/filepath"
2323
"slices"
24+
"sort"
2425
"strings"
2526
"text/template"
2627

@@ -49,43 +50,49 @@ func HasDockerfile(dir string) (bool, error) {
4950
}
5051

5152
func CreateDockerfile(dir string, projectType ProjectType, settingsMap map[string]string) error {
52-
if len(settingsMap) == 0 {
53-
return fmt.Errorf("unable to fetch client settings from server, please try again later")
53+
dockerfileContent, dockerIgnoreContent, err := GenerateDockerArtifacts(dir, projectType, settingsMap)
54+
if err != nil {
55+
return err
5456
}
5557

56-
var dockerfileContent []byte
57-
var dockerIgnoreContent []byte
58-
var err error
59-
60-
dockerfileContent, err = fs.ReadFile("examples/" + string(projectType) + ".Dockerfile")
61-
if err != nil {
58+
if err := os.WriteFile(filepath.Join(dir, "Dockerfile"), dockerfileContent, 0644); err != nil {
6259
return err
6360
}
6461

65-
dockerIgnoreContent, err = fs.ReadFile("examples/" + string(projectType) + ".dockerignore")
66-
if err != nil {
62+
if err := os.WriteFile(filepath.Join(dir, ".dockerignore"), dockerIgnoreContent, 0644); err != nil {
6763
return err
6864
}
6965

70-
// TODO: (@rektdeckard) support Node entrypoint validation
71-
if projectType.IsPython() {
72-
dockerfileContent, err = validateEntrypoint(dir, dockerfileContent, dockerIgnoreContent, projectType, settingsMap)
73-
if err != nil {
74-
return err
75-
}
66+
return nil
67+
}
68+
69+
// GenerateDockerArtifacts returns the Dockerfile and .dockerignore contents for the
70+
// provided project type without writing them to disk. The Dockerfile content may be
71+
// templated/validated (e.g., Python entrypoint).
72+
func GenerateDockerArtifacts(dir string, projectType ProjectType, settingsMap map[string]string) ([]byte, []byte, error) {
73+
if len(settingsMap) == 0 {
74+
return nil, nil, fmt.Errorf("unable to fetch client settings from server, please try again later")
7675
}
7776

78-
err = os.WriteFile(filepath.Join(dir, "Dockerfile"), dockerfileContent, 0644)
77+
dockerfileContent, err := fs.ReadFile("examples/" + string(projectType) + ".Dockerfile")
7978
if err != nil {
80-
return err
79+
return nil, nil, err
8180
}
8281

83-
err = os.WriteFile(filepath.Join(dir, ".dockerignore"), dockerIgnoreContent, 0644)
82+
dockerIgnoreContent, err := fs.ReadFile("examples/" + string(projectType) + ".dockerignore")
8483
if err != nil {
85-
return err
84+
return nil, nil, err
8685
}
8786

88-
return nil
87+
// TODO: (@rektdeckard) support Node entrypoint validation
88+
if projectType.IsPython() {
89+
dockerfileContent, err = validateEntrypoint(dir, dockerfileContent, dockerIgnoreContent, projectType, settingsMap)
90+
if err != nil {
91+
return nil, nil, err
92+
}
93+
}
94+
95+
return dockerfileContent, dockerIgnoreContent, nil
8996
}
9097

9198
func validateEntrypoint(dir string, dockerfileContent []byte, dockerignoreContent []byte, projectType ProjectType, settingsMap map[string]string) ([]byte, error) {
@@ -115,13 +122,16 @@ func validateEntrypoint(dir string, dockerfileContent []byte, dockerignoreConten
115122
return err
116123
}
117124
if !d.IsDir() && strings.HasSuffix(d.Name(), projectType.FileExt()) {
125+
// Exclude files like __init__.py which cannot be entrypoints
126+
if d.Name() == "__init__.py" {
127+
return nil
128+
}
118129
fileList = append(fileList, path)
119130
}
120131
return nil
121132
}); err != nil {
122133
return "", fmt.Errorf("error walking directory %s: %w", dir, err)
123134
}
124-
125135
if slices.Contains(fileList, fileName) {
126136
return fileName, nil
127137
}
@@ -131,11 +141,39 @@ func validateEntrypoint(dir string, dockerfileContent []byte, dockerignoreConten
131141
return "", nil
132142
}
133143

144+
// Prioritize common entrypoint filenames at the top of the list
145+
if len(fileList) > 1 {
146+
priority := func(p string) int {
147+
name := filepath.Base(p)
148+
switch name {
149+
case "main.py":
150+
return 0
151+
case "agent.py":
152+
return 1
153+
default:
154+
return 2
155+
}
156+
}
157+
sort.SliceStable(fileList, func(i, j int) bool {
158+
pi := priority(fileList[i])
159+
pj := priority(fileList[j])
160+
if pi != pj {
161+
return pi < pj
162+
}
163+
return fileList[i] < fileList[j]
164+
})
165+
}
166+
167+
// If there's only one candidate, select it automatically
168+
if len(fileList) == 1 {
169+
return fileList[0], nil
170+
}
171+
134172
var selected string
135173
form := huh.NewForm(
136174
huh.NewGroup(
137175
huh.NewSelect[string]().
138-
Title(fmt.Sprintf("Select %s file to use as entrypoint", projectType.Lang())).
176+
Title(fmt.Sprintf("Select the %s file which contains your agent's entrypoint", projectType.Lang())).
139177
Options(huh.NewOptions(fileList...)...).
140178
Value(&selected).
141179
WithTheme(util.Theme),
@@ -145,7 +183,6 @@ func validateEntrypoint(dir string, dockerfileContent []byte, dockerignoreConten
145183
if err := form.Run(); err != nil {
146184
return "", err
147185
}
148-
149186
return selected, nil
150187
}
151188

@@ -159,6 +196,11 @@ func validateEntrypoint(dir string, dockerfileContent []byte, dockerignoreConten
159196
return nil, err
160197
}
161198

199+
if newEntrypoint == "" {
200+
newEntrypoint = pythonEntrypoint
201+
}
202+
fmt.Printf("Using entrypoint file [%s]\n", util.Accented(newEntrypoint))
203+
162204
tpl := template.Must(template.New("Dockerfile").Parse(string(dockerfileContent)))
163205
buf := &bytes.Buffer{}
164206
tpl.Execute(buf, map[string]string{

0 commit comments

Comments
 (0)