From 56a9a01cfcd0fd1e011d88aa632ad94063352085 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. 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. --- cmd/image-builder/describeimg.go | 107 ++++++++++++++++++++++++++ cmd/image-builder/describeimg_test.go | 60 +++++++++++++++ cmd/image-builder/export_test.go | 7 +- cmd/image-builder/main.go | 40 ++++++++++ cmd/image-builder/main_test.go | 24 ++++++ 5 files changed, 235 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 00000000..c586aff4 --- /dev/null +++ b/cmd/image-builder/describeimg.go @@ -0,0 +1,107 @@ +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"` + + BuildPipelines []string `yaml:"build_pipelines"` + PayloadPipelines []string `yaml:"payload_pipelines"` + Packages map[string]*packagesYAML `yaml:"packages"` +} + +type packagesYAML struct { + Include []string `yaml:"include"` + Exclude []string `yaml:"exclude"` +} + +func packageSetsFor(imgType distro.ImageType) (map[string]*packagesYAML, error) { + var bp blueprint.Blueprint + manifest, _, err := imgType.Manifest(&bp, distro.ImageOptions{}, nil, nil) + if err != nil { + return nil, err + } + + res := make(map[string]*packagesYAML) + + for pipelineName, pkgSets := range manifest.GetPackageSetChains() { + incM := map[string]bool{} + excM := map[string]bool{} + for _, pkgSet := range pkgSets { + for _, s := range pkgSet.Include { + incM[s] = true + } + for _, s := range pkgSet.Exclude { + excM[s] = true + } + } + inc := make([]string, 0, len(incM)) + exc := make([]string, 0, len(excM)) + for name := range incM { + inc = append(inc, name) + } + for name := range excM { + exc = append(exc, name) + } + slices.Sort(inc) + slices.Sort(exc) + + res[pipelineName] = &packagesYAML{ + Include: inc, + Exclude: exc, + } + } + return res, 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) + pkgSets, 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(), + BuildPipelines: img.ImgType.BuildPipelines(), + PayloadPipelines: img.ImgType.PayloadPipelines(), + Packages: pkgSets, + } + // 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) +} diff --git a/cmd/image-builder/describeimg_test.go b/cmd/image-builder/describeimg_test.go new file mode 100644 index 00000000..6d94dd0a --- /dev/null +++ b/cmd/image-builder/describeimg_test.go @@ -0,0 +1,60 @@ +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 +build_pipelines: + - build +payload_pipelines: + - os + - archive +packages: + build: + include: + - coreutils + - glibc + - platform-python + - policycoreutils + - python3 + - rpm + - selinux-policy-targeted + - systemd + - tar + - xz + exclude: [] + os: + include: + - policycoreutils + - 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 5b3005f7..c8bc7e9b 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 d5cf7f5a..909d68b1 100644 --- a/cmd/image-builder/main.go +++ b/cmd/image-builder/main.go @@ -202,6 +202,32 @@ func cmdBuild(cmd *cobra.Command, args []string) error { return buildImage(pbar, 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 generates a lot of noisy logs a bunch of stuff to // Debug/Info that is distracting the user (at least by @@ -271,6 +297,20 @@ operating systems like Fedora, CentOS and RHEL with easy customizations support. buildCmd.Flags().String("progress", "auto", "type of progress bar to use (e.g. verbose,term)") rootCmd.AddCommand(buildCmd) + // XXX: add --format=json too? + describeImgCmd := &cobra.Command{ + Use: "describe-image ", + Short: "Describe the given image-type, e.g. qcow2 (tip: combine with --distro,--arch)", + 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() } diff --git a/cmd/image-builder/main_test.go b/cmd/image-builder/main_test.go index 71063614..5dbef4c4 100644 --- a/cmd/image-builder/main_test.go +++ b/cmd/image-builder/main_test.go @@ -543,3 +543,27 @@ 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", + "--arch=x86_64", + }) + 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`) +}