Skip to content

Commit 1d99523

Browse files
committed
Rewrite "bashbrew children" and "bashbrew parents"
This time, they are distinct implementations because the problem they are solving is inherently different. For listing children of a given name, we *have* to walk the entire library (since we only have tag -> FROM mappings, not the reverse, which is fundamentally the question that "children" answers). On the flip side, listing the parents of a given name is as straightforward as looking up the FROM values and walking until we can't anymore. In my own testing, these new implementations are significantly more correct, and handle more edge cases (including things we couldn't support before like `bashbrew children --depth=1 scratch`, `bashbrew children mcr.microsoft.com/windows/servercore`, etc). They also more correctly handle edge cases like tags that are `FROM` a "`SharedTag`" such that they don't walk up/down both sides of the tree (for example, `orientdb:3.2` -> `FROM eclipse-temurin:8-jdk`, which is both Windows *and* Linux, even though `orientdb:3.2` is Linux-only).
1 parent 8281cbe commit 1d99523

File tree

10 files changed

+407
-973
lines changed

10 files changed

+407
-973
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ jobs:
1616
runs-on: ubuntu-latest
1717
steps:
1818
- uses: actions/checkout@v2
19+
- uses: actions/setup-go@v3
20+
with:
21+
go-version: '>=1.18'
1922
- name: Build
2023
run: |
2124
./bashbrew.sh --version > /dev/null

