diff --git a/cmd/image-builder/bib_cmd.go b/cmd/image-builder/bib_cmd.go new file mode 100644 index 0000000000..e77e7f6405 --- /dev/null +++ b/cmd/image-builder/bib_cmd.go @@ -0,0 +1,155 @@ +package main + +import ( + "fmt" + "os" + + "github.com/osbuild/image-builder/internal/bibimg" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "golang.org/x/exp/slices" +) + +func setupBibRootCmd() (*cobra.Command, error) { + version, err := bibVersionFromBuildInfo() + if err != nil { + return nil, err + } + + rootCmd := &cobra.Command{ + Use: "bootc-image-builder", + Long: "Build operating system images from bootc containers", + PersistentPreRunE: bibRootPreRunE, + SilenceErrors: true, + Version: version, + } + rootCmd.SetVersionTemplate(version) + + rootCmd.PersistentFlags().StringVar(&rootLogLevel, "log-level", "", "logging level (debug, info, error); default error") + rootCmd.PersistentFlags().BoolP("verbose", "v", false, `Switch to verbose mode`) + + buildCmd := setupBibBuildCmd() + if err != nil { + return nil, err + } + rootCmd.AddCommand(buildCmd) + + manifestCmd, err := setupBibManifestCmd() + if err != nil { + return nil, err + } + rootCmd.AddCommand(manifestCmd) + + // add manifest flags to the build subcommand + buildCmd.Flags().AddFlagSet(manifestCmd.Flags()) + // flag rules + for _, dname := range []string{"output", "store", "rpmmd"} { + if err := buildCmd.MarkFlagDirname(dname); err != nil { + return nil, err + } + } + if err := buildCmd.MarkFlagFilename("config"); err != nil { + return nil, err + } + + versionCmd := setupBibVersionCmd() + rootCmd.AddCommand(versionCmd) + + // If no subcommand is given, assume the user wants to use the build subcommand + // See https://github.com/spf13/cobra/issues/823#issuecomment-870027246 + // which cannot be used verbatim because the arguments for "build" like + // "quay.io" will create an "err != nil". Ideally we could check err + // for something like cobra.UnknownCommandError but cobra just gives + // us an error string + cmd, _, err := rootCmd.Find(os.Args[1:]) + injectBuildArg := func() { + args := append([]string{buildCmd.Name()}, os.Args[1:]...) + rootCmd.SetArgs(args) + } + // command not known, i.e. happens for "bib quay.io/centos/..." + if err != nil && !slices.Contains([]string{"help", "completion"}, os.Args[1]) { + injectBuildArg() + } + // command appears valid, e.g. "bib --local quay.io/centos" but this + // is the parser just assuming "quay.io" is an argument for "--local" :( + if err == nil && cmd.Use == rootCmd.Use && cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp { + injectBuildArg() + } + + return rootCmd, nil +} + +func setupBibBuildCmd() *cobra.Command { + buildCmd := &cobra.Command{ + Use: "build IMAGE_NAME", + Args: cobra.ExactArgs(1), + DisableFlagsInUseLine: true, + RunE: bibCmdBuild, + SilenceUsage: true, + } + buildCmd.Flags().String("aws-ami-name", "", "name for the AMI in AWS (only for type=ami)") + buildCmd.Flags().String("aws-bucket", "", "target S3 bucket name for intermediate storage when creating AMI (only for type=ami)") + buildCmd.Flags().String("aws-region", "", "target region for AWS uploads (only for type=ami)") + buildCmd.Flags().String("chown", "", "chown the ouput directory to match the specified UID:GID") + buildCmd.Flags().String("output", ".", "artifact output directory") + buildCmd.Flags().String("store", "/store", "osbuild store for intermediate pipeline trees") + //TODO: add json progress for higher level tools like "podman bootc" + buildCmd.Flags().String("progress", "auto", "type of progress bar to use (e.g. verbose,term)") + + buildCmd.MarkFlagsRequiredTogether("aws-region", "aws-bucket", "aws-ami-name") + + return buildCmd +} + +func setupBibManifestCmd() (*cobra.Command, error) { + manifestCmd := &cobra.Command{ + Use: "manifest", + Short: "Only create the manifest but don't build the image.", + Args: cobra.ExactArgs(1), + DisableFlagsInUseLine: true, + RunE: bibCmdManifest, + SilenceUsage: true, + } + + manifestCmd.Flags().Bool("tls-verify", false, "DEPRECATED: require HTTPS and verify certificates when contacting registries") + if err := manifestCmd.Flags().MarkHidden("tls-verify"); err != nil { + return nil, fmt.Errorf("cannot hide 'tls-verify' :%w", err) + } + manifestCmd.Flags().String("rpmmd", "/rpmmd", "rpm metadata cache directory") + manifestCmd.Flags().String("target-arch", "", "build for the given target architecture (experimental)") + manifestCmd.Flags().String("build-container", "", "Use a custom container for the image build") + // XXX: add --bootc-installer-payload-ref as alias to make it + // cmdline compatible with ibcli(?) + manifestCmd.Flags().String("installer-payload-ref", "", "bootc installer payload ref") + manifestCmd.Flags().StringArray("type", []string{"qcow2"}, fmt.Sprintf("image types to build [%s]", bibimg.Available())) + manifestCmd.Flags().Bool("local", true, "DEPRECATED: --local is now the default behavior, make sure to pull the container image before running bootc-image-builder") + if err := manifestCmd.Flags().MarkHidden("local"); err != nil { + return nil, fmt.Errorf("cannot hide 'local' :%w", err) + } + manifestCmd.Flags().String("rootfs", "", "Root filesystem type. If not given, the default configured in the source container image is used.") + manifestCmd.Flags().Bool("use-librepo", true, "switch to librepo for pkg download, needs new enough osbuild") + // --config is only useful for developers who run bib outside + // of a container to generate a manifest. so hide it by + // default from users. + manifestCmd.Flags().String("config", "", "build config file; /config.json will be used if present") + if err := manifestCmd.Flags().MarkHidden("config"); err != nil { + return nil, fmt.Errorf("cannot hide 'config' :%w", err) + } + + return manifestCmd, nil +} + +func setupBibVersionCmd() *cobra.Command { + versionCmd := &cobra.Command{ + Use: "version", + Short: "Show the version and quit", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + root := cmd.Root() + root.SetArgs([]string{"--version"}) + return root.Execute() + }, + } + + return versionCmd +} diff --git a/cmd/image-builder/bib_main.go b/cmd/image-builder/bib_main.go index c716d4afc8..c9ed56ea0b 100644 --- a/cmd/image-builder/bib_main.go +++ b/cmd/image-builder/bib_main.go @@ -13,7 +13,6 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/spf13/pflag" "golang.org/x/exp/slices" repos "github.com/osbuild/image-builder/data/repositories" @@ -321,12 +320,12 @@ func bibCmdBuild(cmd *cobra.Command, args []string) error { return fmt.Errorf("cannot ensure ownership: %w", err) } if !canChown && chown != "" { - return fmt.Errorf("chowning is not allowed in output directory") + return fmt.Errorf("chown permission check failed for the output directory") } pbar, err := progress.New(progressType, progress.ProgressConfig{}) if err != nil { - return fmt.Errorf("cannto create progress bar: %w", err) + return fmt.Errorf("failed to initialise progress bar: %w", err) } defer pbar.Stop() @@ -467,137 +466,8 @@ build_tainted: %v `, gitRev, buildTime, buildTainted), nil } -func bibBuildCobraCmdline() (*cobra.Command, error) { - version, err := bibVersionFromBuildInfo() - if err != nil { - return nil, err - } - - rootCmd := &cobra.Command{ - Use: "bootc-image-builder", - Long: "Create a bootable image from an ostree native container", - PersistentPreRunE: bibRootPreRunE, - SilenceErrors: true, - Version: version, - } - rootCmd.SetVersionTemplate(version) - - rootCmd.PersistentFlags().StringVar(&rootLogLevel, "log-level", "", "logging level (debug, info, error); default error") - rootCmd.PersistentFlags().BoolP("verbose", "v", false, `Switch to verbose mode`) - - buildCmd := &cobra.Command{ - Use: "build IMAGE_NAME", - Short: rootCmd.Long + " (default command)", - Long: rootCmd.Long + "\n" + - "(default action if no command is given)\n" + - "IMAGE_NAME: container image to build into a bootable image", - Args: cobra.ExactArgs(1), - DisableFlagsInUseLine: true, - RunE: bibCmdBuild, - SilenceUsage: true, - Example: rootCmd.Use + " build quay.io/centos-bootc/centos-bootc:stream9\n" + - rootCmd.Use + " quay.io/centos-bootc/centos-bootc:stream9\n", - Version: rootCmd.Version, - } - buildCmd.SetVersionTemplate(version) - - rootCmd.AddCommand(buildCmd) - manifestCmd := &cobra.Command{ - Use: "manifest", - Short: "Only create the manifest but don't build the image.", - Args: cobra.ExactArgs(1), - DisableFlagsInUseLine: true, - RunE: bibCmdManifest, - SilenceUsage: true, - Version: rootCmd.Version, - } - manifestCmd.SetVersionTemplate(version) - - versionCmd := &cobra.Command{ - Use: "version", - Short: "Show the version and quit", - SilenceUsage: true, - RunE: func(cmd *cobra.Command, args []string) error { - root := cmd.Root() - root.SetArgs([]string{"--version"}) - return root.Execute() - }, - } - - rootCmd.AddCommand(versionCmd) - - rootCmd.AddCommand(manifestCmd) - manifestCmd.Flags().Bool("tls-verify", false, "DEPRECATED: require HTTPS and verify certificates when contacting registries") - if err := manifestCmd.Flags().MarkHidden("tls-verify"); err != nil { - return nil, fmt.Errorf("cannot hide 'tls-verify' :%w", err) - } - manifestCmd.Flags().String("rpmmd", "/rpmmd", "rpm metadata cache directory") - manifestCmd.Flags().String("target-arch", "", "build for the given target architecture (experimental)") - manifestCmd.Flags().String("build-container", "", "Use a custom container for the image build") - // XXX: add --bootc-installer-payload-ref as alias to make it - // cmdline compatible with ibcli(?) - manifestCmd.Flags().String("installer-payload-ref", "", "bootc installer payload ref") - manifestCmd.Flags().StringArray("type", []string{"qcow2"}, fmt.Sprintf("image types to build [%s]", bibimg.Available())) - manifestCmd.Flags().Bool("local", true, "DEPRECATED: --local is now the default behavior, make sure to pull the container image before running bootc-image-builder") - if err := manifestCmd.Flags().MarkHidden("local"); err != nil { - return nil, fmt.Errorf("cannot hide 'local' :%w", err) - } - manifestCmd.Flags().String("rootfs", "", "Root filesystem type. If not given, the default configured in the source container image is used.") - manifestCmd.Flags().Bool("use-librepo", true, "switch to librepo for pkg download, needs new enough osbuild") - // --config is only useful for developers who run bib outside - // of a container to generate a manifest. so hide it by - // default from users. - manifestCmd.Flags().String("config", "", "build config file; /config.json will be used if present") - if err := manifestCmd.Flags().MarkHidden("config"); err != nil { - return nil, fmt.Errorf("cannot hide 'config' :%w", err) - } - - buildCmd.Flags().AddFlagSet(manifestCmd.Flags()) - buildCmd.Flags().String("aws-ami-name", "", "name for the AMI in AWS (only for type=ami)") - buildCmd.Flags().String("aws-bucket", "", "target S3 bucket name for intermediate storage when creating AMI (only for type=ami)") - buildCmd.Flags().String("aws-region", "", "target region for AWS uploads (only for type=ami)") - buildCmd.Flags().String("chown", "", "chown the ouput directory to match the specified UID:GID") - buildCmd.Flags().String("output", ".", "artifact output directory") - buildCmd.Flags().String("store", "/store", "osbuild store for intermediate pipeline trees") - //TODO: add json progress for higher level tools like "podman bootc" - buildCmd.Flags().String("progress", "auto", "type of progress bar to use (e.g. verbose,term)") - // flag rules - for _, dname := range []string{"output", "store", "rpmmd"} { - if err := buildCmd.MarkFlagDirname(dname); err != nil { - return nil, err - } - } - if err := buildCmd.MarkFlagFilename("config"); err != nil { - return nil, err - } - buildCmd.MarkFlagsRequiredTogether("aws-region", "aws-bucket", "aws-ami-name") - - // If no subcommand is given, assume the user wants to use the build subcommand - // See https://github.com/spf13/cobra/issues/823#issuecomment-870027246 - // which cannot be used verbatim because the arguments for "build" like - // "quay.io" will create an "err != nil". Ideally we could check err - // for something like cobra.UnknownCommandError but cobra just gives - // us an error string - cmd, _, err := rootCmd.Find(os.Args[1:]) - injectBuildArg := func() { - args := append([]string{buildCmd.Name()}, os.Args[1:]...) - rootCmd.SetArgs(args) - } - // command not known, i.e. happens for "bib quay.io/centos/..." - if err != nil && !slices.Contains([]string{"help", "completion"}, os.Args[1]) { - injectBuildArg() - } - // command appears valid, e.g. "bib --local quay.io/centos" but this - // is the parser just assuming "quay.io" is an argument for "--local" :( - if err == nil && cmd.Use == rootCmd.Use && cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp { - injectBuildArg() - } - - return rootCmd, nil -} - func bibRun() error { - rootCmd, err := bibBuildCobraCmdline() + rootCmd, err := setupBibRootCmd() if err != nil { return err } diff --git a/cmd/image-builder/bib_main_test.go b/cmd/image-builder/bib_main_test.go index 5bd7448589..ab9af09b02 100644 --- a/cmd/image-builder/bib_main_test.go +++ b/cmd/image-builder/bib_main_test.go @@ -109,7 +109,7 @@ func TestCobraCmdline(t *testing.T) { restore := mockOsArgs(tc.cmdline) defer restore() - rootCmd, err := main.BuildCobraCmdline() + rootCmd, err := main.SetupBibRootCmd() assert.NoError(t, err) addRunLog(rootCmd, &runeCall) @@ -141,7 +141,7 @@ func TestCobraCmdlineVerbose(t *testing.T) { restore := mockOsArgs(tc.cmdline) defer restore() - rootCmd, err := main.BuildCobraCmdline() + rootCmd, err := main.SetupBibRootCmd() assert.NoError(t, err) // collect progressFlag value @@ -213,7 +213,7 @@ func TestHandleAWSFlags(t *testing.T) { return &fau, nil })) - rootCmd, err := main.BuildCobraCmdline() + rootCmd, err := main.SetupBibRootCmd() assert.NoError(t, err) // Commands() returns commandsordered by name buildCmd := rootCmd.Commands()[0] diff --git a/cmd/image-builder/cmd.go b/cmd/image-builder/cmd.go new file mode 100644 index 0000000000..be750efed8 --- /dev/null +++ b/cmd/image-builder/cmd.go @@ -0,0 +1,313 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/osbuild/image-builder/internal/olog" + ilog "github.com/osbuild/image-builder/pkg/olog" + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" +) + +func setupRootCmd() (*cobra.Command, error) { + rootCmd := &cobra.Command{ + Use: "image-builder", + Short: "Build operating system images from a given distro/image-type/blueprint", + Long: `Build operating system images from a given distribution, +image-type and blueprint. + +Image-builder builds operating system images for a range of predefined +operating systems like Fedora, CentOS and RHEL with easy customizations support.`, + SilenceErrors: true, + CompletionOptions: cobra.CompletionOptions{ + HiddenDefaultCmd: true, + }, + Run: func(cmd *cobra.Command, args []string) { + // This is not the ideal way to implement this, but cobra does not + // support lazy version strings and we need to call "osbuild" subprocess + // to get its version. + if versionFlag, err := cmd.Flags().GetBool("version"); err == nil && versionFlag { + fmt.Fprintln(cmd.ErrOrStderr(), `Flag --version has been deprecated, use "image-builder version" instead`) + fmt.Fprint(cmd.OutOrStdout(), prettyVersion()) + } else { + _ = cmd.Help() + } + }, + } + + rootCmd.Flags().Bool("version", false, "Print version information and exit (deprecated: use \"image-builder version\" instead)") + if err := rootCmd.Flags().MarkHidden("version"); err != nil { + return nil, err + } + var forceRepoDir string + rootCmd.PersistentFlags().StringVar(&forceRepoDir, "force-repo-dir", "", "Override the default repository search path for custom repository files") + rootCmd.PersistentFlags().StringVar(&forceRepoDir, "force-data-dir", "", `Override the default data directory for e.g. custom repositories/*.json data`) + if err := rootCmd.PersistentFlags().MarkDeprecated("force-data-dir", `Use --force-repo-dir instead`); err != nil { + return nil, err + } + rootCmd.PersistentFlags().StringVar(&forceRepoDir, "data-dir", "", `Override the default data directory for e.g. custom repositories/*.json data`) + if err := rootCmd.PersistentFlags().MarkDeprecated("data-dir", `Use --force-repo-dir instead`); err != nil { + return nil, err + } + rootCmd.PersistentFlags().String("force-defs-dir", "", "Override the path to load YAML distro definitions from") + if err := rootCmd.PersistentFlags().MarkHidden("force-defs-dir"); err != nil { + return nil, err + } + rootCmd.PersistentFlags().StringArray("extra-repo", nil, `Add an extra repository during build (will *not* be gpg checked and not be part of the final image)`) + rootCmd.PersistentFlags().StringArray("force-repo", nil, `Override the base repositories during build (these will not be part of the final image)`) + rootCmd.PersistentFlags().String("output-dir", "", `Put output into the specified directory`) + rootCmd.PersistentFlags().BoolP("verbose", "v", false, `Switch to verbose mode (more logging on stderr and verbose progress)`) + registerMemProfileFlags(rootCmd) + rootCmd.PersistentPreRun = memProfilePersistentPreRun + + rootCmd.SetOut(osStdout) + rootCmd.SetErr(osStderr) + + bootcCmd, err := setupBootcCmd() + if err != nil { + return nil, err + } + rootCmd.AddCommand(bootcCmd) + + listCmd := setupListCmd() + rootCmd.AddCommand(listCmd) + + versionCmd := setupVersionCmd() + rootCmd.AddCommand(versionCmd) + + systemCmd := setupSystemCmd() + rootCmd.AddCommand(systemCmd) + + manifestCmd, err := setupManifestCmd() + if err != nil { + return nil, err + } + rootCmd.AddCommand(manifestCmd) + + uploadCmd := setupUploadCmd() + rootCmd.AddCommand(uploadCmd) + + buildCmd := setupBuildCmd() + buildCmd.Flags().AddFlagSet(manifestCmd.Flags()) + // The build command can upload images to the appropriate cloud provider, + // so it should support the upload options as well + buildCmd.Flags().AddFlagSet(uploadCmd.Flags()) + rootCmd.AddCommand(buildCmd) + + buildCmd.Flags().AddFlagSet(uploadCmd.Flags()) + // add after the rest of the uploadCmd flag set is added to avoid + // that build gets a "--to" parameter + uploadCmd.Flags().String("to", "", "upload to the given cloud") + + describeCmd := setupDescribeCmd() + rootCmd.AddCommand(describeCmd) + + docCmd := setupDocCmd(rootCmd) + rootCmd.AddCommand(docCmd) + + verbose, err := rootCmd.PersistentFlags().GetBool("verbose") + if err != nil { + return nil, err + } + if verbose { + olog.SetDefault(log.New(os.Stderr, "", 0)) + ilog.SetDefault(log.New(os.Stderr, "", 0)) + } + + return rootCmd, nil +} + +func setupBootcCmd() (*cobra.Command, error) { + bootcCmd := &cobra.Command{ + Use: "bootc", + Short: "bootc-related commands", + Args: cobra.NoArgs, + } + + bootcInspectCommand := &cobra.Command{ + Use: "inspect", + Short: "Show data gathered by `image-builder` for a container", + RunE: cmdBootcInspect, + Args: cobra.NoArgs, + } + bootcInspectCommand.Flags().String("ref", "", `bootc container ref`) + if err := bootcInspectCommand.MarkFlagRequired("ref"); err != nil { + return nil, err + } + bootcInspectCommand.Flags().String("format", "", "Output in a specific format (yaml, json)") + bootcCmd.AddCommand(bootcInspectCommand) + + return bootcCmd, nil +} + +func setupListCmd() *cobra.Command { + listCmd := &cobra.Command{ + Use: "list", + Short: "List buildable images, use --filter to limit further", + RunE: cmdListImages, + SilenceUsage: true, + Args: cobra.NoArgs, + Aliases: []string{"list-images"}, + } + listCmd.Flags().StringArray("filter", nil, `Filter distributions by a specific criteria (e.g. "type:iot*")`) + listCmd.Flags().String("format", "", "Output in a specific format (text, json)") + + return listCmd +} + +func setupVersionCmd() *cobra.Command { + versionCmd := &cobra.Command{ + Use: "version", + Short: "Print version information", + RunE: cmdVersion, + Args: cobra.NoArgs, + } + versionCmd.Flags().String("format", "", "Output in a specific format (yaml, json)") + + return versionCmd +} + +func setupSystemCmd() *cobra.Command { + systemCmd := &cobra.Command{ + Use: "system", + Short: "Show system status information", + RunE: cmdSystem, + Args: cobra.NoArgs, + } + systemCmd.Flags().String("format", "", "Output in a specific format (yaml, json)") + + return systemCmd +} + +func setupManifestCmd() (*cobra.Command, error) { + manifestCmd := &cobra.Command{ + Use: "manifest ", + Short: "Build manifest for the given image-type, e.g. qcow2 (tip: combine with --distro, --arch)", + RunE: cmdManifest, + SilenceUsage: true, + Args: cobra.ExactArgs(1), + Hidden: true, + } + manifestCmd.Flags().String("blueprint", "", `filename of a blueprint to customize an image`) + manifestCmd.Flags().Int64("seed", 0, `rng seed, some values are derived randomly, pinning the seed allows more reproducibility if you need it. must be an integer. only used when changed.`) + manifestCmd.Flags().String("arch", "", `build manifest for a different architecture`) + manifestCmd.Flags().String("distro", "", `build manifest for a different distroname (e.g. centos-9)`) + manifestCmd.Flags().String("ostree-ref", "", `OSTREE reference`) + manifestCmd.Flags().String("ostree-parent", "", `OSTREE parent`) + manifestCmd.Flags().String("ostree-url", "", `OSTREE url`) + manifestCmd.Flags().String("bootc-ref", "", `bootc container ref`) + manifestCmd.Flags().String("bootc-build-ref", "", `bootc build container ref`) + manifestCmd.Flags().String("bootc-installer-payload-ref", "", `bootc installer payload ref`) + manifestCmd.Flags().String("bootc-default-fs", "", `default filesystem to use for the bootc install (e.g. ext4)`) + manifestCmd.Flags().Bool("bootc-no-default-kernel-args", false, `don't use the default kernel arguments`) + manifestCmd.Flags().Bool("use-librepo", true, `use librepo to download packages (disable if you use old versions of osbuild)`) + if err := manifestCmd.Flags().MarkHidden("use-librepo"); err != nil { + return nil, err + } + manifestCmd.Flags().Bool("with-sbom", false, `export SPDX SBOM document`) + manifestCmd.Flags().Bool("with-rpmlist", false, `export RPM list as JSON`) + if err := manifestCmd.Flags().MarkHidden("with-rpmlist"); err != nil { + return nil, err + } + manifestCmd.Flags().Bool("ignore-warnings", false, `ignore warnings during manifest generation`) + manifestCmd.Flags().String("registrations", "", `filename of a registrations file with e.g. subscription details`) + manifestCmd.Flags().String("rpmmd-cache", "", `osbuild directory to cache rpm metadata`) + manifestCmd.Flags().Bool("preview", true, `override distro default preview state if passed`) + if err := manifestCmd.Flags().MarkHidden("preview"); err != nil { + return nil, err + } + return manifestCmd, nil +} + +func setupUploadCmd() *cobra.Command { + uploadCmd := &cobra.Command{ + Use: "upload ", + Short: "Upload the given image from ", + RunE: cmdUpload, + SilenceUsage: true, + Args: cobra.ExactArgs(1), + } + uploadCmd.Flags().String("aws-ami-name", "", "name for the AMI in AWS (only for type=ami)") + uploadCmd.Flags().String("aws-bucket", "", "target S3 bucket name for intermediate storage when creating AMI (only for type=ami)") + uploadCmd.Flags().String("aws-region", "", "target region for AWS uploads (only for type=ami)") + uploadCmd.Flags().String("aws-profile", "", "name of the AWS credentials profile (only for type=aws)") + uploadCmd.Flags().StringArray("aws-tag", []string{}, "tag the AMI with this Key=Value (only for type=aws)") + uploadCmd.Flags().String("aws-boot-mode", "", "boot mode for the AMI: legacy-bios, uefi, uefi-preferred (only for type=aws)") + uploadCmd.Flags().String("libvirt-connection", "", "connection URI (only for type=libvirt)") + uploadCmd.Flags().String("libvirt-pool", "", "pool name (only for type=libvirt)") + uploadCmd.Flags().String("libvirt-volume", "", "volume name (only for type=libvirt)") + uploadCmd.Flags().String("openstack-image", "", "name for the uploaded image (only for type=openstack)") + uploadCmd.Flags().String("openstack-disk-format", "raw", "the disk format of a virtual machine image (only for type=openstack)") + uploadCmd.Flags().String("openstack-container-format", "bare", "this indicates if the image contains metadata about the VM (only for type=openstack)") + uploadCmd.Flags().String("ibmcloud-bucket", "", "target bucket name for storing the image (only for type=ibmcloud)") + uploadCmd.Flags().String("ibmcloud-region", "", "target region for IBM Cloud uploads (only for type=ibmcloud)") + uploadCmd.Flags().String("ibmcloud-image-name", "", "name for the uploaded image (only for type=ibmcloud)") + uploadCmd.Flags().String("azure-client-id", "", "Azure client ID (only for type=azure)") + uploadCmd.Flags().String("azure-client-secret", "", "Azure client secret (only for type=azure)") + uploadCmd.Flags().String("azure-tenant", "", "Azure tenant ID (only for type=azure)") + uploadCmd.Flags().String("azure-subscription", "", "Azure subscription ID (only for type=azure)") + uploadCmd.Flags().String("azure-resource-group", "", "Azure resource group (only for type=azure)") + uploadCmd.Flags().String("azure-image-name", "", "name for the uploaded image (only for type=azure)") + uploadCmd.Flags().String("arch", "", "upload for the given architecture") + uploadCmd.Flags().String("format", "", "output in a specific format (yaml, json)") + + return uploadCmd +} + +func setupBuildCmd() *cobra.Command { + buildCmd := &cobra.Command{ + Use: "build ", + Short: "Build the given image-type, e.g. qcow2 (tip: combine with --distro, --arch)", + RunE: cmdBuild, + SilenceUsage: true, + Args: cobra.ExactArgs(1), + } + buildCmd.Flags().Bool("with-manifest", false, `export osbuild manifest`) + buildCmd.Flags().Bool("with-buildlog", false, `export osbuild buildlog`) + buildCmd.Flags().String("cache", defaultCacheDir(), `osbuild directory to cache intermediate build artifacts"`) + // XXX: add "--verbose" here, similar to how bib is doing this + // (see https://github.com/osbuild/bootc-image-builder/pull/790/commits/5cec7ffd8a526e2ca1e8ada0ea18f927695dfe43) + buildCmd.Flags().String("progress", "auto", "type of progress bar to use (e.g. verbose,term)") + buildCmd.Flags().Bool("with-metrics", false, `print timing information at the end of the build`) + buildCmd.Flags().String("output-name", "", "set specific output basename") + buildCmd.Flags().Bool("in-vm", false, `run the osbuild pipeline in a virtual machine`) + buildCmd.Flags().String("format", "", "Output in a specific format (json)") + + return buildCmd +} + +func setupDescribeCmd() *cobra.Command { + // XXX: add --format=json too? + describeCmd := &cobra.Command{ + Use: "describe ", + Short: "Describe the given image-type, e.g. qcow2 (tip: combine with --distro,--arch)", + RunE: cmdDescribeImg, + SilenceUsage: true, + Args: cobra.ExactArgs(1), + Hidden: false, + Aliases: []string{"describe-image"}, + } + describeCmd.Flags().String("arch", "", `use the different architecture`) + describeCmd.Flags().String("distro", "", `build manifest for a different distroname (e.g. centos-9)`) + describeCmd.Flags().Bool("in-vm", false, `run container in a virtual machine`) + + return describeCmd +} + +func setupDocCmd(rootCmd *cobra.Command) *cobra.Command { + docCmd := &cobra.Command{ + Use: "doc ", + Short: "Generate man pages for this command", + Hidden: true, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + header := &doc.GenManHeader{ + Section: "1", + } + return doc.GenManTree(rootCmd, header, args[0]) + }, + } + return docCmd +} diff --git a/cmd/image-builder/export_bib_test.go b/cmd/image-builder/export_bib_test.go index a08377db55..6638c90b4a 100644 --- a/cmd/image-builder/export_bib_test.go +++ b/cmd/image-builder/export_bib_test.go @@ -1,9 +1,9 @@ package main var ( - CanChownInPath = canChownInPath - BuildCobraCmdline = bibBuildCobraCmdline - HandleAWSFlags = handleAWSFlags + CanChownInPath = canChownInPath + SetupBibRootCmd = setupBibRootCmd + HandleAWSFlags = handleAWSFlags ) func MockOsGetuid(new func() int) (restore func()) { diff --git a/cmd/image-builder/main.go b/cmd/image-builder/main.go index 3fcc027b3c..bf148fd593 100644 --- a/cmd/image-builder/main.go +++ b/cmd/image-builder/main.go @@ -24,13 +24,11 @@ import ( "github.com/osbuild/image-builder/pkg/distro/generic" "github.com/osbuild/image-builder/pkg/imagefilter" "github.com/osbuild/image-builder/pkg/manifestgen" - ilog "github.com/osbuild/image-builder/pkg/olog" "github.com/osbuild/image-builder/pkg/osbuild" "github.com/osbuild/image-builder/pkg/ostree" "github.com/osbuild/image-builder/pkg/progress" "github.com/osbuild/image-builder/internal/blueprintload" - "github.com/osbuild/image-builder/internal/olog" "github.com/osbuild/image-builder/pkg/setup" ) @@ -722,227 +720,10 @@ func run() error { log.SetFlags(0) memProfileResetForProcessStart() - rootCmd := &cobra.Command{ - Use: "image-builder", - Short: "Build operating system images from a given distro/image-type/blueprint", - Long: `Build operating system images from a given distribution, -image-type and blueprint. - -Image-builder builds operating system images for a range of predefined -operating systems like Fedora, CentOS and RHEL with easy customizations support.`, - SilenceErrors: true, - CompletionOptions: cobra.CompletionOptions{ - HiddenDefaultCmd: true, - }, - Run: func(cmd *cobra.Command, args []string) { - // This is not the ideal way to implement this, but cobra does not - // support lazy version strings and we need to call "osbuild" subprocess - // to get its version. - if versionFlag, err := cmd.Flags().GetBool("version"); err == nil && versionFlag { - fmt.Fprintln(cmd.ErrOrStderr(), `Flag --version has been deprecated, use "image-builder version" instead`) - fmt.Fprint(cmd.OutOrStdout(), prettyVersion()) - } else { - _ = cmd.Help() - } - }, - } - - rootCmd.Flags().Bool("version", false, "Print version information and exit (deprecated: use \"image-builder version\" instead)") - if err := rootCmd.Flags().MarkHidden("version"); err != nil { - return err - } - var forceRepoDir string - rootCmd.PersistentFlags().StringVar(&forceRepoDir, "force-repo-dir", "", "Override the default repository search path for custom repository files") - rootCmd.PersistentFlags().StringVar(&forceRepoDir, "force-data-dir", "", `Override the default data directory for e.g. custom repositories/*.json data`) - if err := rootCmd.PersistentFlags().MarkDeprecated("force-data-dir", `Use --force-repo-dir instead`); err != nil { - return err - } - rootCmd.PersistentFlags().StringVar(&forceRepoDir, "data-dir", "", `Override the default data directory for e.g. custom repositories/*.json data`) - if err := rootCmd.PersistentFlags().MarkDeprecated("data-dir", `Use --force-repo-dir instead`); err != nil { - return err - } - rootCmd.PersistentFlags().String("force-defs-dir", "", "Override the path to load YAML distro definitions from") - if err := rootCmd.PersistentFlags().MarkHidden("force-defs-dir"); err != nil { + rootCmd, err := setupRootCmd() + if err != nil { return err } - rootCmd.PersistentFlags().StringArray("extra-repo", nil, `Add an extra repository during build (will *not* be gpg checked and not be part of the final image)`) - rootCmd.PersistentFlags().StringArray("force-repo", nil, `Override the base repositories during build (these will not be part of the final image)`) - rootCmd.PersistentFlags().String("output-dir", "", `Put output into the specified directory`) - rootCmd.PersistentFlags().BoolP("verbose", "v", false, `Switch to verbose mode (more logging on stderr and verbose progress)`) - registerMemProfileFlags(rootCmd) - rootCmd.PersistentPreRun = memProfilePersistentPreRun - - rootCmd.SetOut(osStdout) - rootCmd.SetErr(osStderr) - - bootcCommand := &cobra.Command{ - Use: "bootc", - Short: "bootc-related commands", - Args: cobra.NoArgs, - } - - bootcInspectCommand := &cobra.Command{ - Use: "inspect", - Short: "Show data gathered by `image-builder` for a container", - RunE: cmdBootcInspect, - Args: cobra.NoArgs, - } - bootcInspectCommand.Flags().String("ref", "", `bootc container ref`) - _ = bootcInspectCommand.MarkFlagRequired("ref") - bootcInspectCommand.Flags().String("format", "", "Output in a specific format (yaml, json)") - bootcCommand.AddCommand(bootcInspectCommand) - - rootCmd.AddCommand(bootcCommand) - - listCmd := &cobra.Command{ - Use: "list", - Short: "List buildable images, use --filter to limit further", - RunE: cmdListImages, - SilenceUsage: true, - Args: cobra.NoArgs, - Aliases: []string{"list-images"}, - } - listCmd.Flags().StringArray("filter", nil, `Filter distributions by a specific criteria (e.g. "type:iot*")`) - listCmd.Flags().String("format", "", "Output in a specific format (text, json)") - rootCmd.AddCommand(listCmd) - - versionCmd := &cobra.Command{ - Use: "version", - Short: "Print version information", - RunE: cmdVersion, - Args: cobra.NoArgs, - } - versionCmd.Flags().String("format", "", "Output in a specific format (yaml, json)") - rootCmd.AddCommand(versionCmd) - - systemCmd := &cobra.Command{ - Use: "system", - Short: "Show system status information", - RunE: cmdSystem, - Args: cobra.NoArgs, - } - systemCmd.Flags().String("format", "", "Output in a specific format (yaml, json)") - rootCmd.AddCommand(systemCmd) - - manifestCmd := &cobra.Command{ - Use: "manifest ", - Short: "Build manifest for the given image-type, e.g. qcow2 (tip: combine with --distro, --arch)", - RunE: cmdManifest, - SilenceUsage: true, - Args: cobra.ExactArgs(1), - Hidden: true, - } - manifestCmd.Flags().String("blueprint", "", `filename of a blueprint to customize an image`) - manifestCmd.Flags().Int64("seed", 0, `rng seed, some values are derived randomly, pinning the seed allows more reproducibility if you need it. must be an integer. only used when changed.`) - manifestCmd.Flags().String("arch", "", `build manifest for a different architecture`) - manifestCmd.Flags().String("distro", "", `build manifest for a different distroname (e.g. centos-9)`) - manifestCmd.Flags().String("ostree-ref", "", `OSTREE reference`) - manifestCmd.Flags().String("ostree-parent", "", `OSTREE parent`) - manifestCmd.Flags().String("ostree-url", "", `OSTREE url`) - manifestCmd.Flags().String("bootc-ref", "", `bootc container ref`) - manifestCmd.Flags().String("bootc-build-ref", "", `bootc build container ref`) - manifestCmd.Flags().String("bootc-installer-payload-ref", "", `bootc installer payload ref`) - manifestCmd.Flags().String("bootc-default-fs", "", `default filesystem to use for the bootc install (e.g. ext4)`) - manifestCmd.Flags().Bool("bootc-no-default-kernel-args", false, `don't use the default kernel arguments`) - manifestCmd.Flags().Bool("use-librepo", true, `use librepo to download packages (disable if you use old versions of osbuild)`) - if err := manifestCmd.Flags().MarkHidden("use-librepo"); err != nil { - return err - } - manifestCmd.Flags().Bool("with-sbom", false, `export SPDX SBOM document`) - manifestCmd.Flags().Bool("with-rpmlist", false, `export RPM list as JSON`) - if err := manifestCmd.Flags().MarkHidden("with-rpmlist"); err != nil { - return err - } - manifestCmd.Flags().Bool("ignore-warnings", false, `ignore warnings during manifest generation`) - manifestCmd.Flags().String("registrations", "", `filename of a registrations file with e.g. subscription details`) - manifestCmd.Flags().String("rpmmd-cache", "", `osbuild directory to cache rpm metadata`) - manifestCmd.Flags().Bool("preview", true, `override distro default preview state if passed`) - if err := manifestCmd.Flags().MarkHidden("preview"); err != nil { - return err - } - rootCmd.AddCommand(manifestCmd) - - uploadCmd := &cobra.Command{ - Use: "upload ", - Short: "Upload the given image from ", - RunE: cmdUpload, - SilenceUsage: true, - Args: cobra.ExactArgs(1), - } - uploadCmd.Flags().String("aws-ami-name", "", "name for the AMI in AWS (only for type=ami)") - uploadCmd.Flags().String("aws-bucket", "", "target S3 bucket name for intermediate storage when creating AMI (only for type=ami)") - uploadCmd.Flags().String("aws-region", "", "target region for AWS uploads (only for type=ami)") - uploadCmd.Flags().String("aws-profile", "", "name of the AWS credentials profile (only for type=aws)") - uploadCmd.Flags().StringArray("aws-tag", []string{}, "tag the AMI with this Key=Value (only for type=aws)") - uploadCmd.Flags().String("aws-boot-mode", "", "boot mode for the AMI: legacy-bios, uefi, uefi-preferred (only for type=aws)") - uploadCmd.Flags().String("libvirt-connection", "", "connection URI (only for type=libvirt)") - uploadCmd.Flags().String("libvirt-pool", "", "pool name (only for type=libvirt)") - uploadCmd.Flags().String("libvirt-volume", "", "volume name (only for type=libvirt)") - uploadCmd.Flags().String("openstack-image", "", "name for the uploaded image (only for type=openstack)") - uploadCmd.Flags().String("openstack-disk-format", "raw", "the disk format of a virtual machine image (only for type=openstack)") - uploadCmd.Flags().String("openstack-container-format", "bare", "this indicates if the image contains metadata about the VM (only for type=openstack)") - uploadCmd.Flags().String("ibmcloud-bucket", "", "target bucket name for storing the image (only for type=ibmcloud)") - uploadCmd.Flags().String("ibmcloud-region", "", "target region for IBM Cloud uploads (only for type=ibmcloud)") - uploadCmd.Flags().String("ibmcloud-image-name", "", "name for the uploaded image (only for type=ibmcloud)") - uploadCmd.Flags().String("azure-client-id", "", "Azure client ID (only for type=azure)") - uploadCmd.Flags().String("azure-client-secret", "", "Azure client secret (only for type=azure)") - uploadCmd.Flags().String("azure-tenant", "", "Azure tenant ID (only for type=azure)") - uploadCmd.Flags().String("azure-subscription", "", "Azure subscription ID (only for type=azure)") - uploadCmd.Flags().String("azure-resource-group", "", "Azure resource group (only for type=azure)") - uploadCmd.Flags().String("azure-image-name", "", "name for the uploaded image (only for type=azure)") - uploadCmd.Flags().String("arch", "", "upload for the given architecture") - uploadCmd.Flags().String("format", "", "output in a specific format (yaml, json)") - rootCmd.AddCommand(uploadCmd) - - buildCmd := &cobra.Command{ - Use: "build ", - Short: "Build the given image-type, e.g. qcow2 (tip: combine with --distro, --arch)", - RunE: cmdBuild, - SilenceUsage: true, - Args: cobra.ExactArgs(1), - } - buildCmd.Flags().AddFlagSet(manifestCmd.Flags()) - buildCmd.Flags().Bool("with-manifest", false, `export osbuild manifest`) - buildCmd.Flags().Bool("with-buildlog", false, `export osbuild buildlog`) - buildCmd.Flags().String("cache", defaultCacheDir(), `osbuild directory to cache intermediate build artifacts"`) - // XXX: add "--verbose" here, similar to how bib is doing this - // (see https://github.com/osbuild/bootc-image-builder/pull/790/commits/5cec7ffd8a526e2ca1e8ada0ea18f927695dfe43) - buildCmd.Flags().String("progress", "auto", "type of progress bar to use (e.g. verbose,term)") - buildCmd.Flags().Bool("with-metrics", false, `print timing information at the end of the build`) - buildCmd.Flags().String("output-name", "", "set specific output basename") - buildCmd.Flags().Bool("in-vm", false, `run the osbuild pipeline in a virtual machine`) - buildCmd.Flags().String("format", "", "Output in a specific format (json)") - rootCmd.AddCommand(buildCmd) - buildCmd.Flags().AddFlagSet(uploadCmd.Flags()) - // add after the rest of the uploadCmd flag set is added to avoid - // that build gets a "--to" parameter - uploadCmd.Flags().String("to", "", "upload to the given cloud") - - // XXX: add --format=json too? - describeImgCmd := &cobra.Command{ - Use: "describe ", - Short: "Describe the given image-type, e.g. qcow2 (tip: combine with --distro,--arch)", - RunE: cmdDescribeImg, - SilenceUsage: true, - Args: cobra.ExactArgs(1), - Hidden: false, - Aliases: []string{"describe-image"}, - } - describeImgCmd.Flags().String("arch", "", `use the different architecture`) - describeImgCmd.Flags().String("distro", "", `build manifest for a different distroname (e.g. centos-9)`) - describeImgCmd.Flags().Bool("in-vm", false, `run container in a virtual machine`) - - rootCmd.AddCommand(describeImgCmd) - addDocCmd(rootCmd) - - verbose, err := rootCmd.PersistentFlags().GetBool("verbose") - if err != nil { - return err - } - if verbose { - olog.SetDefault(log.New(os.Stderr, "", 0)) - ilog.SetDefault(log.New(os.Stderr, "", 0)) - } execErr := rootCmd.Execute() if flushErr := memProfileFlush(); flushErr != nil { diff --git a/cmd/image-builder/manpages.go b/cmd/image-builder/manpages.go deleted file mode 100644 index b6882f8c6a..0000000000 --- a/cmd/image-builder/manpages.go +++ /dev/null @@ -1,22 +0,0 @@ -package main - -import ( - "github.com/spf13/cobra" - "github.com/spf13/cobra/doc" -) - -func addDocCmd(rootCmd *cobra.Command) { - docCmd := &cobra.Command{ - Use: "doc ", - Short: "Generate man pages for this command", - Hidden: true, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - header := &doc.GenManHeader{ - Section: "1", - } - return doc.GenManTree(rootCmd, header, args[0]) - }, - } - rootCmd.AddCommand(docCmd) -}