Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
m1k1o
GitHub Repository: m1k1o/neko
Path: blob/master/utils/docker/main.go
1007 views
1
/*
2
This program processes a Dockerfile. When it encounters a FROM command with a relative path,
3
it pastes the content of the referenced Dockerfile into the current Dockerfile with some modifications:
4
- It ensures that all ADD and COPY commands point to the correct context path by adding the relative path
5
to the first part of the command (the file or directory being copied).
6
- It takes the ARG variables defined before the FROM command and prepends them with the alias of the
7
FROM command. It also replaces any occurrences of the ARG variables in the Dockerfile with the new prefixed
8
variables. Then it writes them to the beginning of the new Dockerfile.
9
- It allows user to specify -client flag to just include already built client directory in the Dockerfile.
10
If no client path is specified, it will build the client from the Dockerfile.
11
12
It allows to split large multi-stage Dockerfiles into own directories where they can be built independently. It also
13
allows to dynamically join these Dockerfiles into a single Dockerfile based on various conditions.
14
*/
15
package main
16
17
import (
18
"bufio"
19
"bytes"
20
"flag"
21
"fmt"
22
"log"
23
"os"
24
"path/filepath"
25
"strings"
26
)
27
28
func main() {
29
inputPath := flag.String("i", "", "Path to the input Dockerfile")
30
outputPath := flag.String("o", "", "Path to the output Dockerfile")
31
clientPath := flag.String("client", "", "Path to the client directory, if not set, the client will be built")
32
flag.Parse()
33
34
if *inputPath == "" {
35
log.Println("Usage: go run main.go -i <input Dockerfile> [-o <output Dockerfile>]")
36
os.Exit(1)
37
}
38
39
buildcontext, err := ButidContextFromPath(*inputPath)
40
if err != nil {
41
log.Printf("Error: %v\n", err)
42
os.Exit(1)
43
}
44
45
err = ProcessDockerfile(buildcontext, *outputPath, *clientPath)
46
if err != nil {
47
log.Printf("Error: %v\n", err)
48
os.Exit(1)
49
}
50
}
51
52
type Dockerfile struct {
53
ctx BuildContext // build context for the current Dockerfile
54
args ArgCommand // global args defined in the Dockerfile
55
56
w *bytes.Buffer
57
}
58
59
// Include reads the requested Dockerfile, modifies it to point to the new context path, and includes it in the
60
// current Dockerfile. It also replaces the ARG variables with the new prefixed variables.
61
func (d *Dockerfile) Include(ctx BuildContext, alias string) error {
62
// read the Dockerfile
63
raw, err := os.ReadFile(ctx.String())
64
if err != nil {
65
return fmt.Errorf("failed to read Dockerfile: %w", err)
66
}
67
68
// count how many FROM lines are in the Dockerfile, we need to know which one is the last one
69
// to replace it with our alias
70
fromCount := 0
71
scanner := bufio.NewScanner(bytes.NewReader(raw))
72
for scanner.Scan() {
73
line := scanner.Text()
74
if strings.HasPrefix(line, "FROM") {
75
fromCount++
76
}
77
}
78
if err := scanner.Err(); err != nil {
79
return fmt.Errorf("failed to read Dockerfile: %w", err)
80
}
81
82
// new context path relative to the current context path
83
newContextPath, err := filepath.Rel(d.ctx.ContextPath, ctx.ContextPath)
84
if err != nil {
85
return fmt.Errorf("failed to get relative path: %w", err)
86
}
87
88
// use argPrefix to prepend the alias to the ARG variables
89
argPrefix := strings.ToUpper(alias) + "_"
90
// replace - with _ in the alias
91
argPrefix = strings.ReplaceAll(argPrefix, "-", "_")
92
// use aliasPrefix to prepend the alias to the ARG variables
93
aliasPrefix := alias + "-"
94
95
beforeFrom := true
96
globalArgs := ArgCommand{}
97
98
// read the Dockerfile line by line and modify it
99
scanner = bufio.NewScanner(bytes.NewReader(raw))
100
nthFrom := 0
101
for scanner.Scan() {
102
line := scanner.Text()
103
104
// handle ARG lines defined before FROM
105
if !beforeFrom {
106
line = globalArgs.ReplaceArgPrefix(argPrefix, line)
107
}
108
109
// we need to move the ARG lines before the FROM line
110
if strings.HasPrefix(line, "ARG") {
111
args, err := ParseArgCommand(line)
112
if err != nil {
113
return fmt.Errorf("failed to parse ARG command: %w", err)
114
}
115
if beforeFrom {
116
globalArgs = append(globalArgs, args...)
117
log.Printf("[%s] Found global %q before FROM, moving it to the beginning.\n", ctx, args)
118
} else {
119
// if we are not before FROM and it matches one of the global args, we need to add prefix to it
120
// because they may be redefined in the Dockerfile
121
argKeys := make(map[string]struct{})
122
for _, arg := range globalArgs {
123
argKeys[arg.Key] = struct{}{}
124
}
125
for i := range args {
126
if _, ok := argKeys[args[i].Key]; ok {
127
log.Printf("[%s] Found global ARG %q after FROM, adding %q prefix.\n", ctx, args[i].Key, argPrefix)
128
args[i].Key = argPrefix + args[i].Key
129
}
130
}
131
d.w.WriteString(args.String() + "\n")
132
}
133
continue
134
}
135
136
// modify FROM lines
137
if strings.HasPrefix(line, "FROM") {
138
nthFrom++
139
140
// parse the FROM command
141
cmd, err := ParseFromCommand(line)
142
if err != nil {
143
return fmt.Errorf("failed to parse FROM command: %w", err)
144
}
145
146
// handle the case where ARGs are defined before FROM
147
cmd.Image = globalArgs.ReplaceArgPrefix(argPrefix, cmd.Image)
148
149
if nthFrom == fromCount && cmd.Alias != alias {
150
log.Printf("[%s] Replacing alias in %q with %q.\n", ctx, cmd, cmd.Alias)
151
// if this is the last FROM line, we need to replace with our alias
152
cmd.Alias = alias
153
}
154
if nthFrom != fromCount && alias != "" {
155
log.Printf("[%s] Adding alias prefix %q to %q.\n", ctx, aliasPrefix, cmd)
156
// this is not the last FROM line, add prefix to the alias
157
cmd.Alias = aliasPrefix + cmd.Alias
158
}
159
160
beforeFrom = false
161
d.w.WriteString(cmd.String() + "\n")
162
continue
163
}
164
165
// modify COPY and ADD lines
166
if strings.HasPrefix(line, "COPY") || strings.HasPrefix(line, "ADD") {
167
// parse the COPY/ADD command
168
cmd, err := ParseCopyAddCommand(line)
169
if err != nil {
170
return fmt.Errorf("failed to parse COPY/ADD command: %w", err)
171
}
172
173
if _, ok := cmd.Args["from"]; !ok {
174
// replace the from part with the new context path
175
newFrom := filepath.Join(newContextPath, cmd.From)
176
log.Printf("[%s] Path replace: %s -> %s\n", ctx, cmd.From, newFrom)
177
cmd.From = newFrom
178
} else {
179
// add alias prefix to the --from argument
180
log.Printf("[%s] Found COPY/ADD with --from=%s, adding %q alias prefix.\n", ctx, cmd.Args["from"], aliasPrefix)
181
cmd.Args["from"] = aliasPrefix + cmd.Args["from"]
182
}
183
184
d.w.WriteString(cmd.String() + "\n")
185
continue
186
}
187
188
// write the line as is
189
d.w.WriteString(line + "\n")
190
}
191
192
// add prefix to global ARGs
193
globalArgs.WithPrefix(argPrefix)
194
195
// add the global ARGs to the beginning of the new Dockerfile
196
d.args = append(d.args, globalArgs...)
197
198
return scanner.Err()
199
}
200
201
// Process processes the Dockerfile and resolves sub-Dockerfiles in it
202
func ProcessDockerfile(ctx BuildContext, outputPath, clientPath string) error {
203
d := &Dockerfile{
204
ctx: ctx,
205
args: make(ArgCommand, 0),
206
w: bytes.NewBuffer(nil),
207
}
208
209
// read the Dockerfile
210
raw, err := os.ReadFile(ctx.String())
211
if err != nil {
212
return fmt.Errorf("failed to read Dockerfile: %w", err)
213
}
214
215
// read the Dockerfile line by line and modify it
216
scanner := bufio.NewScanner(bytes.NewReader(raw))
217
for scanner.Scan() {
218
line := scanner.Text()
219
220
// modify FROM lines
221
if strings.HasPrefix(line, "FROM ./") {
222
// parse the FROM command
223
cmd, err := ParseFromCommand(line)
224
if err != nil {
225
return fmt.Errorf("failed to parse FROM command: %w", err)
226
}
227
228
// if we are not building the client, skip this line
229
if clientPath != "" && cmd.Alias == "client" {
230
log.Printf("[%s] Skipping FROM client line.\n", ctx)
231
continue
232
}
233
234
// resolve environment variables in the image name
235
cmd.Image = os.ExpandEnv(cmd.Image)
236
237
// create a new build context
238
newBuildcontext, err := ButidContextFromPath(filepath.Join(ctx.ContextPath, cmd.Image))
239
if err != nil {
240
return fmt.Errorf("failed to get build context: %w", err)
241
}
242
243
// resolve the dockerfile content
244
err = d.Include(newBuildcontext, cmd.Alias)
245
if err != nil {
246
return fmt.Errorf("failed to get relative Dockerfile: %w", err)
247
}
248
249
continue
250
}
251
252
// modify COPY and ADD lines
253
if strings.HasPrefix(line, "COPY") || strings.HasPrefix(line, "ADD") {
254
// parse the COPY/ADD command
255
cmd, err := ParseCopyAddCommand(line)
256
if err != nil {
257
return fmt.Errorf("failed to parse COPY/ADD command: %w", err)
258
}
259
260
// if we are not building the client, take if from the client path
261
if clientPath != "" && cmd.Args["from"] == "client" {
262
log.Printf("[%s] Replacing COPY/ADD --from=client with %q.\n", ctx, clientPath)
263
delete(cmd.Args, "from")
264
cmd.From = clientPath
265
d.w.WriteString(cmd.String() + "\n")
266
continue
267
}
268
}
269
270
// copy all other lines as is
271
d.w.WriteString(line + "\n")
272
}
273
274
// check for errors while reading the Dockerfile
275
if err := scanner.Err(); err != nil {
276
return fmt.Errorf("failed to read input Dockerfile: %w", err)
277
}
278
279
// add the global ARGs to the beginning of the new Dockerfile
280
prefix := "# THIS FILE IS GENERATED, DO NOT EDIT\n"
281
outBytes := append([]byte(prefix+d.args.MultiLineString()), d.w.Bytes()...)
282
283
if outputPath != "" {
284
// write the new Dockerfile to the output path
285
return os.WriteFile(outputPath, outBytes, 0644)
286
}
287
288
// write to stdout
289
fmt.Print(string(outBytes))
290
return nil
291
}
292
293
// BuildContext represents the build context for a Dockerfile
294
type BuildContext struct {
295
ContextPath string
296
Dockerfile string // if empty, use the default Dockerfile name
297
}
298
299
func ButidContextFromPath(path string) (BuildContext, error) {
300
// check if the path exists
301
fi, err := os.Stat(path)
302
if os.IsNotExist(err) {
303
return BuildContext{}, fmt.Errorf("path does not exist: %s", path)
304
}
305
306
// check if the path is a directory
307
if err == nil && fi.IsDir() {
308
return BuildContext{
309
ContextPath: path,
310
Dockerfile: "Dockerfile",
311
}, nil
312
}
313
314
return BuildContext{
315
ContextPath: filepath.Dir(path),
316
Dockerfile: filepath.Base(path),
317
}, nil
318
}
319
320
func (bc BuildContext) String() string {
321
if bc.Dockerfile != "" {
322
return filepath.Join(bc.ContextPath, bc.Dockerfile)
323
}
324
return filepath.Join(bc.ContextPath, "Dockerfile")
325
}
326
327
// FromCommand represents the FROM command in a Dockerfile
328
type FromCommand struct {
329
Image string
330
Alias string
331
Platform string
332
}
333
334
func ParseFromCommand(line string) (fc FromCommand, err error) {
335
parts := strings.Fields(line)
336
if len(parts) < 2 || strings.ToLower(parts[0]) != "from" {
337
err = fmt.Errorf("invalid FROM line: %s", line)
338
return
339
}
340
for i := 1; i < len(parts); i++ {
341
if strings.HasPrefix(parts[i], "--platform=") {
342
fc.Platform = strings.TrimPrefix(parts[i], "--platform=")
343
}
344
if strings.ToLower(parts[i]) == "as" && i+1 < len(parts) {
345
fc.Alias = parts[i+1]
346
break
347
}
348
fc.Image = parts[i]
349
}
350
return
351
}
352
353
func (fc FromCommand) String() string {
354
var sb strings.Builder
355
sb.WriteString("FROM ")
356
if fc.Platform != "" {
357
sb.WriteString(fmt.Sprintf("--platform=%s ", fc.Platform))
358
}
359
sb.WriteString(fc.Image)
360
if fc.Alias != "" {
361
sb.WriteString(fmt.Sprintf(" AS %s", fc.Alias))
362
}
363
return sb.String()
364
}
365
366
// ArgCommand represents the ARG command in a Dockerfile
367
type Arg struct {
368
Key string
369
Value string
370
}
371
372
type ArgCommand []Arg
373
374
func ParseArgCommand(line string) (ac ArgCommand, err error) {
375
parts := strings.Fields(line)
376
if len(parts) < 2 || strings.ToLower(parts[0]) != "arg" {
377
err = fmt.Errorf("invalid ARG line: %s", line)
378
return
379
}
380
381
for i := 1; i < len(parts); i++ {
382
if strings.Contains(parts[i], "=") {
383
kv := strings.SplitN(parts[i], "=", 2)
384
if len(kv) == 2 {
385
ac = append(ac, Arg{Key: kv[0], Value: kv[1]})
386
} else {
387
ac = append(ac, Arg{Key: kv[0], Value: ""})
388
}
389
} else {
390
ac = append(ac, Arg{Key: parts[i], Value: ""})
391
}
392
}
393
394
return
395
}
396
397
func (ac ArgCommand) String() string {
398
var sb strings.Builder
399
sb.WriteString("ARG ")
400
for _, arg := range ac {
401
sb.WriteString(arg.Key)
402
if v := arg.Value; v != "" {
403
sb.WriteString("=" + v)
404
}
405
sb.WriteString(" ")
406
}
407
return sb.String()
408
}
409
410
func (ac ArgCommand) MultiLineString() string {
411
var sb strings.Builder
412
for _, arg := range ac {
413
sb.WriteString("ARG ")
414
sb.WriteString(arg.Key)
415
if v := arg.Value; v != "" {
416
sb.WriteString("=" + v)
417
}
418
sb.WriteString("\n")
419
}
420
return sb.String()
421
}
422
423
func (ac ArgCommand) WithPrefix(prefix string) {
424
for i := range ac {
425
if ac[i].Key != "" {
426
ac[i].Key = prefix + ac[i].Key
427
}
428
}
429
}
430
431
func (ac ArgCommand) ReplaceArgPrefix(prefix string, val string) string {
432
for _, arg := range ac {
433
val = strings.ReplaceAll(val, "$"+arg.Key, "$"+prefix+arg.Key)
434
val = strings.ReplaceAll(val, "${"+arg.Key+"}", "${"+prefix+arg.Key+"}")
435
}
436
return val
437
}
438
439
// CopyAddCommand represents the COPY and ADD commands in a Dockerfile
440
type CopyAddCommand struct {
441
Command string
442
Args map[string]string
443
From string
444
To string
445
}
446
447
func ParseCopyAddCommand(line string) (ca CopyAddCommand, err error) {
448
parts := strings.Fields(line)
449
if len(parts) < 2 || (strings.ToLower(parts[0]) != "copy" && strings.ToLower(parts[0]) != "add") {
450
err = fmt.Errorf("invalid COPY/ADD line: %s", line)
451
return
452
}
453
454
ca.Command = parts[0]
455
456
ca.Args = make(map[string]string)
457
for i := 1; i < len(parts); i++ {
458
if strings.HasPrefix(parts[i], "--") {
459
kv := strings.SplitN(parts[i][2:], "=", 2)
460
if len(kv) == 2 {
461
ca.Args[kv[0]] = kv[1]
462
} else {
463
ca.Args[kv[0]] = ""
464
}
465
continue
466
}
467
if ca.From == "" {
468
ca.From = parts[i]
469
continue
470
}
471
if ca.To == "" {
472
ca.To = parts[i]
473
continue
474
}
475
}
476
477
return
478
}
479
480
func (ca CopyAddCommand) String() string {
481
var sb strings.Builder
482
sb.WriteString(ca.Command + " ")
483
for k, v := range ca.Args {
484
sb.WriteString("--" + k)
485
if v != "" {
486
sb.WriteString("=" + v)
487
}
488
sb.WriteString(" ")
489
}
490
if ca.From != "" {
491
sb.WriteString(ca.From + " ")
492
}
493
if ca.To != "" {
494
sb.WriteString(ca.To)
495
}
496
return sb.String()
497
}
498
499