Skip to content

Commit

Permalink
Merge pull request #38 from SongStitch/feat/add-ignore-directive
Browse files Browse the repository at this point in the history
feat: add basic ignore directive, allowing the user to tell anchor to ignore certain packages
  • Loading branch information
BradLewis authored Jul 27, 2024
2 parents a1b40af + 641c44e commit 2554f70
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 7 deletions.
1 change: 1 addition & 0 deletions Dockerfile.template
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Test Dockerfile for anchor
FROM golang:1.22-bookworm as builder

# anchor ignore=curl,wget
# hadolint ignore=DL3008
RUN apt-get update \
&& apt-get install --no-install-recommends -y curl wget \
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ A tool for anchoring dependencies in dockerfiles
- [Specifying Input and Output Files](#specifying-input-and-output-files)
- [Non-Interactive Mode (CI/CD Pipelines)](#non-interactive-mode-cicd-pipelines)
- [Printing the Output Instead of Writing to a File](#printing-the-output-instead-of-writing-to-a-file)
- [Ignoring Images and Packages](#ignoring-images-and-packages)
- [License](#license)

<!-- tocstop -->
Expand Down Expand Up @@ -140,6 +141,34 @@ You can print the output to stdout by using the `-p` flag.
anchor -i Dockerfile.template --dry-run
```

## Ignoring Images and Packages

It is possible to tell anchor to ignore images and packages in the Dockerfile statement by adding a `# anchor ignore` comment above the statement in the Dockerfile template. For example:

```dockerfile
# ignore this statement
# anchor ignore
FROM golang:1.22-bookworm as builder

# ignore this statement
# anchor ignore
RUN apt-get update \
&& apt-get install --no-install-recommends -y curl wget \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean

# explicitly tell anchor to ignore this image
# anchor ignore=golang:1.22-bookworm
FROM golang:1.22-bookworm

# explicitly tell anchor to ignore the curl package
# anchor ignore=curl
RUN apt-get update \
&& apt-get install --no-install-recommends -y curl wget \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
```

# License

This project is licensed under the GPL-2.0 License - see the [LICENSE](/LICENSE) file for details.
67 changes: 65 additions & 2 deletions pkg/anchor/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os/exec"
"slices"
"strings"

"github.com/fatih/color"
Expand All @@ -14,8 +15,15 @@ func processFromCommand(node *Node) (string, error) {
if node.CommandType != CommandFrom {
return "", fmt.Errorf("node is not a FROM command")
}
ignoredPackages := []string{}
ignoreAll := false
for i := range node.Entries {
entry := node.Entries[i]
if entry.Type == EntryComment {
var ignored []string
ignored, ignoreAll = parseComment(entry)
ignoredPackages = append(ignoredPackages, ignored...)
}
if entry.Type != EntryCommand {
continue
}
Expand All @@ -33,6 +41,10 @@ func processFromCommand(node *Node) (string, error) {

image := commandSplit[1]
image = strings.TrimSpace((image))
if slices.Contains(ignoredPackages, image) || ignoreAll {
return image, nil
}

digest, err := crane.Digest(image)
if err != nil {
return "", err
Expand Down Expand Up @@ -82,12 +94,55 @@ func processRunCommand(ctx context.Context, node *Node, architecture string, ima
return nil
}

func parseComment(entry Entry) ([]string, bool) {
ignoredPackages := []string{}
if entry.Type != EntryComment {
return ignoredPackages, false
}

command := strings.TrimLeft(entry.Value, "# ")
commands := strings.SplitN(command, " ", 2)
if len(commands) < 2 {
return ignoredPackages, false
}
if strings.TrimSpace(commands[0]) != "anchor" {
return ignoredPackages, false
}

next := strings.SplitN(commands[1], "=", 2)
if len(next) < 2 {
if strings.TrimSpace(next[0]) == "ignore" {
return ignoredPackages, true
}
return ignoredPackages, false
}

if strings.TrimSpace(next[0]) != "ignore" {
return ignoredPackages, false
}

packages := strings.Split(next[1], ",")
for _, pkg := range packages {
ignoredPackages = append(ignoredPackages, strings.TrimSpace(pkg))
}

return ignoredPackages, false
}

func appendPackageVersions(node *Node, packageMap map[string]string, architecture string) {
aptGet := false
install := false
dpkgSet := false
ignoredPackages := []string{}
for i := range node.Entries {
entry := node.Entries[i]
if entry.Type == EntryComment {
ignored, all := parseComment(entry)
if all {
return
}
ignoredPackages = append(ignoredPackages, ignored...)
}
if entry.Type != EntryCommand {
continue
}
Expand All @@ -102,8 +157,16 @@ func appendPackageVersions(node *Node, packageMap map[string]string, architectur

}
if aptGet && install {
if _, ok := packageMap[elements[j]]; ok {
elements[j] = fmt.Sprintf("%s=%s", elements[j], packageMap[elements[j]])
pkg := strings.TrimSpace(elements[j])
if _, ok := packageMap[pkg]; ok {
if !slices.Contains(ignoredPackages, pkg) {
fmt.Printf(
"\t⚓Anchored %s to %s\n",
pkg,
packageMap[elements[j]],
)
elements[j] = fmt.Sprintf("%s=%s", elements[j], packageMap[elements[j]])
}
}
}
if strings.TrimSpace(elements[j]) == "&&" {
Expand Down
125 changes: 125 additions & 0 deletions pkg/anchor/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,128 @@ RUN dpkg --add-architecture %s && apt-get update && apt-get update \
t.Errorf("Expected:\n%v\ngot:\n%v", expected, result)
}
}

func TestParseComment(t *testing.T) {
cases := []struct {
name string
entry Entry
expectedIgnored []string
expectedAll bool
}{
{
name: "simple comment",
entry: Entry{
Type: EntryComment,
Value: "# anchor ignore=curl,wget",
},
expectedIgnored: []string{"curl", "wget"},
expectedAll: false,
},
{
name: "poorly formatted comment",
entry: Entry{
Type: EntryComment,
Value: "#anchor ignore =curl, test,wget",
},
expectedIgnored: []string{"curl", "test", "wget"},
expectedAll: false,
},
{
name: "non anchor comment",
entry: Entry{
Type: EntryComment,
Value: "# hadolint ignore=DL3008",
},
expectedIgnored: []string{},
expectedAll: false,
},
{
name: "non anchor ignore comment",
entry: Entry{
Type: EntryComment,
Value: "# anchor is a tool for anchoring dependencies in dockerfiles",
},
expectedIgnored: []string{},
expectedAll: false,
},
{
name: "basic ignore all",
entry: Entry{
Type: EntryComment,
Value: "# anchor ignore",
},
expectedIgnored: []string{},
expectedAll: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
actual, all := parseComment(tc.entry)
if !reflect.DeepEqual(actual, tc.expectedIgnored) {
t.Errorf("Expected %v but got %v", tc.expectedIgnored, actual)
}
if all != tc.expectedAll {
t.Errorf("Expected %v but got %v", tc.expectedAll, all)
}
})
}
}

func TestAppendPackageVersionsWithIgnore(t *testing.T) {
file := `# hadolint ignore=DL3008
# anchor ignore=curl
RUN apt-get update \
&& apt-get install \
--no-install-recommends -y \
# We just need curl and wget
curl wget \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean`
input := strings.NewReader(file)
nodes := Parse(input)
architecture := "amd64"

packageMap := map[string]string{
"curl": "7.68.0",
"wget": "1.20.3",
}

expected := fmt.Sprintf(`# hadolint ignore=DL3008
# anchor ignore=curl
RUN dpkg --add-architecture %s && apt-get update && apt-get update \
&& apt-get install \
--no-install-recommends -y \
# We just need curl and wget
curl wget=%s \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
`, architecture, packageMap["wget"])

node := nodes[0]
appendPackageVersions(&node, packageMap, architecture)
nodes[0] = node

w := &strings.Builder{}
nodes.Write(w)
result := w.String()
if result != expected {
t.Errorf("Expected:\n%v\ngot:\n%v", expected, result)
}
}

func TestImageIgnore(t *testing.T) {
file := `# hadolint ignore=DL3008
# anchor ignore=golang:1.22-bookworm
FROM golang:1.22-bookworm as builder
`

input := strings.NewReader(file)
nodes := Parse(input)
image, err := processFromCommand(&nodes[0])
if err != nil {
t.Errorf("Expected no error but got %v", err)
}
if image != "golang:1.22-bookworm" {
t.Errorf("Expected golang:1.22-bookworm but got %v", image)
}
}
5 changes: 0 additions & 5 deletions pkg/anchor/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,6 @@ func parsePackageVersions(s string) (map[string]string, error) {
continue
}
versions[currentPackage] = strings.Split(line, ": ")[1]
fmt.Printf(
"\t⚓Anchored %s to %s\n",
currentPackage,
versions[currentPackage],
)
currentPackage = ""
}
}
Expand Down

0 comments on commit 2554f70

Please sign in to comment.