Skip to content

Commit

Permalink
cmd: add new describe-image command
Browse files Browse the repository at this point in the history
This commit adds a new `describe-image` comamnd that contains
the details about the given image type. The output is yaml as
it is both nicely human readable and also machine readable.

Note that this version carries an invalid yaml header on
purpose to avoid people replying on the feature for scripts
before it is stable.

The output looks like this:
```yaml
$ ./image-builder describe-image rhel-9.1 tar
@warning - the output format is not stable yet and may change
distro: rhel-9.1
type: tar
arch: x86_64
os_vesion: "9.1"
bootmode: none
partition_type: ""
default_filename: root.tar.xz
packages:
  include:
    - policycoreutils
    - selinux-policy-targeted
    - selinux-policy-targeted
  exclude:
    - rng-tools
```

Thanks to Ondrej Budai for the idea and the example.
  • Loading branch information
mvo5 committed Jan 28, 2025
1 parent 98e4beb commit 7625143
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 3 deletions.
104 changes: 104 additions & 0 deletions cmd/image-builder/describeimg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package main

import (
"fmt"
"io"
"slices"

"gopkg.in/yaml.v3"

"github.com/osbuild/images/pkg/blueprint"
"github.com/osbuild/images/pkg/distro"
"github.com/osbuild/images/pkg/imagefilter"
)

// Use yaml output by default because it is both nicely human and
// machine readable and parts of our image defintions will be written
// in yaml too. This means this should be a possible input a
// "flattended" image definiton.
type describeImgYAML struct {
Distro string `yaml:"distro"`
Type string `yaml:"type"`
Arch string `yaml:"arch"`

// XXX: think about ordering (as this is what the user will see)
OsVersion string `yaml:"os_vesion"`

Bootmode string `yaml:"bootmode"`
PartitionType string `yaml:"partition_type"`
DefaultFilename string `yaml:"default_filename"`

// XXX: add pipelines here? maybe at least exports?
Packages *packagesYAML `yaml:"packages"`
}

type packagesYAML struct {
Include []string `yaml:"include"`
Exclude []string `yaml:"exclude"`
}

func packageSetsFor(imgType distro.ImageType) (inc, exc []string, err error) {
var bp blueprint.Blueprint
manifest, _, err := imgType.Manifest(&bp, distro.ImageOptions{}, nil, nil)
if err != nil {
return nil, nil, err
}

incM := map[string]bool{}
excM := map[string]bool{}
// XXX: or should this just do what osbuild-package-sets does
// and inlcude what pipeline needs the package set too?
for pipelineName, pkgSets := range manifest.GetPackageSetChains() {
// XXX: or shouldn't we exclude the build pipeline here?
if pipelineName == "build" {
continue
}
for _, pkgSet := range pkgSets {
for _, s := range pkgSet.Include {
incM[s] = true
}
for _, s := range pkgSet.Exclude {
excM[s] = true
}
}
}
for name := range incM {
inc = append(inc, name)
}
for name := range excM {
exc = append(exc, name)
}
slices.Sort(inc)
slices.Sort(exc)
return inc, exc, nil
}

// XXX: should this live in images instead?
func describeImage(img *imagefilter.Result, out io.Writer) error {
// see
// https://github.com/osbuild/images/pull/1019#discussion_r1832376568
// for what is available on an image (without depsolve or partitioning)
inc, exc, err := packageSetsFor(img.ImgType)
if err != nil {
return err
}

outYaml := &describeImgYAML{
Distro: img.Distro.Name(),
OsVersion: img.Distro.OsVersion(),
Arch: img.Arch.Name(),
Type: img.ImgType.Name(),
Bootmode: img.ImgType.BootMode().String(),
PartitionType: img.ImgType.PartitionType().String(),
DefaultFilename: img.ImgType.Filename(),
Packages: &packagesYAML{
Include: inc,
Exclude: exc,
},
}
// deliberately break the yaml until the feature is stable
fmt.Fprint(out, "@WARNING - the output format is not stable yet and may change\n")
enc := yaml.NewEncoder(out)
enc.SetIndent(2)
return enc.Encode(outYaml)
}
41 changes: 41 additions & 0 deletions cmd/image-builder/describeimg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package main_test

