Skip to content

Commit

Permalink
Rewrite "bashbrew children" and "bashbrew parents"
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
tianon committed Nov 14, 2022
1 parent 8281cbe commit 1d99523
Show file tree
Hide file tree
Showing 10 changed files with 407 additions and 973 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v3
with:
go-version: '>=1.18'
- name: Build
run: |
./bashbrew.sh --version > /dev/null
Expand Down
176 changes: 176 additions & 0 deletions cmd/bashbrew/cmd-children.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package main

import (
"fmt"
"os"
"path"
"strings"

"github.com/urfave/cli"
)

func cmdChildren(c *cli.Context) error {
// 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)
args := c.Args()
if len(args) < 1 {
return fmt.Errorf(`need at least one argument`)
}

allRepos, err := repos(true)
if err != nil {
return cli.NewMultiError(fmt.Errorf(`failed gathering ALL repos list`), err)
}

applyConstraints := c.Bool("apply-constraints")
archFilter := c.Bool("arch-filter")

// build up a list of canonical tag mappings and canonical tag architectures
canonical := map[string]string{}
arches := dedupeSliceMap[string, string]{}
for _, repo := range allRepos {
r, err := fetch(repo)
if err != nil {
return cli.NewMultiError(fmt.Errorf(`failed fetching repo %q`, repo), err)
}

for _, entry := range r.Entries() {
if applyConstraints && r.SkipConstraints(entry) {
continue
}
if archFilter && !entry.HasArchitecture(arch) {
continue
}

tags := r.Tags(namespace, false, entry)
for _, tag := range tags {
canonical[tag] = tags[0]
}

entryArches := []string{arch}
if !applyConstraints && !archFilter {
entryArches = entry.Architectures
}
for _, entryArch := range entryArches {
arches.add(tags[0], entryArch)
}
}
}

// 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)
// 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)
children := dedupeSliceMap[string, string]{}
repoTags := dedupeSliceMap[string, string]{}
for _, repo := range allRepos {
r, err := fetch(repo)
if err != nil {
return cli.NewMultiError(fmt.Errorf(`failed fetching repo %q`, repo), err)
}

nsRepo := path.Join(namespace, r.RepoName)

for _, entry := range r.Entries() {
if applyConstraints && r.SkipConstraints(entry) {
continue
}
if archFilter && !entry.HasArchitecture(arch) {
continue
}

entryArches := []string{arch}
if !applyConstraints && !archFilter {
entryArches = entry.Architectures
}

tag := nsRepo + ":" + entry.Tags[0]
repoTags.add(nsRepo, tag)

for _, entryArch := range entryArches {
froms, err := r.ArchDockerFroms(entryArch, entry)
if err != nil {
return cli.NewMultiError(fmt.Errorf(`failed fetching/scraping FROM for %q (tags %q, arch %q)`, r.RepoName, entry.TagsString(), entryArch), err)
}

for _, from := range froms {
if canon, ok := canonical[from]; ok {
from = canon
} else {
// must be unsupported, let's make sure our current implied supported architecture value for it is recorded!
arches.add(from, entryArch)
}
children.add(from, tag)
if fromRepo, _, ok := strings.Cut(from, ":"); ok {
// make sure things like old "alpine" tags that are no longer supported still come up with "bashbrew children alpine"
repoTags.add(fromRepo, from)
}
}
}
}
}

uniq := c.Bool("uniq")
depth := c.Int("depth")

// used in conjunction with "uniq" to make sure we print a given tag once and only once when enabled
seen := map[string]struct{}{}

for _, arg := range args {
var tags []string
if children.has(arg) {
// if the string has children, let's walk them verbatim
tags = []string{arg}
} else if tag, ok := canonical[arg]; ok {
// if the string has a "canonical" tag (meaning is a supported tag), let's use it verbatim (whether it has children or not)
tags = []string{tag}
} else if nsArg := path.Join(namespace, arg); repoTags.has(nsArg) {
// otherwise, let's do a couple lookups based on the provided argument being a repository like "alpine"
tags = repoTags.slice(nsArg)
} else if repoTags.has(arg) {
tags = repoTags.slice(arg)
}
if len(tags) < 1 {
return fmt.Errorf(`failed to resolve argument as repo or tag %q`, arg)
}

for _, tag := range tags {
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)
if debugFlag {
fmt.Fprintf(os.Stderr, "DEBUG: relevant architectures of %q: %s\n", tag, strings.Join(supportedArches, ", "))
}
if depth == -1 {
// special value to let "bashbrew children mcr.microsoft.com/windows/servercore" print the list of FROM values in use for a repo
fmt.Println(tag)
continue
}
lookup := []string{tag}
for d := depth; len(lookup) > 0 && (depth == 0 || d > 0); d-- {
nextLookup := []string{}
for _, tag := range lookup {
kids := children.slice(tag)
for _, kid := range kids {
supported := false
for _, arch := range arches.slice(kid) {
if sliceHas[string](supportedArches, arch) {
supported = true
break
}
}
if !supported {
continue
}
nextLookup = append(nextLookup, kid)
if uniq {
if _, ok := seen[kid]; ok {
continue
}
seen[kid] = struct{}{}
}
fmt.Println(kid)
}
}
lookup = nextLookup
}
}
}

return nil
}
177 changes: 0 additions & 177 deletions cmd/bashbrew/cmd-deps.go

This file was deleted.

Loading

0 comments on commit 1d99523

Please sign in to comment.