Skip to content

Commit

Permalink
feat: add info command (#101)
Browse files Browse the repository at this point in the history
Add new info command which shows detailed information about packages and slices.

Co-authored-by: Alberto Carretero
  • Loading branch information
rebornplusplus authored Sep 27, 2024
1 parent ac90003 commit 247bdb9
Show file tree
Hide file tree
Showing 6 changed files with 682 additions and 21 deletions.
2 changes: 1 addition & 1 deletion cmd/chisel/cmd_help.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ type helpCategory struct {
var helpCategories = []helpCategory{{
Label: "Basic",
Description: "general operations",
Commands: []string{"find", "help", "version"},
Commands: []string{"find", "info", "help", "version"},
}, {
Label: "Action",
Description: "make things happen",
Expand Down
137 changes: 137 additions & 0 deletions cmd/chisel/cmd_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package main

import (
"fmt"
"strconv"
"strings"

"github.com/jessevdk/go-flags"
"gopkg.in/yaml.v3"

"github.com/canonical/chisel/internal/setup"
)

var shortInfoHelp = "Show information about package slices"
var longInfoHelp = `
The info command shows detailed information about package slices.
It accepts a whitespace-separated list of strings. The list can be
composed of package names, slice names, or a combination of both. The
default output format is YAML. When multiple arguments are provided,
the output is a list of YAML documents separated by a "---" line.
Slice definitions are shown verbatim according to their definition in
the selected release. For example, globs are not expanded.
`

var infoDescs = map[string]string{
"release": "Chisel release name or directory (e.g. ubuntu-22.04)",
}

type infoCmd struct {
Release string `long:"release" value-name:"<branch|dir>"`

Positional struct {
Queries []string `positional-arg-name:"<pkg|slice>" required:"yes"`
} `positional-args:"yes"`
}

func init() {
addCommand("info", shortInfoHelp, longInfoHelp, func() flags.Commander { return &infoCmd{} }, infoDescs, nil)
}

func (cmd *infoCmd) Execute(args []string) error {
if len(args) > 0 {
return ErrExtraArgs
}

release, err := obtainRelease(cmd.Release)
if err != nil {
return err
}

packages, notFound := selectPackageSlices(release, cmd.Positional.Queries)

for i, pkg := range packages {
data, err := yaml.Marshal(pkg)
if err != nil {
return err
}
if i > 0 {
fmt.Fprintln(Stdout, "---")
}
fmt.Fprint(Stdout, string(data))
}

if len(notFound) > 0 {
for i := range notFound {
notFound[i] = strconv.Quote(notFound[i])
}
return fmt.Errorf("no slice definitions found for: " + strings.Join(notFound, ", "))
}

return nil
}

// selectPackageSlices takes in a release and a list of query strings
// of package names and/or slice names, and returns a list of packages
// containing the found slices. It also returns a list of query
// strings that were not found.
func selectPackageSlices(release *setup.Release, queries []string) (packages []*setup.Package, notFound []string) {
var pkgOrder []string
pkgSlices := make(map[string][]string)
allPkgSlices := make(map[string]bool)

sliceExists := func(key setup.SliceKey) bool {
pkg, ok := release.Packages[key.Package]
if !ok {
return false
}
_, ok = pkg.Slices[key.Slice]
return ok
}
for _, query := range queries {
var pkg, slice string
if strings.Contains(query, "_") {
key, err := setup.ParseSliceKey(query)
if err != nil || !sliceExists(key) {
notFound = append(notFound, query)
continue
}
pkg, slice = key.Package, key.Slice
} else {
if _, ok := release.Packages[query]; !ok {
notFound = append(notFound, query)
continue
}
pkg = query
}
if len(pkgSlices[pkg]) == 0 && !allPkgSlices[pkg] {
pkgOrder = append(pkgOrder, pkg)
}
if slice == "" {
allPkgSlices[pkg] = true
} else {
pkgSlices[pkg] = append(pkgSlices[pkg], slice)
}
}

for _, pkgName := range pkgOrder {
var pkg *setup.Package
if allPkgSlices[pkgName] {
pkg = release.Packages[pkgName]
} else {
releasePkg := release.Packages[pkgName]
pkg = &setup.Package{
Name: releasePkg.Name,
Archive: releasePkg.Archive,
Slices: make(map[string]*setup.Slice),
}
for _, sliceName := range pkgSlices[pkgName] {
pkg.Slices[sliceName] = releasePkg.Slices[sliceName]
}
}
packages = append(packages, pkg)
}
return packages, notFound
}
234 changes: 234 additions & 0 deletions cmd/chisel/cmd_info_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package main_test

import (
"os"
"path/filepath"
"strings"

. "gopkg.in/check.v1"

chisel "github.com/canonical/chisel/cmd/chisel"
"github.com/canonical/chisel/internal/testutil"
)

type infoTest struct {
summary string
input map[string]string
query []string
err string
stdout string
}

var infoTests = []infoTest{{
summary: "A single slice inspection",
input: infoRelease,
query: []string{"mypkg1_myslice1"},
stdout: `
package: mypkg1
archive: ubuntu
slices:
myslice1:
contents:
/dir/file: {}
`,
}, {
summary: "A single package inspection",
input: infoRelease,
query: []string{"mypkg2"},
stdout: `
package: mypkg2
archive: ubuntu
slices:
myslice:
contents:
/dir/another-file: {}
`,
}, {
summary: "Multiple slices within the same package",
input: infoRelease,
query: []string{"mypkg1_myslice2", "mypkg1_myslice1"},
stdout: `
package: mypkg1
archive: ubuntu
slices:
myslice1:
contents:
/dir/file: {}
myslice2:
essential:
- mypkg1_myslice1
- mypkg2_myslice
`,
}, {
summary: "Packages and slices",
input: infoRelease,
query: []string{"mypkg1_myslice1", "mypkg2", "mypkg1_myslice2"},
stdout: `
package: mypkg1
archive: ubuntu
slices:
myslice1:
contents:
/dir/file: {}
myslice2:
essential:
- mypkg1_myslice1
- mypkg2_myslice
---
package: mypkg2
archive: ubuntu
slices:
myslice:
contents:
/dir/another-file: {}
`,
}, {
summary: "Package and its slices",
input: infoRelease,
query: []string{"mypkg1_myslice1", "mypkg1"},
stdout: `
package: mypkg1
archive: ubuntu
slices:
myslice1:
contents:
/dir/file: {}
myslice2:
essential:
- mypkg1_myslice1
- mypkg2_myslice
`,
}, {
summary: "Same slice appearing multiple times",
input: infoRelease,
query: []string{"mypkg1_myslice1", "mypkg1_myslice1", "mypkg1_myslice1"},
stdout: `
package: mypkg1
archive: ubuntu
slices:
myslice1:
contents:
/dir/file: {}
`,
}, {
summary: "No slices found",
input: infoRelease,
query: []string{"foo", "bar_foo"},
err: `no slice definitions found for: "foo", "bar_foo"`,
}, {
summary: "Some slices found, others not found",
input: infoRelease,
query: []string{"foo", "mypkg1_myslice1", "bar_foo"},
stdout: `
package: mypkg1
archive: ubuntu
slices:
myslice1:
contents:
/dir/file: {}
/dir/sub-dir/: {make: true, mode: 0644}
`,
err: `no slice definitions found for: "foo", "bar_foo"`,
}, {
summary: "No args",
input: infoRelease,
err: "the required argument `<pkg|slice> (at least 1 argument)` was not provided",
}, {
summary: "Empty, whitespace args",
input: infoRelease,
query: []string{"", " "},
err: `no slice definitions found for: "", " "`,
}, {
summary: "Ignore invalid slice names",
input: infoRelease,
query: []string{"foo_bar_foo", "a_b", "7_c", "a_b c", "a_b x_y"},
err: `no slice definitions found for: "foo_bar_foo", "a_b", "7_c", "a_b c", "a_b x_y"`,
}}

var testKey = testutil.PGPKeys["key1"]

var defaultChiselYaml = `
format: chisel-v1
archives:
ubuntu:
version: 22.04
components: [main, universe]
v1-public-keys: [test-key]
v1-public-keys:
test-key:
id: ` + testKey.ID + `
armor: |` + "\n" + testutil.PrefixEachLine(testKey.PubKeyArmor, "\t\t\t\t\t\t")

var infoRelease = map[string]string{
"chisel.yaml": string(defaultChiselYaml),
"slices/mypkg1.yaml": `
package: mypkg1
essential:
- mypkg1_myslice1
slices:
myslice1:
contents:
/dir/file:
myslice2:
essential:
- mypkg2_myslice
`,
"slices/mypkg2.yaml": `
package: mypkg2
slices:
myslice:
contents:
/dir/another-file:
`,
"slices/mypkg3.yaml": `
package: mypkg3
essential:
- mypkg1_myslice1
slices:
myslice:
essential:
- mypkg2_myslice
contents:
/dir/other-file:
/dir/glob*:
/dir/sub-dir/: {make: true, mode: 0644}
/dir/copy: {copy: /dir/file}
/dir/symlink: {symlink: /dir/file}
/dir/mutable: {text: TODO, mutable: true, arch: riscv64}
/dir/arch-specific*: {arch: [amd64,arm64,i386]}
/dir/until: {until: mutate}
/dir/unfolded:
copy: /dir/file
mode: 0644
mutate: |
# Test multi-line string.
content.write("/dir/mutable", foo)
`,
}

func (s *ChiselSuite) TestInfoCommand(c *C) {
for _, test := range infoTests {
c.Logf("Summary: %s", test.summary)

s.ResetStdStreams()

dir := c.MkDir()
for path, data := range test.input {
fpath := filepath.Join(dir, path)
err := os.MkdirAll(filepath.Dir(fpath), 0755)
c.Assert(err, IsNil)
err = os.WriteFile(fpath, testutil.Reindent(data), 0644)
c.Assert(err, IsNil)
}
test.query = append([]string{"info", "--release", dir}, test.query...)

_, err := chisel.Parser().ParseArgs(test.query)
if test.err != "" {
c.Assert(err, ErrorMatches, test.err)
continue
}
c.Assert(err, IsNil)
test.stdout = string(testutil.Reindent(test.stdout))
c.Assert(s.Stdout(), Equals, strings.TrimSpace(test.stdout)+"\n")
}
}
Loading

0 comments on commit 247bdb9

Please sign in to comment.