import (
"bytes"
"testing"

"github.com/stretchr/testify/assert"

testrepos "github.com/osbuild/images/test/data/repositories"

"github.com/osbuild/image-builder-cli/cmd/image-builder"
)

func TestDescribeImage(t *testing.T) {
restore := main.MockNewRepoRegistry(testrepos.New)
defer restore()

res, err := main.GetOneImage("", "centos-9", "tar", "x86_64")
assert.NoError(t, err)

var buf bytes.Buffer
err = main.DescribeImage(res, &buf)
assert.NoError(t, err)

expectedOutput := `@WARNING - the output format is not stable yet and may change
distro: centos-9
type: tar
arch: x86_64
os_vesion: 9-stream
bootmode: none
partition_type: ""
default_filename: root.tar.xz
packages:
include:
- policycoreutils
- selinux-policy-targeted
exclude:
- rng-tools
`
assert.Equal(t, expectedOutput, buf.String())
}
7 changes: 4 additions & 3 deletions cmd/image-builder/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import (
)

var (
GetOneImage = getOneImage
Run = run
FindDistro = findDistro
GetOneImage = getOneImage
Run = run
FindDistro = findDistro
DescribeImage = describeImage
)

func MockOsArgs(new []string) (restore func()) {
Expand Down
40 changes: 40 additions & 0 deletions cmd/image-builder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,32 @@ func cmdBuild(cmd *cobra.Command, args []string) error {
return buildImage(res, mf.Bytes(), buildOpts)
}

func cmdDescribeImg(cmd *cobra.Command, args []string) error {
// XXX: boilderplate identical to cmdManifest() above
dataDir, err := cmd.Flags().GetString("datadir")
if err != nil {
return err
}
distroStr, err := cmd.Flags().GetString("distro")
if err != nil {
return err
}
archStr, err := cmd.Flags().GetString("arch")
if err != nil {
return err
}
if archStr == "" {
archStr = arch.Current().String()
}
imgTypeStr := args[0]
res, err := getOneImage(dataDir, distroStr, imgTypeStr, archStr)
if err != nil {
return err
}

return describeImage(res, osStdout)
}

func run() error {
// images logs a bunch of stuff to Debug/Info that is distracting
// the user (at least by default, like what repos being loaded)
Expand Down Expand Up @@ -246,6 +272,20 @@ operating systems like Fedora, CentOS and RHEL with easy customizations support.
buildCmd.Flags().String("cache", "/var/cache/image-builder/store", `osbuild directory to cache intermediate build artifacts"`)
rootCmd.AddCommand(buildCmd)

// XXX: add --format=json too?
describeImgCmd := &cobra.Command{
Use: "describe-image <distro> <image-type>",
Short: "describe the given distro/image-type, e.g. centos-9 qcow2",
RunE: cmdDescribeImg,
SilenceUsage: true,
Args: cobra.ExactArgs(1),
Hidden: true,
}
describeImgCmd.Flags().String("arch", "", `use the different architecture`)
describeImgCmd.Flags().String("distro", "", `build manifest for a different distroname (e.g. centos-9)`)

rootCmd.AddCommand(describeImgCmd)

return rootCmd.Execute()
}

Expand Down
23 changes: 23 additions & 0 deletions cmd/image-builder/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,3 +510,26 @@ func TestManifestIntegrationWithSBOMWithOutputDir(t *testing.T) {
assert.Equal(t, filepath.Join(outputDir, "centos-9-qcow2-x86_64.buildroot-build.spdx.json"), sboms[0])
assert.Equal(t, filepath.Join(outputDir, "centos-9-qcow2-x86_64.image-os.spdx.json"), sboms[1])
}

func TestDescribeImageSmoke(t *testing.T) {
restore := main.MockNewRepoRegistry(testrepos.New)
defer restore()

restore = main.MockOsArgs([]string{
"describe-image",
"qcow2",
"--distro=centos-9",
})
defer restore()

var fakeStdout bytes.Buffer
restore = main.MockOsStdout(&fakeStdout)
defer restore()

err := main.Run()
assert.NoError(t, err)

assert.Contains(t, fakeStdout.String(), `distro: centos-9
type: qcow2
arch: x86_64`)
}

0 comments on commit 7625143

Please sign in to comment.