cmd/bashbrew/cmd-children.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path"
7+
"strings"
8+
9+
"github.com/urfave/cli"
10+
)
11+
12+
func cmdChildren(c *cli.Context) error {
13+
// we don't need this until later, but we want to bail early if we don't have it (before we've done a lot of work creating the graph)
14+
args := c.Args()
15+
if len(args) < 1 {
16+
return fmt.Errorf(`need at least one argument`)
17+
}
18+
19+
allRepos, err := repos(true)
20+
if err != nil {
21+
return cli.NewMultiError(fmt.Errorf(`failed gathering ALL repos list`), err)
22+
}
23+
24+
applyConstraints := c.Bool("apply-constraints")
25+
archFilter := c.Bool("arch-filter")
26+
27+
// build up a list of canonical tag mappings and canonical tag architectures
28+
canonical := map[string]string{}
29+
arches := dedupeSliceMap[string, string]{}
30+
for _, repo := range allRepos {
31+
r, err := fetch(repo)
32+
if err != nil {
33+
return cli.NewMultiError(fmt.Errorf(`failed fetching repo %q`, repo), err)
34+
}
35+
36+
for _, entry := range r.Entries() {
37+
if applyConstraints && r.SkipConstraints(entry) {
38+
continue
39+
}
40+
if archFilter && !entry.HasArchitecture(arch) {
41+
continue
42+
}
43+
44+
tags := r.Tags(namespace, false, entry)
45+
for _, tag := range tags {
46+
canonical[tag] = tags[0]
47+
}
48+
49+
entryArches := []string{arch}
50+
if !applyConstraints && !archFilter {
51+
entryArches = entry.Architectures
52+
}
53+
for _, entryArch := range entryArches {
54+
arches.add(tags[0], entryArch)
55+
}
56+
}
57+
}
58+
59+
// now build up a map of FROM -> canonical tag references and a "repo -> tags" lookup (including things like "alpine:3.11" that are no longer supported)
60+
// for non-canonical/unsupported tags, auto-create/supplement their "arches" list from the thing that's FROM them (so we can filter properly later and make sure "bashbrew children mcr.microsoft.com/windows/servercore" doesn't list non-Windows images that happen to be "FROM xyz-shared-tag" that includes Windows)
61+
children := dedupeSliceMap[string, string]{}
62+
repoTags := dedupeSliceMap[string, string]{}
63+
for _, repo := range allRepos {
64+
r, err := fetch(repo)
65+
if err != nil {
66+
return cli.NewMultiError(fmt.Errorf(`failed fetching repo %q`, repo), err)
67+
}
68+
69+
nsRepo := path.Join(namespace, r.RepoName)
70+
71+
for _, entry := range r.Entries() {
72+
if applyConstraints && r.SkipConstraints(entry) {
73+
continue
74+
}
75+
if archFilter && !entry.HasArchitecture(arch) {
76+
continue
77+
}
78+
79+
entryArches := []string{arch}
80+
if !applyConstraints && !archFilter {
81+
entryArches = entry.Architectures
82+
}
83+
84+
tag := nsRepo + ":" + entry.Tags[0]
85+
repoTags.add(nsRepo, tag)
86+
87+
for _, entryArch := range entryArches {
88+
froms, err := r.ArchDockerFroms(entryArch, entry)
89+
if err != nil {
90+
return cli.NewMultiError(fmt.Errorf(`failed fetching/scraping FROM for %q (tags %q, arch %q)`, r.RepoName, entry.TagsString(), entryArch), err)
91+
}
92+
93+
for _, from := range froms {
94+
if canon, ok := canonical[from]; ok {
95+
from = canon
96+
} else {
97+
// must be unsupported, let's make sure our current implied supported architecture value for it is recorded!
98+
arches.add(from, entryArch)
99+
}
100+
children.add(from, tag)
101+
if fromRepo, _, ok := strings.Cut(from, ":"); ok {
102+
// make sure things like old "alpine" tags that are no longer supported still come up with "bashbrew children alpine"
103+
repoTags.add(fromRepo, from)
104+
}
105+
}
106+
}
107+
}
108+
}
109+
110+
uniq := c.Bool("uniq")
111+
depth := c.Int("depth")
112+
113+
// used in conjunction with "uniq" to make sure we print a given tag once and only once when enabled
114+
seen := map[string]struct{}{}
115+
116+
for _, arg := range args {
117+
var tags []string
118+
if children.has(arg) {
119+
// if the string has children, let's walk them verbatim
120+
tags = []string{arg}
121+
} else if tag, ok := canonical[arg]; ok {
122+
// if the string has a "canonical" tag (meaning is a supported tag), let's use it verbatim (whether it has children or not)
123+
tags = []string{tag}
124+
} else if nsArg := path.Join(namespace, arg); repoTags.has(nsArg) {
125+
// otherwise, let's do a couple lookups based on the provided argument being a repository like "alpine"
126+
tags = repoTags.slice(nsArg)
127+
} else if repoTags.has(arg) {
128+
tags = repoTags.slice(arg)
129+
}
130+
if len(tags) < 1 {
131+
return fmt.Errorf(`failed to resolve argument as repo or tag %q`, arg)
132+
}
133+
134+
for _, tag := range tags {
135+
supportedArches := arches.slice(tag) // this will already be filtered in terms of archFilter / applyConstraints and is pre-implied by the above code for non-supported images like Windows base images (used to filter the children to only those that have intersection to avoid "bashbrew from .../windows/servercore" from listing non-Windows images, for example)
136+
if debugFlag {
137+
fmt.Fprintf(os.Stderr, "DEBUG: relevant architectures of %q: %s\n", tag, strings.Join(supportedArches, ", "))
138+
}
139+
if depth == -1 {
140+
// special value to let "bashbrew children mcr.microsoft.com/windows/servercore" print the list of FROM values in use for a repo
141+
fmt.Println(tag)
142+
continue
143+
}
144+
lookup := []string{tag}
145+
for d := depth; len(lookup) > 0 && (depth == 0 || d > 0); d-- {
146+
nextLookup := []string{}
147+
for _, tag := range lookup {
148+
kids := children.slice(tag)
149+
for _, kid := range kids {
150+
supported := false
151+
for _, arch := range arches.slice(kid) {
152+
if sliceHas[string](supportedArches, arch) {
153+
supported = true
154+
break
155+
}
156+
}
157+
if !supported {
158+
continue
159+
}
160+
nextLookup = append(nextLookup, kid)
161+
if uniq {
162+
if _, ok := seen[kid]; ok {
163+
continue
164+
}
165+
seen[kid] = struct{}{}
166+
}
167+
fmt.Println(kid)
168+
}
169+
}
170+
lookup = nextLookup
171+
}
172+
}
173+
}
174+
175+
return nil
176+
}

cmd/bashbrew/cmd-deps.go

Lines changed: 0 additions & 177 deletions
This file was deleted.

0 commit comments

Comments
 (0)