/* Copyright © 2021 Drew Short 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" "fmt" "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) { dockerfile, dockerfileOpenErr := os.Open(dockerfilePath) if dockerfileOpenErr != nil { log.Fatalf("Unexpected error reading file %s: %s", dockerfilePath, dockerfileOpenErr) return parser.ParseResult{}, dockerfileOpenErr } defer func(dockerfile *os.File) { dockerfileCloseErr := dockerfile.Close() if dockerfileCloseErr != nil { log.Error("Unexpected error closing file %s: %s", dockerfilePath, dockerfileCloseErr) } }(dockerfile) parsedASTResult, ASTParseErr := buildkitParser.Parse(dockerfile) if ASTParseErr != nil { log.Fatalf("Unexpected error parsing Dockerfile %s: %s", dockerfilePath, ASTParseErr) return parser.ParseResult{}, ASTParseErr } parseResult, parseErr := parseDockerfileAST(parsedASTResult.AST) if parseErr != nil { log.Fatalf("Unexpected error while parsing AST for %s: %s", dockerfilePath, parseErr) return parser.ParseResult{}, parseErr } return parseResult, nil } // parseDockerfileAST Read the parsed AST and extract the required information from each stage func parseDockerfileAST(dockerfileAST *buildkitParser.Node) (parser.ParseResult, error) { dockerfileStages, _, stageParsingErr := buildkitInstructions.Parse(dockerfileAST) if stageParsingErr != nil { log.Fatalf("Unexpected error while parsing Dockerfile Instructions: %s", stageParsingErr) return parser.ParseResult{}, stageParsingErr } var stages []*parser.Stage for stageIndex, stage := range dockerfileStages { stage, stageErr := parseDockerfileStage(stage) if stageErr != nil { log.Fatal("Unexpected error while parsing stage %d", stageIndex) return parser.ParseResult{}, stageErr } stages = append(stages, &stage) } return parser.ParseResult{Stages: stages}, nil } func parseDockerfileStage(dockerfileStage buildkitInstructions.Stage) (parser.Stage, error) { imageParts := strings.Split(dockerfileStage.BaseName, ":") if len(imageParts) < 2 { message := fmt.Sprintf("Not enough information in image (%s) to determine base", imageParts) log.Fatal(message) return parser.Stage{}, errors.New(message) } name := dockerfileStage.Name image := parser.Image{ Name: imageParts[0], Tag: imageParts[1], } commandParser, commandParserRetrievalErr := parser.GetCommandParser(image) if commandParserRetrievalErr != nil { log.Fatalf("Unexpected error while determining command parser for image \"%s\": %s", image, commandParserRetrievalErr) return parser.Stage{}, commandParserRetrievalErr } repositories, repositoryCommandParseErr := parseRepositoriesFromDockerfileStage(dockerfileStage, commandParser) if repositoryCommandParseErr != nil { message := fmt.Sprintf("Unexpected error while parsing repositories from stage: %s", repositoryCommandParseErr) log.Fatal(message) return parser.Stage{}, errors.New(message) } packages, packageCommandParseErr := parsePackagesFromDockerfileStage(dockerfileStage, commandParser) if packageCommandParseErr != nil { message := fmt.Sprintf("Unexpected error while parsing packages from stage: %s", packageCommandParseErr) log.Fatal(message) return parser.Stage{}, errors.New(message) } return parser.Stage{ Name: name, Image: &image, Repositories: repositories, Packages: packages, }, nil } func parseRepositoriesFromDockerfileStage(dockerfileStage buildkitInstructions.Stage, commandParser *parser.CommandParser) ([]*parser.Repository, error) { var repositories []*parser.Repository typedCommandParser := *commandParser for _, command := range dockerfileStage.Commands { switch command.(type) { case *buildkitInstructions.RunCommand: runCommand := command.(*buildkitInstructions.RunCommand) log.Tracef("Parsing RunCommand \"%s\" for repositories", runCommand.CmdLine) parsedRepositories, repositoryParseErr := typedCommandParser.GetRepositories(runCommand.CmdLine[0]) if repositoryParseErr != nil { log.Fatalf("Unexpected error while parsing repositories: %s", repositoryParseErr) return nil, repositoryParseErr } for _, parsedRepository := range parsedRepositories { repositories = append(repositories, parsedRepository) } } } return repositories, nil } func parsePackagesFromDockerfileStage(dockerfileStage buildkitInstructions.Stage, commandParser *parser.CommandParser) ([]*parser.Package, error) { var packages []*parser.Package typedCommandParser := *commandParser for _, command := range dockerfileStage.Commands { switch command.(type) { case *buildkitInstructions.RunCommand: runCommand := command.(*buildkitInstructions.RunCommand) log.Tracef("Parsing RunCommand \"%s\" for packages", runCommand.CmdLine) parsedPinnedPackages, packageParseErr := typedCommandParser.GetPinnedPackages(runCommand.CmdLine[0]) if packageParseErr != nil { log.Fatalf("Unexpected error while parsing pinned packages: %s", packageParseErr) return nil, packageParseErr } for _, parsedPinnedPackage := range parsedPinnedPackages { packages = append(packages, parsedPinnedPackage) } } } return packages, nil }