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

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. "os"
  18. "strings"
  19. log "github.com/sirupsen/logrus"
  20. buildkitInstructions "github.com/moby/buildkit/frontend/dockerfile/instructions"
  21. buildkitParser "github.com/moby/buildkit/frontend/dockerfile/parser"
  22. "sothr.com/warricksothr/pinned-package-updater/internal/parser"
  23. )
  24. // Parse reads a Dockerfile, interprets the AST and returns information about the image(s) and package(s) defined
  25. func Parse(dockerfilePath string) (parser.ParseResult, error) {
  26. parseLogger := log.WithFields(log.Fields{
  27. "filePath": dockerfilePath,
  28. })
  29. dockerfile, err := os.Open(dockerfilePath)
  30. if err != nil {
  31. log.WithFields(log.Fields{
  32. "sourceError": err,
  33. }).Fatal("unexpected error reading file")
  34. return parser.ParseResult{}, err
  35. }
  36. defer func(dockerfile *os.File) {
  37. err := dockerfile.Close()
  38. if err != nil {
  39. parseLogger.WithFields(log.Fields{
  40. "sourceError": err,
  41. }).Error("unexpected error closing file")
  42. }
  43. }(dockerfile)
  44. parsedASTResult, err := buildkitParser.Parse(dockerfile)
  45. if err != nil {
  46. parseLogger.WithFields(log.Fields{
  47. "sourceError": err,
  48. }).Fatal("unexpected error parsing Dockerfile")
  49. return parser.ParseResult{}, err
  50. }
  51. parseResult, err := parseDockerfileAST(parseLogger, dockerfilePath, parsedASTResult.AST)
  52. if err != nil {
  53. parseLogger.WithFields(log.Fields{
  54. "sourceError": err,
  55. }).Fatal("unexpected error while parsing AST")
  56. return parser.ParseResult{}, err
  57. }
  58. return parseResult, nil
  59. }
  60. /*
  61. General Helpers
  62. */
  63. // getCommandLineLocation parse commandLineLocation information into the structure we will persist
  64. func getContentLocation(location []buildkitParser.Range) (parser.ContentLocation, error) {
  65. commandLocationStart := location[0].Start
  66. commandLocationEnd := location[len(location)-1].End
  67. return parser.ContentLocation{
  68. StartLine: commandLocationStart.Line,
  69. StartCharacter: commandLocationStart.Character,
  70. EndLine: commandLocationEnd.Line,
  71. EndCharacter: commandLocationEnd.Character,
  72. }, nil
  73. }
  74. /*
  75. Parsing Helper
  76. */
  77. // parseDockerfileAST Read the parsed AST and extract the required information from each stage
  78. func parseDockerfileAST(logger *log.Entry, dockerFilePath string, dockerfileAST *buildkitParser.Node) (parser.ParseResult, error) {
  79. dockerfileStages, _, err := buildkitInstructions.Parse(dockerfileAST)
  80. if err != nil {
  81. logger.WithFields(log.Fields{
  82. "sourceError": err,
  83. }).Fatal("unexpected error while parsing Dockerfile instructions")
  84. return parser.ParseResult{}, err
  85. }
  86. var stages []*parser.Stage
  87. for stageIndex, stage := range dockerfileStages {
  88. stageLogger := logger.WithFields(log.Fields{
  89. "stageIndex": stageIndex,
  90. "stageName": stage.Name,
  91. })
  92. stage, err := parseDockerfileStage(stageLogger, stageIndex, stage)
  93. if err != nil {
  94. stageLogger.WithFields(log.Fields{
  95. "sourceError": err,
  96. }).Fatal("unexpected error while parsing stage")
  97. return parser.ParseResult{}, err
  98. }
  99. stages = append(stages, &stage)
  100. }
  101. return parser.ParseResult{FilePath: dockerFilePath, Stages: stages}, nil
  102. }
  103. func parseDockerfileStage(logger *log.Entry, dockerfileStageIndex int, dockerfileStage buildkitInstructions.Stage) (parser.Stage, error) {
  104. stageLocation, err := getContentLocation(dockerfileStage.Location)
  105. if err != nil {
  106. logger.WithFields(log.Fields{
  107. "rawStageLocation": dockerfileStage.Location,
  108. }).Fatal("unexpected failure parsing stage location information")
  109. return parser.Stage{}, err
  110. }
  111. stageLogger := logger.WithFields(log.Fields{
  112. "stageImage": dockerfileStage.BaseName,
  113. "stageLocation": stageLocation,
  114. })
  115. imageParts := strings.Split(dockerfileStage.BaseName, ":")
  116. if len(imageParts) < 2 {
  117. stageLogger.Fatal("not enough information in image to determine base")
  118. return parser.Stage{}, errors.New("not enough information in image to determine base")
  119. }
  120. name := dockerfileStage.Name
  121. image := parser.Image{
  122. Name: imageParts[0],
  123. Tag: imageParts[1],
  124. }
  125. commandParser, err := parser.GetCommandParser(image)
  126. if err != nil {
  127. stageLogger.WithFields(log.Fields{
  128. "sourceError": err,
  129. }).Fatal("unexpected error while determining command parser for image")
  130. return parser.Stage{}, err
  131. }
  132. repositories, repositoryCommandLocations, err := parseRepositoriesFromDockerfileStage(stageLogger, dockerfileStage, commandParser)
  133. if err != nil {
  134. stageLogger.WithFields(log.Fields{
  135. "sourceError": err,
  136. }).Fatal("unexpected error while parsing repositories from stage")
  137. return parser.Stage{}, err
  138. }
  139. packages, packageCommandLocations, err := parsePackagesFromDockerfileStage(stageLogger, dockerfileStage, commandParser)
  140. if err != nil {
  141. stageLogger.WithFields(log.Fields{
  142. "sourceError": err,
  143. }).Fatal("unexpected error while parsing packages from stage")
  144. return parser.Stage{}, err
  145. }
  146. var commandLocations []*parser.ContentLocation
  147. commandLocations = append(commandLocations, repositoryCommandLocations...)
  148. commandLocations = append(commandLocations, packageCommandLocations...)
  149. return parser.Stage{
  150. Index: dockerfileStageIndex,
  151. Name: name,
  152. Image: &image,
  153. StageLocation: stageLocation,
  154. Repositories: repositories,
  155. Packages: packages,
  156. CommandLocations: commandLocations,
  157. }, nil
  158. }
  159. func parseRepositoriesFromDockerfileStage(logger *log.Entry, dockerfileStage buildkitInstructions.Stage, commandParser *parser.CommandParser) ([]*parser.Repository, []*parser.ContentLocation, error) {
  160. var repositories []*parser.Repository
  161. var commandLocations []*parser.ContentLocation
  162. typedCommandParser := *commandParser
  163. for _, command := range dockerfileStage.Commands {
  164. switch typedCommand := command.(type) {
  165. case *buildkitInstructions.RunCommand:
  166. commandLocation, err := getContentLocation(typedCommand.Location())
  167. if err != nil {
  168. logger.WithFields(log.Fields{
  169. "rawCommandLineLocation": typedCommand.Location(),
  170. }).Fatal("unexpected failure parsing script location information")
  171. return repositories, commandLocations, err
  172. }
  173. commandLocations = append(commandLocations, &commandLocation)
  174. for _, line := range typedCommand.CmdLine {
  175. logger.WithFields(log.Fields{
  176. "commandLine": line,
  177. "commandLocation": commandLocation,
  178. }).Trace("parsing RunCommand for repositories")
  179. parsedRepositories, err := typedCommandParser.GetRepositories(logger, line)
  180. if err != nil {
  181. logger.WithFields(log.Fields{
  182. "commandLine": line,
  183. "commandLocation": commandLocation,
  184. "sourceError": err,
  185. }).Fatal("unexpected error while parsing repositories")
  186. return repositories, commandLocations, err
  187. }
  188. for _, parsedRepository := range parsedRepositories {
  189. repositories = append(repositories, parsedRepository)
  190. }
  191. }
  192. }
  193. }
  194. return repositories, commandLocations, nil
  195. }
  196. func parsePackagesFromDockerfileStage(logger *log.Entry, dockerfileStage buildkitInstructions.Stage, commandParser *parser.CommandParser) ([]*parser.Package, []*parser.ContentLocation, error) {
  197. var packages []*parser.Package
  198. var commandLocations []*parser.ContentLocation
  199. typedCommandParser := *commandParser
  200. for _, command := range dockerfileStage.Commands {
  201. switch typedCommand := command.(type) {
  202. case *buildkitInstructions.RunCommand:
  203. commandLocation, err := getContentLocation(typedCommand.Location())
  204. if err != nil {
  205. logger.WithFields(log.Fields{
  206. "rawCommandLineLocation": typedCommand.Location(),
  207. }).Fatal("unexpected failure parsing script location information")
  208. return packages, commandLocations, err
  209. }
  210. commandLocations = append(commandLocations, &commandLocation)
  211. for _, line := range typedCommand.CmdLine {
  212. logger.WithFields(log.Fields{
  213. "commandLine": line,
  214. "commandLocation": commandLocation,
  215. }).Trace("parsing RunCommand for packages")
  216. parsedPinnedPackages, err := typedCommandParser.GetPinnedPackages(logger, line)
  217. if err != nil {
  218. logger.WithFields(log.Fields{
  219. "commandLine": line,
  220. "commandLocation": commandLocation,
  221. "sourceError": err,
  222. }).Fatal("unexpected error while parsing pinned packages")
  223. return packages, commandLocations, err
  224. }
  225. for _, parsedPinnedPackage := range parsedPinnedPackages {
  226. packages = append(packages, parsedPinnedPackage)
  227. }
  228. }
  229. }
  230. }
  231. return packages, commandLocations, nil
  232. }