diff --git a/cmd/check.go b/cmd/check.go index 5b15544..cf57dc9 100644 --- a/cmd/check.go +++ b/cmd/check.go @@ -17,12 +17,14 @@ limitations under the License. package cmd import ( - log "github.com/sirupsen/logrus" "os" "path" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "sothr.com/warricksothr/pinned-package-updater/internal/parser" "sothr.com/warricksothr/pinned-package-updater/internal/parser/dockerfile" ) @@ -69,6 +71,14 @@ pinned-package-updater --remote
check [Dockerf func init() { rootCmd.AddCommand(checkCmd) + // TODO: Add dynamic loading of maps between images and command parsers + + err := parser.InitCommandParsers() + if err != nil { + log.Errorf("Unexpected error while ") + return + } + // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command diff --git a/go.mod b/go.mod index 23ca992..409d246 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/spf13/cobra v1.2.1 github.com/spf13/viper v1.8.1 github.com/thediveo/enumflag v0.10.1 + mvdan.cc/sh/v3 v3.4.0 ) require ( @@ -30,7 +31,7 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.2.0 // indirect - golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect + golang.org/x/sys v0.0.0-20210925032602-92d5a993a665 // indirect golang.org/x/text v0.3.5 // indirect google.golang.org/protobuf v1.26.0 // indirect gopkg.in/ini.v1 v1.62.0 // indirect diff --git a/go.sum b/go.sum index dbf1502..e27d858 100644 --- a/go.sum +++ b/go.sum @@ -338,6 +338,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= @@ -414,6 +415,8 @@ github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/frankban/quicktest v1.13.1 h1:xVm/f9seEhZFL9+n5kv5XLrGwy6elc4V9v/XFY2vmd8= +github.com/frankban/quicktest v1.13.1/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= @@ -585,6 +588,7 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk= github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= @@ -726,6 +730,8 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= @@ -890,6 +896,7 @@ github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -947,6 +954,9 @@ github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451 h1:d1PiN4RxzIFXCJTvRkvSkKqwtRAl5ZV4lATKtQI0B7I= +github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= @@ -1391,10 +1401,13 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210925032602-92d5a993a665 h1:QOQNt6vCjMpXE7JSK5VvAzJC1byuN3FgTNSBwf+CJgI= +golang.org/x/sys v0.0.0-20210925032602-92d5a993a665/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210916214954-140adaaadfaf/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1744,8 +1757,11 @@ modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= +mvdan.cc/editorconfig v0.2.0/go.mod h1:lvnnD3BNdBYkhq+B4uBuFFKatfp02eB6HixDvEz91C0= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= +mvdan.cc/sh/v3 v3.4.0 h1:thPCJ0hffn/Y/vMGs3HVMPYStNTyr2+lQee8LQiPZSU= +mvdan.cc/sh/v3 v3.4.0/go.mod h1:p/tqPPI4Epfk2rICAe2RoaNd8HBSJ8t9Y2DA9yQlbzY= mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZIVpwbkw+04kSxk3rAtzlimaUJw= mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7/go.mod h1:HGC5lll35J70Y5v7vCGb9oLhHoScFwkHDJm/05RdSTc= pack.ag/amqp v0.11.2/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= diff --git a/internal/parser/debian.go b/internal/parser/debian.go new file mode 100644 index 0000000..a5ece33 --- /dev/null +++ b/internal/parser/debian.go @@ -0,0 +1,201 @@ +/* +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 parser + +import ( + "bytes" + "errors" + "strings" + + log "github.com/sirupsen/logrus" + + "mvdan.cc/sh/v3/syntax" +) + +// DebianCommandParser the parser responsible for handling Debian based images +type DebianCommandParser struct { + parser *syntax.Parser + printer *syntax.Printer +} + +// newDebianCommandParser Provision a new instance of DebianCommandParser with required components +func newDebianCommandParser() DebianCommandParser { + return DebianCommandParser{ + parser: syntax.NewParser(), + printer: syntax.NewPrinter(), + } +} + +// shellNodeToString Convert a node to the string representation +func (c DebianCommandParser) shellNodeToString(node syntax.Node) string { + buffer := bytes.NewBufferString("") + err := c.printer.Print(buffer, node) + if err != nil { + log.Panic("Failed to convert node to string") + } + return buffer.String() +} + +// isRedirectedToAptSourcesList Checks if the list of redirects contains the apt sources list directory +func isRedirectedToAptSourcesList(redirects []*syntax.Redirect) bool { + if redirects != nil && len(redirects) > 0 { + for _, redirect := range redirects { + if strings.Contains(redirect.Word.Lit(), "/etc/apt/sources.list") { + return true + } + } + } + return false +} + +// isAddRepositoryExpression Checks for expressions that add to the repository +func isAddRepositoryExpression(command syntax.CallExpr, redirects []*syntax.Redirect) bool { + // Adding via debian tooling + if command.Args[0].Lit() == "add-apt-repository" { + return true + // Echoing into a sources file directly + } else if command.Args[0].Lit() == "echo" && isRedirectedToAptSourcesList(redirects) { + return true + } + return false +} + +// trimQuotes return a string without surrounding quotes +func trimQuotes(value string) string { + if value[0] == '"' && value[len(value)-1] == '"' { + return value[1 : len(value)-1] + } else if value[0] == '\'' && value[len(value)-1] == '\'' { + return value[1 : len(value)-1] + } + return value +} + +// getAddedRepository parse the repository information from the shell commands +func (c DebianCommandParser) getAddedRepository(command syntax.CallExpr, redirects []*syntax.Redirect) (string, error) { + // Adding via debian tooling + if command.Args[0].Lit() == "add-apt-repository" { + return command.Args[len(command.Args)-1].Lit(), nil + // Echoing into a sources file directly + } else if command.Args[0].Lit() == "echo" && isRedirectedToAptSourcesList(redirects) { + for _, argument := range command.Args { + var argumentNode syntax.Node = argument + argumentValue := trimQuotes(c.shellNodeToString(argumentNode)) + if strings.Contains(argumentValue, "deb") || strings.Contains(argumentValue, "http://") { + return argumentValue, nil + } + } + } + return "", errors.New("failed to parse repository") +} + +// GetRepositories Returns a list of Repository entries that were identified +func (c DebianCommandParser) GetRepositories(command string) ([]*Repository, error) { + var repositories []*Repository + + reader := strings.NewReader(command) + parsedScript, err := c.parser.Parse(reader, "") + if err != nil { + log.Fatalf("Failed to parse script \"%s\"", command) + return nil, err + } + + syntax.Walk(parsedScript, func(node syntax.Node) bool { + switch parsedStatement := node.(type) { + case *syntax.Stmt: + switch parsedCommand := parsedStatement.Cmd.(type) { + case *syntax.CallExpr: + if isAddRepositoryExpression(*parsedCommand, parsedStatement.Redirs) { + repositoryInformation, err := c.getAddedRepository(*parsedCommand, parsedStatement.Redirs) + if err != nil { + log.Fatalf("Unexpected error while parsing repository from \"%s\": %s", c.shellNodeToString(node), err) + } + repositories = append(repositories, &Repository{Information: repositoryInformation}) + log.Debugf("Found repository \"%s\"", repositoryInformation) + } + } + } + return true + }) + + return repositories, nil +} + +// isAddRepositoryExpression Checks for expressions that add to the repository +func isInstallerExpression(command syntax.CallExpr) bool { + // Adding via debian tooling + commandProgram := command.Args[0].Lit() + if (commandProgram == "apt-get" || commandProgram == "apt") && len(command.Args) > 1 && command.Args[1].Lit() == "install" { + return true + } + return false +} + +func (c DebianCommandParser) getInstalledPinnedPackages(command syntax.CallExpr) ([]*Package, error) { + var packages []*Package + + // Adding via debian tooling + commandProgram := command.Args[0].Lit() + if (commandProgram == "apt-get" || commandProgram == "apt") && len(command.Args) > 1 && command.Args[1].Lit() == "install" { + for _, argument := range command.Args { + var argumentNode syntax.Node = argument + argumentValue := trimQuotes(c.shellNodeToString(argumentNode)) + if strings.Contains(argumentValue, "=") { + pinnedPackageParts := strings.SplitN(argumentValue, "=", 2) + packages = append(packages, &Package{ + Name: pinnedPackageParts[0], + Version: pinnedPackageParts[1], + }) + } + } + return packages, nil + } + return packages, errors.New("failed to parse packages") +} + +// GetPinnedPackages get a list of all pinned packages +func (c DebianCommandParser) GetPinnedPackages(command string) ([]*Package, error) { + var packages []*Package + + reader := strings.NewReader(command) + parsedScript, err := c.parser.Parse(reader, "") + if err != nil { + log.Fatalf("Failed to parse script \"%s\"", command) + return nil, err + } + + syntax.Walk(parsedScript, func(node syntax.Node) bool { + switch parsedStatement := node.(type) { + case *syntax.Stmt: + switch parsedCommand := parsedStatement.Cmd.(type) { + case *syntax.CallExpr: + if isInstallerExpression(*parsedCommand) { + pinnedPackages, err := c.getInstalledPinnedPackages(*parsedCommand) + if err != nil { + log.Fatalf("Unexpected error while parsing pinned packages from \"%s\": %s", c.shellNodeToString(node), err) + } + for _, pinnedPackage := range pinnedPackages { + packages = append(packages, pinnedPackage) + log.Debugf("Found pinned package \"%s=%s\"", pinnedPackage.Name, pinnedPackage.Version) + } + } + } + } + return true + }) + + return packages, nil +} diff --git a/internal/parser/dockerfile/dockerfile.go b/internal/parser/dockerfile/dockerfile.go index 82652f8..455aece 100644 --- a/internal/parser/dockerfile/dockerfile.go +++ b/internal/parser/dockerfile/dockerfile.go @@ -102,14 +102,20 @@ func parseDockerfileStage(dockerfileStage buildkitDockerfileInstructions.Stage) Tag: imageParts[1], } - repositories, err := parseRepositoriesFromDockerfileStage(dockerfileStage) + commandParser, err := parser.GetCommandParser(image) + if err != nil { + log.Fatalf("Unexpected error while determining command parser for image %s", image) + return parser.Stage{}, err + } + + repositories, err := parseRepositoriesFromDockerfileStage(dockerfileStage, commandParser) if err != nil { message := fmt.Sprintf("Unexpected error while parsing repositories from stage") log.Fatal(message) return parser.Stage{}, errors.New(message) } - packages, err := parsePackagesFromDockerfileStage(dockerfileStage) + packages, err := parsePackagesFromDockerfileStage(dockerfileStage, commandParser) if err != nil { message := fmt.Sprintf("Unexpected error while parsing packages from stage") log.Fatal(message) @@ -124,12 +130,44 @@ func parseDockerfileStage(dockerfileStage buildkitDockerfileInstructions.Stage) }, nil } -func parsePackagesFromDockerfileStage(_ buildkitDockerfileInstructions.Stage) ([]*parser.Package, error) { - var packages []*parser.Package - return packages, nil -} - -func parseRepositoriesFromDockerfileStage(_ buildkitDockerfileInstructions.Stage) ([]*parser.Repository, error) { +func parseRepositoriesFromDockerfileStage(dockerfileStage buildkitDockerfileInstructions.Stage, commandParser *parser.CommandParser) ([]*parser.Repository, error) { var repositories []*parser.Repository + typedCommandParser := *commandParser + + for _, command := range dockerfileStage.Commands { + switch command.(type) { + case *buildkitDockerfileInstructions.RunCommand: + runCommand := command.(*buildkitDockerfileInstructions.RunCommand) + + log.Tracef("Parsing RunCommand \"%s\" for repositories", runCommand.CmdLine) + + _, err := typedCommandParser.GetRepositories(runCommand.CmdLine[0]) + if err != nil { + return nil, err + } + } + } + return repositories, nil } + +func parsePackagesFromDockerfileStage(dockerfileStage buildkitDockerfileInstructions.Stage, commandParser *parser.CommandParser) ([]*parser.Package, error) { + var packages []*parser.Package + typedCommandParser := *commandParser + + for _, command := range dockerfileStage.Commands { + switch command.(type) { + case *buildkitDockerfileInstructions.RunCommand: + runCommand := command.(*buildkitDockerfileInstructions.RunCommand) + + log.Tracef("Parsing RunCommand \"%s\" for packages", runCommand.CmdLine) + + _, err := typedCommandParser.GetPinnedPackages(runCommand.CmdLine[0]) + if err != nil { + return nil, err + } + } + } + + return packages, nil +} diff --git a/internal/parser/parser.go b/internal/parser/parser.go index b0ffc29..f9c78e0 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -17,6 +17,13 @@ limitations under the License. // Package parser implements the required components for working with image definitions package parser +import ( + "errors" + "fmt" + + log "github.com/sirupsen/logrus" +) + // ParseResult is the standard representation for am image declaration type ParseResult struct { Stages []*Stage @@ -44,5 +51,66 @@ type Package struct { // Repository is the representation of an added repository defined in a stage declaration type Repository struct { - Path string + Information string +} + +// CommandParserType represents the recognized type of parser usable for a specific image +type CommandParserType int + +const ( + CommandParserTypeDebian CommandParserType = iota +) + +// CommandParserTypeIds is a lookup map between CommandParserType and a representation string +var commandParserTypeIds = map[CommandParserType][]string{ + CommandParserTypeDebian: {"debian"}, +} + +func lookupCommandParserTypeName(parserType CommandParserType) string { + if parserTypeName, ok := commandParserTypeIds[parserType]; ok { + return parserTypeName[0] + } else { + return "UNKNOWN" + } +} + +var availableCommandParsers = map[CommandParserType]CommandParser{} + +// CommandParser handles parsing commands from RUN arguments dependent on the base image type +type CommandParser interface { + GetRepositories(runCommand string) ([]*Repository, error) + GetPinnedPackages(runCommand string) ([]*Package, error) +} + +// InitCommandParsers exposes functionality to configure the behavior of mapping images to a CommandParser +func InitCommandParsers() error { + + // Registering known parsers + availableCommandParsers[CommandParserTypeDebian] = newDebianCommandParser() + + return nil +} + +func GetCommandParser(image Image) (*CommandParser, error) { + parserType, err := determineParserForImage(image) + if err != nil { + log.Fatalf("Failed to determine a matching CommandParser for %s: %s", image, err) + return nil, err + } + + if requestedCommandParser, ok := availableCommandParsers[parserType]; ok { + return &requestedCommandParser, nil + } else { + message := fmt.Sprintf("Unable to identify parser for %s", lookupCommandParserTypeName(parserType)) + log.Fatal(message) + return nil, errors.New(message) + } +} + +// determineParserForImage matches image information to the correct CommandParserType +func determineParserForImage(_ Image) (CommandParserType, error) { + + // TODO: Map image information to the list of command parsers supported + + return CommandParserTypeDebian, nil } diff --git a/test/data/old_pinned_dependencies_ubuntu.Dockerfile b/test/data/old_pinned_dependencies_ubuntu.Dockerfile index 23abb03..51f62d9 100644 --- a/test/data/old_pinned_dependencies_ubuntu.Dockerfile +++ b/test/data/old_pinned_dependencies_ubuntu.Dockerfile @@ -1,7 +1,8 @@ FROM ubuntu:18.04 -RUN apt-get update && apt-get install \ +RUN echo "deb http://archive.canonical.com/ubuntu bionic partner" > /etc/apt/sources.list.d/partner.list \ + && apt-get update && apt-get install \ ca-certificates=20210119~18.04.1 \ curl=7.58.0-2ubuntu3.15 \ && apt-get clean \ - && rm -rf /var/lib/apt/lists/* \ No newline at end of file + && rm -rf /var/lib/apt/lists/* \ \ No newline at end of file