224 lines
4.7 KiB
Go
224 lines
4.7 KiB
Go
/*
|
|
CarterPerez-dev | 2026
|
|
dockerfile.go
|
|
|
|
Dockerfile parser that builds a structured AST with stage and command info
|
|
|
|
Wraps the Moby buildkit parser to add stage tracking, per-stage
|
|
instruction slicing, and multi-stage build awareness. DockerfileAST
|
|
exposes query methods (GetInstructions, HasInstruction, FinalStage)
|
|
so callers can scan specific instruction types without traversing
|
|
the raw AST.
|
|
|
|
Key exports:
|
|
DockerfileAST - parsed Dockerfile with Stages and Commands
|
|
ParseDockerfile, ParseDockerfileReader - parse from path or io.Reader
|
|
Stage, Command - typed instruction and stage containers
|
|
DockerfileVisitor - interface for the visitor pattern
|
|
|
|
Connects to:
|
|
visitor.go - DockerfileVisitor interface and RuleContext use these types
|
|
*/
|
|
|
|
package parser
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/moby/buildkit/frontend/dockerfile/parser"
|
|
)
|
|
|
|
type DockerfileAST struct {
|
|
Path string
|
|
Root *parser.Node
|
|
Stages []Stage
|
|
Commands []Command
|
|
}
|
|
|
|
type Stage struct {
|
|
Name string
|
|
BaseName string
|
|
BaseTag string
|
|
StartLine int
|
|
EndLine int
|
|
Commands []Command
|
|
}
|
|
|
|
type Command struct {
|
|
Instruction string
|
|
Arguments []string
|
|
Original string
|
|
StartLine int
|
|
EndLine int
|
|
Stage int
|
|
}
|
|
|
|
func ParseDockerfile(path string) (*DockerfileAST, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("opening dockerfile: %w", err)
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
return ParseDockerfileReader(path, file)
|
|
}
|
|
|
|
func ParseDockerfileReader(path string, r io.Reader) (*DockerfileAST, error) {
|
|
result, err := parser.Parse(r)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing dockerfile: %w", err)
|
|
}
|
|
|
|
ast := &DockerfileAST{
|
|
Path: path,
|
|
Root: result.AST,
|
|
}
|
|
|
|
ast.extractStructure()
|
|
|
|
return ast, nil
|
|
}
|
|
|
|
func (d *DockerfileAST) extractStructure() {
|
|
stageIndex := -1
|
|
|
|
for _, node := range d.Root.Children {
|
|
instruction := strings.ToUpper(node.Value)
|
|
|
|
cmd := Command{
|
|
Instruction: instruction,
|
|
Original: node.Original,
|
|
StartLine: node.StartLine,
|
|
EndLine: node.EndLine,
|
|
Stage: stageIndex,
|
|
}
|
|
|
|
for n := node.Next; n != nil; n = n.Next {
|
|
cmd.Arguments = append(cmd.Arguments, n.Value)
|
|
}
|
|
|
|
if instruction == "FROM" {
|
|
stageIndex++
|
|
stage := d.parseFromInstruction(node, stageIndex)
|
|
d.Stages = append(d.Stages, stage)
|
|
cmd.Stage = stageIndex
|
|
}
|
|
|
|
d.Commands = append(d.Commands, cmd)
|
|
|
|
if stageIndex >= 0 && stageIndex < len(d.Stages) {
|
|
d.Stages[stageIndex].Commands = append(
|
|
d.Stages[stageIndex].Commands,
|
|
cmd,
|
|
)
|
|
d.Stages[stageIndex].EndLine = node.EndLine
|
|
}
|
|
}
|
|
}
|
|
|
|
func (d *DockerfileAST) parseFromInstruction(
|
|
node *parser.Node,
|
|
index int,
|
|
) Stage {
|
|
stage := Stage{
|
|
StartLine: node.StartLine,
|
|
EndLine: node.EndLine,
|
|
}
|
|
|
|
if node.Next != nil {
|
|
imageRef := node.Next.Value
|
|
stage.BaseName, stage.BaseTag = parseImageReference(imageRef)
|
|
}
|
|
|
|
for n := node.Next; n != nil; n = n.Next {
|
|
if strings.ToUpper(n.Value) == "AS" && n.Next != nil {
|
|
stage.Name = n.Next.Value
|
|
break
|
|
}
|
|
}
|
|
|
|
if stage.Name == "" {
|
|
stage.Name = fmt.Sprintf("stage-%d", index)
|
|
}
|
|
|
|
return stage
|
|
}
|
|
|
|
func parseImageReference(ref string) (name, tag string) {
|
|
ref = strings.TrimSpace(ref)
|
|
|
|
if atIdx := strings.Index(ref, "@"); atIdx != -1 {
|
|
return ref[:atIdx], ref[atIdx:]
|
|
}
|
|
|
|
if colonIdx := strings.LastIndex(ref, ":"); colonIdx != -1 {
|
|
possibleTag := ref[colonIdx+1:]
|
|
if !strings.Contains(possibleTag, "/") {
|
|
return ref[:colonIdx], possibleTag
|
|
}
|
|
}
|
|
|
|
return ref, "latest"
|
|
}
|
|
|
|
func (d *DockerfileAST) GetInstructions(instruction string) []Command {
|
|
instruction = strings.ToUpper(instruction)
|
|
var result []Command
|
|
for _, cmd := range d.Commands {
|
|
if cmd.Instruction == instruction {
|
|
result = append(result, cmd)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (d *DockerfileAST) HasInstruction(instruction string) bool {
|
|
return len(d.GetInstructions(instruction)) > 0
|
|
}
|
|
|
|
func (d *DockerfileAST) GetLastInstruction(instruction string) *Command {
|
|
instructions := d.GetInstructions(instruction)
|
|
if len(instructions) == 0 {
|
|
return nil
|
|
}
|
|
return &instructions[len(instructions)-1]
|
|
}
|
|
|
|
func (d *DockerfileAST) GetStageInstructions(
|
|
stageIndex int,
|
|
instruction string,
|
|
) []Command {
|
|
instruction = strings.ToUpper(instruction)
|
|
var result []Command
|
|
for _, cmd := range d.Commands {
|
|
if cmd.Stage == stageIndex && cmd.Instruction == instruction {
|
|
result = append(result, cmd)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (d *DockerfileAST) FinalStage() *Stage {
|
|
if len(d.Stages) == 0 {
|
|
return nil
|
|
}
|
|
return &d.Stages[len(d.Stages)-1]
|
|
}
|
|
|
|
func (d *DockerfileAST) IsMultiStage() bool {
|
|
return len(d.Stages) > 1
|
|
}
|
|
|
|
func (d *DockerfileAST) Visit(visitor DockerfileVisitor) {
|
|
for _, cmd := range d.Commands {
|
|
visitor.VisitCommand(cmd)
|
|
}
|
|
}
|
|
|
|
type DockerfileVisitor interface {
|
|
VisitCommand(cmd Command)
|
|
}
|