From 1664311ac1ee5b5108da71d9c13155f633a2822a Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 12 Dec 2024 10:27:01 +0100 Subject: [PATCH] cmd: add new `describe-image` command 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. The output looks like this: ```yaml $ ./image-builder describe-image rhel-9.1 tar 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. --- cmd/image-builder/describeimg.go | 89 +++++++++++++++++++++++++++ cmd/image-builder/describeimg_test.go | 41 ++++++++++++ cmd/image-builder/export_test.go | 7 ++- cmd/image-builder/main.go | 35 +++++++++++ cmd/image-builder/main_test.go | 22 +++++++ 5 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 cmd/image-builder/describeimg.go create mode 100644 cmd/image-builder/describeimg_test.go diff --git a/cmd/image-builder/describeimg.go b/cmd/image-builder/describeimg.go new file mode 100644 index 0000000..bc224e3 --- /dev/null +++ b/cmd/image-builder/describeimg.go @@ -0,0 +1,89 @@ +package main + +import ( + "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, 0) + if err != nil { + return nil, nil, err + } + + // 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 { + inc = append(inc, pkgSet.Include...) + exc = append(exc, pkgSet.Exclude...) + } + } + 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, + }, + } + enc := yaml.NewEncoder(out) + enc.SetIndent(2) + return enc.Encode(outYaml) +} diff --git a/cmd/image-builder/describeimg_test.go b/cmd/image-builder/describeimg_test.go new file mode 100644 index 0000000..2776cfd --- /dev/null +++ b/cmd/image-builder/describeimg_test.go @@ -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 := `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 + - selinux-policy-targeted + exclude: + - rng-tools +` + assert.Equal(t, expectedOutput, buf.String()) +} diff --git a/cmd/image-builder/export_test.go b/cmd/image-builder/export_test.go index 5b3005f..c8bc7e9 100644 --- a/cmd/image-builder/export_test.go +++ b/cmd/image-builder/export_test.go @@ -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()) { diff --git a/cmd/image-builder/main.go b/cmd/image-builder/main.go index 273c779..be82db6 100644 --- a/cmd/image-builder/main.go +++ b/cmd/image-builder/main.go @@ -108,6 +108,29 @@ func cmdBuild(cmd *cobra.Command, args []string) error { return buildImage(res, mf.Bytes(), storeDir) } +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 + } + archStr, err := cmd.Flags().GetString("arch") + if err != nil { + return err + } + if archStr == "" { + archStr = arch.Current().String() + } + distroStr := args[0] + imgTypeStr := args[1] + 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) @@ -161,6 +184,18 @@ operating sytsems like centos and RHEL with easy customizations support.`, buildCmd.Flags().String("store", ".store", `osbuild store directory to cache intermediata build artifacts"`) rootCmd.AddCommand(buildCmd) + // XXX: add --format=json too? + describeImgCmd := &cobra.Command{ + Use: "describe-image ", + Short: "describe the given distro/image-type, e.g. centos-9 qcow2", + RunE: cmdDescribeImg, + SilenceUsage: true, + Args: cobra.ExactArgs(2), + Hidden: true, + } + describeImgCmd.Flags().String("arch", "", `use the different architecture`) + rootCmd.AddCommand(describeImgCmd) + return rootCmd.Execute() } diff --git a/cmd/image-builder/main_test.go b/cmd/image-builder/main_test.go index a7eb06b..6c2552a 100644 --- a/cmd/image-builder/main_test.go +++ b/cmd/image-builder/main_test.go @@ -317,3 +317,25 @@ exit 1 // osbuild-exec.go) assert.Equal(t, "error on stderr\n", fakeStderr.String()) } + +func TestDescribeImageSmoke(t *testing.T) { + restore := main.MockNewRepoRegistry(testrepos.New) + defer restore() + + restore = main.MockOsArgs([]string{ + "describe-image", + "centos-9", "qcow2", + }) + 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`) +}