You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

276 lines
8.9 KiB

/*
Copyright © 2021 Drew Short <warrick@sothr.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package dockerfile implements the required components for working with dockerfile image definitions
package dockerfile
import (
"errors"
"os"
"strings"
log "github.com/sirupsen/logrus"
buildkitInstructions "github.com/moby/buildkit/frontend/dockerfile/instructions"
buildkitParser "github.com/moby/buildkit/frontend/dockerfile/parser"
"sothr.com/warricksothr/pinned-package-updater/internal/parser"
)
// Parse reads a Dockerfile, interprets the AST and returns information about the image(s) and package(s) defined
func Parse(dockerfilePath string) (parser.ParseResult, error) {
parseLogger := log.WithFields(log.Fields{
"filePath": dockerfilePath,
})
dockerfile, err := os.Open(dockerfilePath)
if err != nil {
log.WithFields(log.Fields{
"sourceError": err,
}).Fatal("unexpected error reading file")
return parser.ParseResult{}, err
}
defer func(dockerfile *os.File) {
err := dockerfile.Close()
if err != nil {
parseLogger.WithFields(log.Fields{
"sourceError": err,
}).Error("unexpected error closing file")
}
}(dockerfile)
parsedASTResult, err := buildkitParser.Parse(dockerfile)
if err != nil {
parseLogger.WithFields(log.Fields{
"sourceError": err,
}).Fatal("unexpected error parsing Dockerfile")
return parser.ParseResult{}, err
}
parseResult, err := parseDockerfileAST(parseLogger, dockerfilePath, parsedASTResult.AST)
if err != nil {
parseLogger.WithFields(log.Fields{
"sourceError": err,
}).Fatal("unexpected error while parsing AST")
return parser.ParseResult{}, err
}
return parseResult, nil
}
/*
General Helpers
*/
// getCommandLineLocation parse commandLineLocation information into the structure we will persist
func getContentLocation(location []buildkitParser.Range) (parser.ContentLocation, error) {
commandLocationStart := location[0].Start
commandLocationEnd := location[len(location)-1].End
return parser.ContentLocation{
StartLine: commandLocationStart.Line,
StartCharacter: commandLocationStart.Character,
EndLine: commandLocationEnd.Line,
EndCharacter: commandLocationEnd.Character,
}, nil
}
/*
Parsing Helper
*/
// parseDockerfileAST Read the parsed AST and extract the required information from each stage
func parseDockerfileAST(logger *log.Entry, dockerFilePath string, dockerfileAST *buildkitParser.Node) (parser.ParseResult, error) {
dockerfileStages, _, err := buildkitInstructions.Parse(dockerfileAST)
if err != nil {
logger.WithFields(log.Fields{
"sourceError": err,
}).Fatal("unexpected error while parsing Dockerfile instructions")
return parser.ParseResult{}, err
}
var stages []*parser.Stage
for stageIndex, stage := range dockerfileStages {
stageLogger := logger.WithFields(log.Fields{
"stageIndex": stageIndex,
"stageName": stage.Name,
})
stage, err := parseDockerfileStage(stageLogger, stageIndex, stage)
if err != nil {
stageLogger.WithFields(log.Fields{
"sourceError": err,
}).Fatal("unexpected error while parsing stage")
return parser.ParseResult{}, err
}
stages = append(stages, &stage)
}
return parser.ParseResult{FilePath: dockerFilePath, Stages: stages}, nil
}
func parseDockerfileStage(logger *log.Entry, dockerfileStageIndex int, dockerfileStage buildkitInstructions.Stage) (parser.Stage, error) {
stageLocation, err := getContentLocation(dockerfileStage.Location)
if err != nil {
logger.WithFields(log.Fields{
"rawStageLocation": dockerfileStage.Location,
}).Fatal("unexpected failure parsing stage location information")
return parser.Stage{}, err
}
stageLogger := logger.WithFields(log.Fields{
"stageImage": dockerfileStage.BaseName,
"stageLocation": stageLocation,
})
imageParts := strings.Split(dockerfileStage.BaseName, ":")
if len(imageParts) < 2 {
stageLogger.Fatal("not enough information in image to determine base")
return parser.Stage{}, errors.New("not enough information in image to determine base")
}
name := dockerfileStage.Name
image := parser.Image{
Name: imageParts[0],
Tag: imageParts[1],
}
commandParser, err := parser.GetCommandParser(image)
if err != nil {
stageLogger.WithFields(log.Fields{
"sourceError": err,
}).Fatal("unexpected error while determining command parser for image")
return parser.Stage{}, err
}
repositories, repositoryCommandLocations, err := parseRepositoriesFromDockerfileStage(stageLogger, dockerfileStage, commandParser)
if err != nil {
stageLogger.WithFields(log.Fields{
"sourceError": err,
}).Fatal("unexpected error while parsing repositories from stage")
return parser.Stage{}, err
}
packages, packageCommandLocations, err := parsePackagesFromDockerfileStage(stageLogger, dockerfileStage, commandParser)
if err != nil {
stageLogger.WithFields(log.Fields{
"sourceError": err,
}).Fatal("unexpected error while parsing packages from stage")
return parser.Stage{}, err
}
var commandLocations []*parser.ContentLocation
commandLocations = append(commandLocations, repositoryCommandLocations...)
commandLocations = append(commandLocations, packageCommandLocations...)
return parser.Stage{
Index: dockerfileStageIndex,
Name: name,
Image: &image,
StageLocation: stageLocation,
Repositories: repositories,
Packages: packages,
CommandLocations: commandLocations,
}, nil
}
func parseRepositoriesFromDockerfileStage(logger *log.Entry, dockerfileStage buildkitInstructions.Stage, commandParser *parser.CommandParser) ([]*parser.Repository, []*parser.ContentLocation, error) {
var repositories []*parser.Repository
var commandLocations []*parser.ContentLocation
typedCommandParser := *commandParser
for _, command := range dockerfileStage.Commands {
switch typedCommand := command.(type) {
case *buildkitInstructions.RunCommand:
commandLocation, err := getContentLocation(typedCommand.Location())
if err != nil {
logger.WithFields(log.Fields{
"rawCommandLineLocation": typedCommand.Location(),
}).Fatal("unexpected failure parsing script location information")
return repositories, commandLocations, err
}
commandLocations = append(commandLocations, &commandLocation)
for _, line := range typedCommand.CmdLine {
logger.WithFields(log.Fields{
"commandLine": line,
"commandLocation": commandLocation,
}).Trace("parsing RunCommand for repositories")
parsedRepositories, err := typedCommandParser.GetRepositories(logger, line)
if err != nil {
logger.WithFields(log.Fields{
"commandLine": line,
"commandLocation": commandLocation,
"sourceError": err,
}).Fatal("unexpected error while parsing repositories")
return repositories, commandLocations, err
}
for _, parsedRepository := range parsedRepositories {
repositories = append(repositories, parsedRepository)
}
}
}
}
return repositories, commandLocations, nil
}
func parsePackagesFromDockerfileStage(logger *log.Entry, dockerfileStage buildkitInstructions.Stage, commandParser *parser.CommandParser) ([]*parser.Package, []*parser.ContentLocation, error) {
var packages []*parser.Package
var commandLocations []*parser.ContentLocation
typedCommandParser := *commandParser
for _, command := range dockerfileStage.Commands {
switch typedCommand := command.(type) {
case *buildkitInstructions.RunCommand:
commandLocation, err := getContentLocation(typedCommand.Location())
if err != nil {
logger.WithFields(log.Fields{
"rawCommandLineLocation": typedCommand.Location(),
}).Fatal("unexpected failure parsing script location information")
return packages, commandLocations, err
}
commandLocations = append(commandLocations, &commandLocation)
for _, line := range typedCommand.CmdLine {
logger.WithFields(log.Fields{
"commandLine": line,
"commandLocation": commandLocation,
}).Trace("parsing RunCommand for packages")
parsedPinnedPackages, err := typedCommandParser.GetPinnedPackages(logger, line)
if err != nil {
logger.WithFields(log.Fields{
"commandLine": line,
"commandLocation": commandLocation,
"sourceError": err,
}).Fatal("unexpected error while parsing pinned packages")
return packages, commandLocations, err
}
for _, parsedPinnedPackage := range parsedPinnedPackages {
packages = append(packages, parsedPinnedPackage)
}
}
}
}
return packages, commandLocations, nil
}