Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add basic ignore directive, allowing the user to tell anchor to ignore certain packages #38

Merged
merged 6 commits into from
Jul 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got GPT-4o to "refactor" this and what it claims (I don't know about readability compared to what you have though)—I'll leave it up to you:

Changes Made:

1.	Replaced strings.TrimLeft with strings.TrimSpace and strings.TrimPrefix for clarity and correctness.
2.	Used strings.Fields to split the command into parts more cleanly.
3.	Simplified the condition checks to enhance readability.
4.	Removed redundant TrimSpace calls in certain places where they were unnecessary.
func parseComment(entry Entry) ([]string, bool) {
	ignoredPackages := []string{}

	if entry.Type != EntryComment {
		return ignoredPackages, false
	}

	command := strings.TrimSpace(strings.TrimPrefix(entry.Value, "#"))
	parts := strings.Fields(command)

	if len(parts) < 2 || parts[0] != "anchor" {
		return ignoredPackages, false
	}

	keyValue := strings.SplitN(parts[1], "=", 2)

	if len(keyValue) < 2 {
		if strings.TrimSpace(keyValue[0]) == "ignore" {
			return ignoredPackages, true
		}
		return ignoredPackages, false
	}

	if keyValue[0] != "ignore" {
		return ignoredPackages, false
	}

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

	return ignoredPackages, false
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out this was just wrong lol but thanks chatgpt

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 {
BradLewis marked this conversation as resolved.
Show resolved Hide resolved
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
t.Errorf("Expected no error but got %v", err)
t.Errorf("Expected no error but got %w", err)

}
if image != "golang:1.22-bookworm" {
t.Errorf("Expected golang:1.22-bookworm but got %v", image)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this be a %s, since image is a sting?

Suggested change
t.Errorf("Expected golang:1.22-bookworm but got %v", image)
t.Errorf("Expected golang:1.22-bookworm but got %s", 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
Loading