diff --git a/cmd/image-builder/describeimg.go b/cmd/image-builder/describeimg.go new file mode 100644 index 0000000..c586aff --- /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 0000000..6d94dd0 --- /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 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 d5cf7f5..909d68b 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 7106361..5dbef4c 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`) +}