package image
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
)
type ExtensionPlan struct {
KernelFile string
InitrdFile string
NeedIndex bool
NeedSnap bool
}
type PreprocessResult struct {
TempDockerfile string
Plan ExtensionPlan
}
func PreprocessDockerfile(dockerfilePath, contextDir string) (PreprocessResult, error) {
var res PreprocessResult
var absCtx string
if contextDir != "" {
var err error
absCtx, err = filepath.Abs(contextDir)
if err != nil {
return res, fmt.Errorf("context directory: %w", err)
}
}
f, err := os.Open(dockerfilePath)
if err != nil {
return res, fmt.Errorf("open dockerfile: %w", err)
}
defer f.Close()
var out strings.Builder
sc := bufio.NewScanner(f)
lineNo := 0
kernelCount, indexCount, snapCount := 0, 0, 0
for sc.Scan() {
lineNo++
line := sc.Text()
trim := strings.TrimSpace(line)
fields := strings.Fields(trim)
if len(fields) > 0 && strings.EqualFold(fields[0], "KERNEL") {
kernelCount++
if len(fields) != 3 {
return res, fmt.Errorf("dockerfile line %d: KERNEL requires exactly two arguments (kernel_file initrd_file)", lineNo)
}
res.Plan.KernelFile = fields[1]
res.Plan.InitrdFile = fields[2]
continue
}
if len(fields) == 1 && strings.EqualFold(fields[0], "SNAP") {
if kernelCount == 0 {
return res, fmt.Errorf("dockerfile line %d: SNAP requires a preceding KERNEL <kernel> <initrd>", lineNo)
}
snapCount++
res.Plan.NeedSnap = true
continue
}
if len(fields) == 1 && strings.EqualFold(fields[0], "INDEX") {
if kernelCount == 0 {
return res, fmt.Errorf("dockerfile line %d: INDEX requires a preceding KERNEL <kernel> <initrd>", lineNo)
}
indexCount++
res.Plan.NeedIndex = true
continue
}
out.WriteString(line)
out.WriteString("\n")
}
if err := sc.Err(); err != nil {
return res, fmt.Errorf("read dockerfile: %w", err)
}
if snapCount > 1 {
return res, fmt.Errorf("dockerfile: at most one SNAP instruction is allowed")
}
if indexCount > 1 {
return res, fmt.Errorf("dockerfile: at most one INDEX instruction is allowed")
}
if res.Plan.NeedSnap && kernelCount == 0 {
return res, fmt.Errorf("dockerfile: SNAP requires a preceding KERNEL <kernel> <initrd>")
}
if res.Plan.NeedIndex && kernelCount == 0 {
return res, fmt.Errorf("dockerfile: INDEX requires a preceding KERNEL <kernel> <initrd>")
}
if res.Plan.NeedIndex && res.Plan.NeedSnap {
return res, fmt.Errorf("dockerfile: INDEX and SNAP cannot be used together")
}
if kernelCount > 1 {
return res, fmt.Errorf("dockerfile: at most one KERNEL instruction is allowed in this version")
}
if absCtx != "" && res.Plan.KernelFile != "" && res.Plan.InitrdFile != "" {
for _, name := range []string{res.Plan.KernelFile, res.Plan.InitrdFile} {
p := filepath.Clean(filepath.Join(absCtx, name))
rel, relErr := filepath.Rel(absCtx, p)
if relErr != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
return res, fmt.Errorf("KERNEL file %q escapes context directory", name)
}
if st, stErr := os.Stat(p); stErr != nil {
return res, fmt.Errorf("KERNEL file %q under context: %w", name, stErr)
} else if st.IsDir() {
return res, fmt.Errorf("KERNEL path %q is a directory", name)
}
}
}
tmp, err := os.CreateTemp(filepath.Dir(dockerfilePath), "conch-dockerfile-*.tmp")
if err != nil {
return res, fmt.Errorf("create temp dockerfile: %w", err)
}
path := tmp.Name()
if _, err := tmp.WriteString(out.String()); err != nil {
tmp.Close()
os.Remove(path)
return res, fmt.Errorf("write temp dockerfile: %w", err)
}
if err := tmp.Close(); err != nil {
os.Remove(path)
return res, err
}
res.TempDockerfile = path
return res, nil
}