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.

181 lines
5.7 KiB

  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. buildkitDockerfileInstructions "github.com/moby/buildkit/frontend/dockerfile/instructions"
  22. buildkitDockerfileParser "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, err := os.Open(dockerfilePath)
  28. if err != nil {
  29. log.Fatalf("Unexpected error reading file %s: %s", dockerfilePath, err)
  30. return parser.ParseResult{}, err
  31. }
  32. defer func(dockerfile *os.File) {
  33. err := dockerfile.Close()
  34. if err != nil {
  35. log.Error("Unexpected error closing file %s: %s", dockerfilePath, err)
  36. }
  37. }(dockerfile)
  38. dockerfileParseResults, err := buildkitDockerfileParser.Parse(dockerfile)
  39. if err != nil {
  40. log.Fatalf("Unexpected error parsing Dockerfile %s: %s", dockerfilePath, err)
  41. return parser.ParseResult{}, err
  42. }
  43. dockerfileParseResult, err := parseDockerfileAST(dockerfileParseResults.AST)
  44. if err != nil {
  45. log.Fatalf("Unexpected error while parsing AST for %s: %s", dockerfilePath, err)
  46. return parser.ParseResult{}, err
  47. }
  48. return dockerfileParseResult, nil
  49. }
  50. type ASTStage struct {
  51. children []*buildkitDockerfileParser.Node
  52. }
  53. // parseDockerfileAST Read the parsed AST and extract the required information from each stage
  54. func parseDockerfileAST(dockerfileAST *buildkitDockerfileParser.Node) (parser.ParseResult, error) {
  55. dockerfileStages, _, err := buildkitDockerfileInstructions.Parse(dockerfileAST)
  56. if err != nil {
  57. log.Fatal("Unexpected error while parsing Dockerfile Instructions")
  58. return parser.ParseResult{}, err
  59. }
  60. var stages []*parser.Stage
  61. for stageIndex, stage := range dockerfileStages {
  62. stage, err := parseDockerfileStage(stage)
  63. if err != nil {
  64. log.Fatal("Unexpected error while parsing stage %d", stageIndex)
  65. return parser.ParseResult{}, err
  66. }
  67. stages = append(stages, &stage)
  68. }
  69. return parser.ParseResult{Stages: stages}, nil
  70. }
  71. func parseDockerfileStage(dockerfileStage buildkitDockerfileInstructions.Stage) (parser.Stage, error) {
  72. imageParts := strings.Split(dockerfileStage.BaseName, ":")
  73. if len(imageParts) < 2 {
  74. message := fmt.Sprintf("Not enough information in image (%s) to determine base", imageParts)
  75. log.Fatal(message)
  76. return parser.Stage{}, errors.New(message)
  77. }
  78. name := dockerfileStage.Name
  79. image := parser.Image{
  80. Name: imageParts[0],
  81. Tag: imageParts[1],
  82. }
  83. commandParser, err := parser.GetCommandParser(image)
  84. if err != nil {
  85. log.Fatalf("Unexpected error while determining command parser for image %s", image)
  86. return parser.Stage{}, err
  87. }
  88. repositories, err := parseRepositoriesFromDockerfileStage(dockerfileStage, commandParser)
  89. if err != nil {
  90. message := fmt.Sprintf("Unexpected error while parsing repositories from stage")
  91. log.Fatal(message)
  92. return parser.Stage{}, errors.New(message)
  93. }
  94. packages, err := parsePackagesFromDockerfileStage(dockerfileStage, commandParser)
  95. if err != nil {
  96. message := fmt.Sprintf("Unexpected error while parsing packages from stage")
  97. log.Fatal(message)
  98. return parser.Stage{}, errors.New(message)
  99. }
  100. return parser.Stage{
  101. Name: name,
  102. Image: &image,
  103. Repositories: repositories,
  104. Packages: packages,
  105. }, nil
  106. }
  107. func parseRepositoriesFromDockerfileStage(dockerfileStage buildkitDockerfileInstructions.Stage, commandParser *parser.CommandParser) ([]*parser.Repository, error) {
  108. var repositories []*parser.Repository
  109. typedCommandParser := *commandParser
  110. for _, command := range dockerfileStage.Commands {
  111. switch command.(type) {
  112. case *buildkitDockerfileInstructions.RunCommand:
  113. runCommand := command.(*buildkitDockerfileInstructions.RunCommand)
  114. log.Tracef("Parsing RunCommand \"%s\" for repositories", runCommand.CmdLine)
  115. parsedRepositories, err := typedCommandParser.GetRepositories(runCommand.CmdLine[0])
  116. if err != nil {
  117. log.Fatalf("Unexpected error while parsing repositories: %s", err)
  118. return nil, err
  119. }
  120. for _, parsedRepository := range parsedRepositories {
  121. repositories = append(repositories, parsedRepository)
  122. }
  123. }
  124. }
  125. return repositories, nil
  126. }
  127. func parsePackagesFromDockerfileStage(dockerfileStage buildkitDockerfileInstructions.Stage, commandParser *parser.CommandParser) ([]*parser.Package, error) {
  128. var packages []*parser.Package
  129. typedCommandParser := *commandParser
  130. for _, command := range dockerfileStage.Commands {
  131. switch command.(type) {
  132. case *buildkitDockerfileInstructions.RunCommand:
  133. runCommand := command.(*buildkitDockerfileInstructions.RunCommand)
  134. log.Tracef("Parsing RunCommand \"%s\" for packages", runCommand.CmdLine)
  135. parsedPinnedPackages, err := typedCommandParser.GetPinnedPackages(runCommand.CmdLine[0])
  136. if err != nil {
  137. log.Fatalf("Unexpected error while parsing pinned packages: %s", err)
  138. return nil, err
  139. }
  140. for _, parsedPinnedPackage := range parsedPinnedPackages {
  141. packages = append(packages, parsedPinnedPackage)
  142. }
  143. }
  144. }
  145. return packages, nil
  146. }