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.

177 lines
6.1 KiB

3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
  1. /*
  2. Copyright © 2021 Drew Short <warrick@sothr.com>
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. // Package dockerfile implements the required components for working with dockerfile image definitions
  14. package dockerfile
  15. import (
  16. "errors"
  17. "fmt"
  18. "os"
  19. "strings"
  20. log "github.com/sirupsen/logrus"
  21. buildkitInstructions "github.com/moby/buildkit/frontend/dockerfile/instructions"
  22. buildkitParser "github.com/moby/buildkit/frontend/dockerfile/parser"
  23. "sothr.com/warricksothr/pinned-package-updater/internal/parser"
  24. )
  25. // Parse reads a Dockerfile, interprets the AST and returns information about the image(s) and package(s) defined
  26. func Parse(dockerfilePath string) (parser.ParseResult, error) {
  27. dockerfile, dockerfileOpenErr := os.Open(dockerfilePath)
  28. if dockerfileOpenErr != nil {
  29. log.Fatalf("Unexpected error reading file %s: %s", dockerfilePath, dockerfileOpenErr)
  30. return parser.ParseResult{}, dockerfileOpenErr
  31. }
  32. defer func(dockerfile *os.File) {
  33. dockerfileCloseErr := dockerfile.Close()
  34. if dockerfileCloseErr != nil {
  35. log.Error("Unexpected error closing file %s: %s", dockerfilePath, dockerfileCloseErr)
  36. }
  37. }(dockerfile)
  38. parsedASTResult, ASTParseErr := buildkitParser.Parse(dockerfile)
  39. if ASTParseErr != nil {
  40. log.Fatalf("Unexpected error parsing Dockerfile %s: %s", dockerfilePath, ASTParseErr)
  41. return parser.ParseResult{}, ASTParseErr
  42. }
  43. parseResult, parseErr := parseDockerfileAST(parsedASTResult.AST)
  44. if parseErr != nil {
  45. log.Fatalf("Unexpected error while parsing AST for %s: %s", dockerfilePath, parseErr)
  46. return parser.ParseResult{}, parseErr
  47. }
  48. return parseResult, nil
  49. }
  50. // parseDockerfileAST Read the parsed AST and extract the required information from each stage
  51. func parseDockerfileAST(dockerfileAST *buildkitParser.Node) (parser.ParseResult, error) {
  52. dockerfileStages, _, stageParsingErr := buildkitInstructions.Parse(dockerfileAST)
  53. if stageParsingErr != nil {
  54. log.Fatalf("Unexpected error while parsing Dockerfile Instructions: %s", stageParsingErr)
  55. return parser.ParseResult{}, stageParsingErr
  56. }
  57. var stages []*parser.Stage
  58. for stageIndex, stage := range dockerfileStages {
  59. stage, stageErr := parseDockerfileStage(stage)
  60. if stageErr != nil {
  61. log.Fatal("Unexpected error while parsing stage %d", stageIndex)
  62. return parser.ParseResult{}, stageErr
  63. }
  64. stages = append(stages, &stage)
  65. }
  66. return parser.ParseResult{Stages: stages}, nil
  67. }
  68. func parseDockerfileStage(dockerfileStage buildkitInstructions.Stage) (parser.Stage, error) {
  69. imageParts := strings.Split(dockerfileStage.BaseName, ":")
  70. if len(imageParts) < 2 {
  71. message := fmt.Sprintf("Not enough information in image (%s) to determine base", imageParts)
  72. log.Fatal(message)
  73. return parser.Stage{}, errors.New(message)
  74. }
  75. name := dockerfileStage.Name
  76. image := parser.Image{
  77. Name: imageParts[0],
  78. Tag: imageParts[1],
  79. }
  80. commandParser, commandParserRetrievalErr := parser.GetCommandParser(image)
  81. if commandParserRetrievalErr != nil {
  82. log.Fatalf("Unexpected error while determining command parser for image \"%s\": %s", image, commandParserRetrievalErr)
  83. return parser.Stage{}, commandParserRetrievalErr
  84. }
  85. repositories, repositoryCommandParseErr := parseRepositoriesFromDockerfileStage(dockerfileStage, commandParser)
  86. if repositoryCommandParseErr != nil {
  87. message := fmt.Sprintf("Unexpected error while parsing repositories from stage: %s", repositoryCommandParseErr)
  88. log.Fatal(message)
  89. return parser.Stage{}, errors.New(message)
  90. }
  91. packages, packageCommandParseErr := parsePackagesFromDockerfileStage(dockerfileStage, commandParser)
  92. if packageCommandParseErr != nil {
  93. message := fmt.Sprintf("Unexpected error while parsing packages from stage: %s", packageCommandParseErr)
  94. log.Fatal(message)
  95. return parser.Stage{}, errors.New(message)
  96. }
  97. return parser.Stage{
  98. Name: name,
  99. Image: &image,
  100. Repositories: repositories,
  101. Packages: packages,
  102. }, nil
  103. }
  104. func parseRepositoriesFromDockerfileStage(dockerfileStage buildkitInstructions.Stage, commandParser *parser.CommandParser) ([]*parser.Repository, error) {
  105. var repositories []*parser.Repository
  106. typedCommandParser := *commandParser
  107. for _, command := range dockerfileStage.Commands {
  108. switch command.(type) {
  109. case *buildkitInstructions.RunCommand:
  110. runCommand := command.(*buildkitInstructions.RunCommand)
  111. log.Tracef("Parsing RunCommand \"%s\" for repositories", runCommand.CmdLine)
  112. parsedRepositories, repositoryParseErr := typedCommandParser.GetRepositories(runCommand.CmdLine[0])
  113. if repositoryParseErr != nil {
  114. log.Fatalf("Unexpected error while parsing repositories: %s", repositoryParseErr)
  115. return nil, repositoryParseErr
  116. }
  117. for _, parsedRepository := range parsedRepositories {
  118. repositories = append(repositories, parsedRepository)
  119. }
  120. }
  121. }
  122. return repositories, nil
  123. }
  124. func parsePackagesFromDockerfileStage(dockerfileStage buildkitInstructions.Stage, commandParser *parser.CommandParser) ([]*parser.Package, error) {
  125. var packages []*parser.Package
  126. typedCommandParser := *commandParser
  127. for _, command := range dockerfileStage.Commands {
  128. switch command.(type) {
  129. case *buildkitInstructions.RunCommand:
  130. runCommand := command.(*buildkitInstructions.RunCommand)
  131. log.Tracef("Parsing RunCommand \"%s\" for packages", runCommand.CmdLine)
  132. parsedPinnedPackages, packageParseErr := typedCommandParser.GetPinnedPackages(runCommand.CmdLine[0])
  133. if packageParseErr != nil {
  134. log.Fatalf("Unexpected error while parsing pinned packages: %s", packageParseErr)
  135. return nil, packageParseErr
  136. }
  137. for _, parsedPinnedPackage := range parsedPinnedPackages {
  138. packages = append(packages, parsedPinnedPackage)
  139. }
  140. }
  141. }
  142. return packages, nil
  143. }