From 154f943d014c0ad5e419fdea5ef09a4c0e39c2eb Mon Sep 17 00:00:00 2001 From: Avi Deitcher Date: Thu, 29 Dec 2022 10:31:57 +0200 Subject: [PATCH] switch from flags to cobra (#3884) Signed-off-by: Avi Deitcher Signed-off-by: Avi Deitcher --- docs/platform-hyperkit.md | 2 +- docs/platform-hyperv.md | 2 +- docs/platform-qemu.md | 6 +- docs/platform-virtualization-framework.md | 2 +- examples/docker-for-mac.md | 2 +- pkg/swap/README.md | 2 +- src/cmd/linuxkit/build.go | 387 ++-- src/cmd/linuxkit/cache.go | 56 +- src/cmd/linuxkit/cache_clean.go | 59 +- src/cmd/linuxkit/cache_export.go | 140 +- src/cmd/linuxkit/cache_ls.go | 40 +- src/cmd/linuxkit/cache_rm.go | 70 +- src/cmd/linuxkit/cmd.go | 75 + src/cmd/linuxkit/go.mod | 3 + src/cmd/linuxkit/go.sum | 5 + src/cmd/linuxkit/main.go | 106 +- src/cmd/linuxkit/metadata.go | 77 +- src/cmd/linuxkit/moby/linuxkit.go | 6 +- src/cmd/linuxkit/pkg.go | 120 +- src/cmd/linuxkit/pkg_build.go | 350 ++-- src/cmd/linuxkit/pkg_builder.go | 116 +- src/cmd/linuxkit/pkg_manifest.go | 79 +- src/cmd/linuxkit/pkg_push.go | 17 +- src/cmd/linuxkit/pkg_showtag.go | 50 +- src/cmd/linuxkit/pkglib/build.go | 2 +- src/cmd/linuxkit/pkglib/pkglib.go | 112 +- src/cmd/linuxkit/pkglib/pkglib_test.go | 132 +- src/cmd/linuxkit/push.go | 63 +- src/cmd/linuxkit/push_aws.go | 324 +-- src/cmd/linuxkit/push_azure.go | 75 +- src/cmd/linuxkit/push_gcp.go | 123 +- src/cmd/linuxkit/push_openstack.go | 68 +- src/cmd/linuxkit/push_packet.go | 129 +- src/cmd/linuxkit/push_scaleway.go | 217 +- src/cmd/linuxkit/push_vcenter.go | 106 +- src/cmd/linuxkit/run.go | 134 +- src/cmd/linuxkit/run_aws.go | 348 ++-- src/cmd/linuxkit/run_azure.go | 121 +- src/cmd/linuxkit/run_gcp.go | 151 +- src/cmd/linuxkit/run_hyperkit.go | 614 +++--- src/cmd/linuxkit/run_hyperv.go | 311 ++- src/cmd/linuxkit/run_openstack.go | 133 +- src/cmd/linuxkit/run_packet.go | 400 ++-- src/cmd/linuxkit/run_qemu.go | 421 ++-- src/cmd/linuxkit/run_scaleway.go | 148 +- src/cmd/linuxkit/run_vbox.go | 501 ++--- src/cmd/linuxkit/run_vcenter.go | 239 ++- .../linuxkit/run_virtualizationframework.go | 69 +- ...ualizationframework_darwin_cgo_disabled.go | 6 +- ...tualizationframework_darwin_cgo_enabled.go | 120 +- .../run_virtualizationframework_others.go | 13 + src/cmd/linuxkit/run_vmware.go | 262 ++- src/cmd/linuxkit/serve.go | 39 +- src/cmd/linuxkit/util.go | 12 + src/cmd/linuxkit/util/flags.go | 34 +- .../inconshreveable/mousetrap/LICENSE | 201 ++ .../inconshreveable/mousetrap/README.md | 23 + .../inconshreveable/mousetrap/trap_others.go | 15 + .../inconshreveable/mousetrap/trap_windows.go | 98 + .../mousetrap/trap_windows_1.4.go | 46 + .../github.com/mitchellh/go-ps/Vagrantfile | 86 +- .../vendor/github.com/spf13/cobra/.gitignore | 39 + .../github.com/spf13/cobra/.golangci.yml | 62 + .../vendor/github.com/spf13/cobra/.mailmap | 3 + .../vendor/github.com/spf13/cobra/CONDUCT.md | 37 + .../github.com/spf13/cobra/CONTRIBUTING.md | 50 + .../vendor/github.com/spf13/cobra/LICENSE.txt | 174 ++ .../vendor/github.com/spf13/cobra/MAINTAINERS | 13 + .../vendor/github.com/spf13/cobra/Makefile | 35 + .../vendor/github.com/spf13/cobra/README.md | 112 + .../github.com/spf13/cobra/active_help.go | 63 + .../github.com/spf13/cobra/active_help.md | 157 ++ .../vendor/github.com/spf13/cobra/args.go | 131 ++ .../spf13/cobra/bash_completions.go | 712 +++++++ .../spf13/cobra/bash_completions.md | 93 + .../spf13/cobra/bash_completionsV2.go | 383 ++++ .../vendor/github.com/spf13/cobra/cobra.go | 239 +++ .../vendor/github.com/spf13/cobra/command.go | 1810 +++++++++++++++++ .../github.com/spf13/cobra/command_notwin.go | 20 + .../github.com/spf13/cobra/command_win.go | 41 + .../github.com/spf13/cobra/completions.go | 871 ++++++++ .../spf13/cobra/fish_completions.go | 234 +++ .../spf13/cobra/fish_completions.md | 4 + .../github.com/spf13/cobra/flag_groups.go | 224 ++ .../spf13/cobra/powershell_completions.go | 310 +++ .../spf13/cobra/powershell_completions.md | 3 + .../spf13/cobra/projects_using_cobra.md | 60 + .../spf13/cobra/shell_completions.go | 98 + .../spf13/cobra/shell_completions.md | 568 ++++++ .../github.com/spf13/cobra/user_guide.md | 695 +++++++ .../github.com/spf13/cobra/zsh_completions.go | 301 +++ .../github.com/spf13/cobra/zsh_completions.md | 48 + .../vendor/github.com/spf13/pflag/.gitignore | 2 + .../vendor/github.com/spf13/pflag/.travis.yml | 22 + .../vendor/github.com/spf13/pflag/LICENSE | 28 + .../vendor/github.com/spf13/pflag/README.md | 296 +++ .../vendor/github.com/spf13/pflag/bool.go | 94 + .../github.com/spf13/pflag/bool_slice.go | 185 ++ .../vendor/github.com/spf13/pflag/bytes.go | 209 ++ .../vendor/github.com/spf13/pflag/count.go | 96 + .../vendor/github.com/spf13/pflag/duration.go | 86 + .../github.com/spf13/pflag/duration_slice.go | 166 ++ .../vendor/github.com/spf13/pflag/flag.go | 1239 +++++++++++ .../vendor/github.com/spf13/pflag/float32.go | 88 + .../github.com/spf13/pflag/float32_slice.go | 174 ++ .../vendor/github.com/spf13/pflag/float64.go | 84 + .../github.com/spf13/pflag/float64_slice.go | 166 ++ .../github.com/spf13/pflag/golangflag.go | 105 + .../vendor/github.com/spf13/pflag/int.go | 84 + .../vendor/github.com/spf13/pflag/int16.go | 88 + .../vendor/github.com/spf13/pflag/int32.go | 88 + .../github.com/spf13/pflag/int32_slice.go | 174 ++ .../vendor/github.com/spf13/pflag/int64.go | 84 + .../github.com/spf13/pflag/int64_slice.go | 166 ++ .../vendor/github.com/spf13/pflag/int8.go | 88 + .../github.com/spf13/pflag/int_slice.go | 158 ++ .../vendor/github.com/spf13/pflag/ip.go | 94 + .../vendor/github.com/spf13/pflag/ip_slice.go | 186 ++ .../vendor/github.com/spf13/pflag/ipmask.go | 122 ++ .../vendor/github.com/spf13/pflag/ipnet.go | 98 + .../vendor/github.com/spf13/pflag/string.go | 80 + .../github.com/spf13/pflag/string_array.go | 129 ++ .../github.com/spf13/pflag/string_slice.go | 163 ++ .../github.com/spf13/pflag/string_to_int.go | 149 ++ .../github.com/spf13/pflag/string_to_int64.go | 149 ++ .../spf13/pflag/string_to_string.go | 160 ++ .../vendor/github.com/spf13/pflag/uint.go | 88 + .../vendor/github.com/spf13/pflag/uint16.go | 88 + .../vendor/github.com/spf13/pflag/uint32.go | 88 + .../vendor/github.com/spf13/pflag/uint64.go | 88 + .../vendor/github.com/spf13/pflag/uint8.go | 88 + .../github.com/spf13/pflag/uint_slice.go | 168 ++ src/cmd/linuxkit/vendor/modules.txt | 9 + src/cmd/linuxkit/version.go | 27 + test/Makefile | 2 +- .../000_formats/000_kernel+initrd/test.sh | 2 +- .../000_formats/001_iso-bios/test.sh | 2 +- .../000_build/000_formats/002_iso-efi/test.sh | 2 +- .../000_build/000_formats/003_gcp/test.sh | 2 +- .../000_build/000_formats/004_aws/test.sh | 2 +- .../000_build/000_formats/006_vhd/test.sh | 2 +- .../000_build/000_formats/007_vmdk/test.sh | 2 +- .../000_formats/008_raw_bios/test.sh | 2 +- .../000_build/000_formats/009_raw_efi/test.sh | 2 +- .../000_formats/010_qcow2_bios/test.sh | 2 +- .../000_formats/011_kernel+squashfs/test.sh | 2 +- .../000_formats/012_kernel+iso/test.sh | 2 +- .../000_formats/013_iso-efi-initrd/test.sh | 2 +- .../010_reproducible/000_tar/test.sh | 4 +- .../002_kernel+initrd/test.sh | 4 +- test/cases/000_build/020_binds/test.sh | 4 +- .../000_qemu/000_run_kernel+initrd/test.sh | 4 +- .../000_qemu/005_run_kernel+squashfs/test.sh | 4 +- .../000_qemu/010_run_iso/test.sh | 4 +- .../000_qemu/020_run_efi/test.sh | 4 +- .../000_qemu/030_run_qcow_bios/test.sh | 2 +- .../000_qemu/040_run_raw_bios/test.sh | 2 +- .../000_qemu/050_run_aws/test.sh | 2 +- .../000_qemu/100_container/test.sh | 2 +- .../000_run_kernel+initrd/test.sh | 2 +- .../005_run_kernel+squashfs/test.exp | 2 +- .../005_run_kernel+squashfs/test.sh | 2 +- .../010_hyperkit/010_acpi/test.sh | 2 +- .../010_platforms/110_gcp/000_run/test.sh | 2 +- .../cases/020_kernel/011_config_5.4.x/test.sh | 2 +- .../020_kernel/013_config_5.10.x/test.sh | 2 +- .../020_kernel/016_config_5.15.x/test.sh | 2 +- test/cases/020_kernel/111_kmod_5.4.x/test.sh | 2 +- test/cases/020_kernel/113_kmod_5.10.x/test.sh | 2 +- test/cases/020_kernel/116_kmod_5.15.x/test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../017_echo-tcp-ipv4-long-5con-multi/test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../027_echo-tcp-ipv6-long-5con-multi/test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../037_echo-udp-ipv4-long-5con-multi/test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../test.sh | 2 +- .../047_echo-udp-ipv6-long-5con-multi/test.sh | 2 +- .../test.sh | 2 +- .../010_echo-short-1con-single/test.sh | 2 +- .../test.sh | 2 +- .../011_echo-short-10con-single/test.sh | 2 +- .../012_echo-short-5con-multi-reverse/test.sh | 2 +- .../012_echo-short-5con-multi/test.sh | 2 +- .../015_echo-long-1con-single-reverse/test.sh | 2 +- .../015_echo-long-1con-single/test.sh | 2 +- .../test.sh | 2 +- .../016_echo-long-10con-single/test.sh | 2 +- .../017_echo-long-5con-multi-reverse/test.sh | 2 +- .../017_echo-long-5con-multi/test.sh | 2 +- .../100_mix/010_veth-unix-domain-echo/test.sh | 2 +- .../011_veth-unix-domain-echo-reverse/test.sh | 2 +- .../100_mix/012_veth-ipv4-echo/test.sh | 2 +- .../100_mix/013_veth-ipv6-echo/test.sh | 2 +- .../100_mix/014_veth-tcp-echo/test.sh | 2 +- .../100_mix/015_veth-udp-echo/test.sh | 2 +- .../100_mix/020_unix-domain-echo/test.sh | 2 +- test/cases/030_security/010_ports/test.sh | 6 +- test/cases/040_packages/001_dummy/test.sh | 2 +- test/cases/040_packages/002_bcc/test.sh | 2 +- test/cases/040_packages/002_binfmt/test.sh | 2 +- test/cases/040_packages/002_bpftrace/test.sh | 2 +- .../040_packages/003_ca-certificates/test.sh | 2 +- test/cases/040_packages/003_cgroupv2/test.sh | 4 +- .../cases/040_packages/003_containerd/test.sh | 4 +- test/cases/040_packages/004_dhcpcd/test.sh | 2 +- .../004_dm-crypt/000_simple/test.sh | 4 +- .../004_dm-crypt/001_luks/test.sh | 4 +- .../040_packages/004_dm-crypt/002_key/test.sh | 4 +- .../040_packages/005_extend/000_ext4/test.sh | 8 +- .../040_packages/005_extend/001_btrfs/test.sh | 8 +- .../040_packages/005_extend/002_xfs/test.sh | 8 +- .../040_packages/005_extend/003_gpt/test.sh | 8 +- .../006_format_mount/000_auto/test.sh | 4 +- .../006_format_mount/001_by_label/test.sh | 4 +- .../006_format_mount/002_by_name/test.sh | 4 +- .../006_format_mount/003_btrfs/test.sh | 4 +- .../006_format_mount/004_xfs/test.sh | 4 +- .../005_by_device_force/test.sh | 4 +- .../006_format_mount/006_gpt/test.sh | 4 +- .../006_format_mount/010_multiple/test.sh | 4 +- .../040_packages/007_getty-containerd/test.sh | 2 +- .../040_packages/009_init_containerd/test.sh | 2 +- test/cases/040_packages/011_kmsg/test.sh | 2 +- test/cases/040_packages/012_logwrite/test.sh | 2 +- test/cases/040_packages/012_losetup/test.sh | 2 +- .../013_metadata/000_cidata/test.sh | 4 +- test/cases/040_packages/013_mkimage/test.sh | 6 +- test/cases/040_packages/019_sysctl/test.sh | 2 +- test/cases/040_packages/023_wireguard/test.sh | 2 +- test/pkg/Makefile | 2 +- 271 files changed, 17757 insertions(+), 3793 deletions(-) create mode 100644 src/cmd/linuxkit/cmd.go create mode 100644 src/cmd/linuxkit/run_virtualizationframework_others.go create mode 100644 src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/LICENSE create mode 100644 src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/README.md create mode 100644 src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/trap_others.go create mode 100644 src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/trap_windows.go create mode 100644 src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/trap_windows_1.4.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/.gitignore create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/.golangci.yml create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/.mailmap create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/CONDUCT.md create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/CONTRIBUTING.md create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/LICENSE.txt create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/MAINTAINERS create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/Makefile create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/README.md create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/active_help.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/active_help.md create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/args.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/bash_completions.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/bash_completions.md create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/bash_completionsV2.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/cobra.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/command.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/command_notwin.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/command_win.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/completions.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/fish_completions.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/fish_completions.md create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/flag_groups.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/powershell_completions.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/powershell_completions.md create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/projects_using_cobra.md create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/shell_completions.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/shell_completions.md create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/user_guide.md create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/zsh_completions.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/cobra/zsh_completions.md create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/.gitignore create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/.travis.yml create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/LICENSE create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/README.md create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/bool.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/bool_slice.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/bytes.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/count.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/duration.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/duration_slice.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/flag.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/float32.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/float32_slice.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/float64.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/float64_slice.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/golangflag.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/int.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/int16.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/int32.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/int32_slice.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/int64.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/int64_slice.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/int8.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/int_slice.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/ip.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/ip_slice.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/ipmask.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/ipnet.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/string.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/string_array.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/string_slice.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/string_to_int.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/string_to_int64.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/string_to_string.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/uint.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/uint16.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/uint32.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/uint64.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/uint8.go create mode 100644 src/cmd/linuxkit/vendor/github.com/spf13/pflag/uint_slice.go create mode 100644 src/cmd/linuxkit/version.go diff --git a/docs/platform-hyperkit.md b/docs/platform-hyperkit.md index f9153eb20..4f988dc84 100644 --- a/docs/platform-hyperkit.md +++ b/docs/platform-hyperkit.md @@ -20,7 +20,7 @@ The HyperKit backend currently supports booting: You need to select the boot method manually using the command line options. The default is `kernel+initrd`. `kernel+squashfs` can be selected using `-squashfs` and to boot a ISO with EFI you have to -specify `-iso -uefi`. +specify `--iso --uefi`. The `kernel+initrd` uses a RAM disk for the root filesystem. If you have RAM constraints or large images we recommend using either the diff --git a/docs/platform-hyperv.md b/docs/platform-hyperv.md index 83c606f50..5cde25d1b 100644 --- a/docs/platform-hyperv.md +++ b/docs/platform-hyperv.md @@ -8,7 +8,7 @@ manage the Hyper-V VMs. Example: ```sh -linuxkit.exe run -disk size=1 linuxkit-efi.iso +linuxkit.exe run --disk size=1 linuxkit-efi.iso ``` The Hyper-V VM, by default, is named after the prefix of the ISO, ie diff --git a/docs/platform-qemu.md b/docs/platform-qemu.md index cf8688f99..66ff6232a 100644 --- a/docs/platform-qemu.md +++ b/docs/platform-qemu.md @@ -24,9 +24,9 @@ specified with `-arch` and currently accepts `x86_64`, `aarch64`, and `linuxkit run qemu` can boot in different types of images: - `kernel+initrd`: This is the default mode of `linuxkit run qemu` [`x86_64`, `arm64`, `s390x`] -- `kernel+squashfs`: `linuxkit run qemu -squashfs `. This expects a kernel and a squashfs image. [`x86_64`, `arm64`, `s390x`] -- `iso-bios`: `linuxkit run qemu -iso ` [`x86_64`] -- `iso-efi`: `linuxkit run qemu -iso -uefi `. This looks in `/usr/share/ovmf/bios.bin` for the EFI firmware by default. Can be overwritten with `-fw`. [`x86_64`, `arm64`] +- `kernel+squashfs`: `linuxkit run qemu --squashfs `. This expects a kernel and a squashfs image. [`x86_64`, `arm64`, `s390x`] +- `iso-bios`: `linuxkit run qemu --iso ` [`x86_64`] +- `iso-efi`: `linuxkit run qemu --iso --uefi `. This looks in `/usr/share/ovmf/bios.bin` for the EFI firmware by default. Can be overwritten with `-fw`. [`x86_64`, `arm64`] - `qcow-bios`: `linuxkit run qemu disk.qcow2` [`x86_64`] - `raw-bios`: `linuxkit run qemu disk.img` [`x86_64`] - `aws`: `linuxkit run qemu disk.img` boots a raw AWS disk image. [`x86_64`] diff --git a/docs/platform-virtualization-framework.md b/docs/platform-virtualization-framework.md index 8b743b8fb..e979bd16e 100644 --- a/docs/platform-virtualization-framework.md +++ b/docs/platform-virtualization-framework.md @@ -21,7 +21,7 @@ The Virtualization.Framework backend currently supports booting: You need to select the boot method manually using the command line options. The default is `kernel+initrd`. `kernel+squashfs` can be selected using `-squashfs` and to boot a ISO with EFI you have to -specify `-iso -uefi`. +specify `--iso --uefi`. The `kernel+initrd` uses a RAM disk for the root filesystem. If you have RAM constraints or large images we recommend using either the diff --git a/examples/docker-for-mac.md b/examples/docker-for-mac.md index 7ae4f2565..d22fdefcb 100644 --- a/examples/docker-for-mac.md +++ b/examples/docker-for-mac.md @@ -16,7 +16,7 @@ $ linuxkit build -format iso-efi docker-for-mac.yml To run the VM with a 4G disk: ``` -linuxkit run hyperkit -networking=vpnkit -vsock-ports=2376 -disk size=4096M -data-file ./metadata.json -iso -uefi docker-for-mac-efi +linuxkit run hyperkit --networking=vpnkit --vsock-ports=2376 --disk size=4096M --data-file ./metadata.json --iso --uefi docker-for-mac-efi ``` Where the file `./metadata.json` should contain the desired docker daemon diff --git a/pkg/swap/README.md b/pkg/swap/README.md index f6d6acca2..bcfafb898 100644 --- a/pkg/swap/README.md +++ b/pkg/swap/README.md @@ -79,5 +79,5 @@ The sample command to run the enclosed is: ``` linuxkit build swap.yml -linuxkit run -disk size=4G swap +linuxkit run --disk size=4G swap ``` diff --git a/src/cmd/linuxkit/build.go b/src/cmd/linuxkit/build.go index d11d462dd..890a79953 100644 --- a/src/cmd/linuxkit/build.go +++ b/src/cmd/linuxkit/build.go @@ -2,7 +2,6 @@ package main import ( "bytes" - "flag" "fmt" "io" "net/http" @@ -13,6 +12,7 @@ import ( "github.com/linuxkit/linuxkit/src/cmd/linuxkit/moby" log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) const defaultNameForStdin = "moby" @@ -30,193 +30,200 @@ func (f *formatList) Set(value string) error { } return nil } - -// Process the build arguments and execute build -func build(args []string) { - var buildFormats formatList - - outputTypes := moby.OutputTypes() - - buildCmd := flag.NewFlagSet("build", flag.ExitOnError) - buildCmd.Usage = func() { - fmt.Printf("USAGE: %s build [options] [.yml] | -\n\n", os.Args[0]) - fmt.Printf("Options:\n") - buildCmd.PrintDefaults() - } - buildName := buildCmd.String("name", "", "Name to use for output files") - buildDir := buildCmd.String("dir", "", "Directory for output files, default current directory") - buildOutputFile := buildCmd.String("o", "", "File to use for a single output, or '-' for stdout") - buildSize := buildCmd.String("size", "1024M", "Size for output image, if supported and fixed size") - buildPull := buildCmd.Bool("pull", false, "Always pull images") - buildDocker := buildCmd.Bool("docker", false, "Check for images in docker before linuxkit cache") - buildDecompressKernel := buildCmd.Bool("decompress-kernel", false, "Decompress the Linux kernel (default false)") - buildCmd.Var(&buildFormats, "format", "Formats to create [ "+strings.Join(outputTypes, " ")+" ]") - buildArch := buildCmd.String("arch", runtime.GOARCH, "target architecture for which to build") - cacheDir := flagOverEnvVarOverDefaultString{def: defaultLinuxkitCache(), envVar: envVarCacheDir} - buildCmd.Var(&cacheDir, "cache", fmt.Sprintf("Directory for caching and finding cached image, overrides env var %s", envVarCacheDir)) - - if err := buildCmd.Parse(args); err != nil { - log.Fatal("Unable to parse args") - } - remArgs := buildCmd.Args() - - if len(remArgs) == 0 { - fmt.Println("Please specify a configuration file") - buildCmd.Usage() - os.Exit(1) - } - - name := *buildName - if name == "" { - conf := remArgs[len(remArgs)-1] - if conf == "-" { - name = defaultNameForStdin - } else { - name = strings.TrimSuffix(filepath.Base(conf), filepath.Ext(conf)) - } - } - - // There are two types of output, they will probably be split into "build" and "package" later - // the basic outputs are tarballs, while the packaged ones are the LinuxKit out formats that - // cannot be streamed but we do allow multiple ones to be built. - - if len(buildFormats) == 0 { - if *buildOutputFile == "" { - buildFormats = formatList{"kernel+initrd"} - } else { - buildFormats = formatList{"tar"} - } - } - - log.Debugf("Formats selected: %s", buildFormats.String()) - - if len(buildFormats) > 1 { - for _, o := range buildFormats { - if moby.Streamable(o) { - log.Fatalf("Format type %s must be the only format specified", o) - } - } - } - - if len(buildFormats) == 1 && moby.Streamable(buildFormats[0]) { - if *buildOutputFile == "" { - *buildOutputFile = filepath.Join(*buildDir, name+"."+buildFormats[0]) - // stop the errors in the validation below - *buildName = "" - *buildDir = "" - } - } else { - err := moby.ValidateFormats(buildFormats, cacheDir.String()) - if err != nil { - log.Errorf("Error parsing formats: %v", err) - buildCmd.Usage() - os.Exit(1) - } - } - - var outputFile *os.File - if *buildOutputFile != "" { - if len(buildFormats) > 1 { - log.Fatal("The -output option can only be specified when generating a single output format") - } - if *buildName != "" { - log.Fatal("The -output option cannot be specified with -name") - } - if *buildDir != "" { - log.Fatal("The -output option cannot be specified with -dir") - } - if !moby.Streamable(buildFormats[0]) { - log.Fatalf("The -output option cannot be specified for build type %s as it cannot be streamed", buildFormats[0]) - } - if *buildOutputFile == "-" { - outputFile = os.Stdout - } else { - var err error - outputFile, err = os.Create(*buildOutputFile) - if err != nil { - log.Fatalf("Cannot open output file: %v", err) - } - defer outputFile.Close() - } - } - - size, err := getDiskSizeMB(*buildSize) - if err != nil { - log.Fatalf("Unable to parse disk size: %v", err) - } - - var m moby.Moby - for _, arg := range remArgs { - var config []byte - if conf := arg; conf == "-" { - var err error - config, err = io.ReadAll(os.Stdin) - if err != nil { - log.Fatalf("Cannot read stdin: %v", err) - } - } else if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") { - buffer := new(bytes.Buffer) - response, err := http.Get(arg) - if err != nil { - log.Fatalf("Cannot fetch remote yaml file: %v", err) - } - defer response.Body.Close() - _, err = io.Copy(buffer, response.Body) - if err != nil { - log.Fatalf("Error reading http body: %v", err) - } - config = buffer.Bytes() - } else { - var err error - config, err = os.ReadFile(conf) - if err != nil { - log.Fatalf("Cannot open config file: %v", err) - } - } - - c, err := moby.NewConfig(config) - if err != nil { - log.Fatalf("Invalid config: %v", err) - } - m, err = moby.AppendConfig(m, c) - if err != nil { - log.Fatalf("Cannot append config files: %v", err) - } - } - - var tf *os.File - var w io.Writer - if outputFile != nil { - w = outputFile - } else { - if tf, err = os.CreateTemp("", ""); err != nil { - log.Fatalf("Error creating tempfile: %v", err) - } - defer os.Remove(tf.Name()) - w = tf - } - - // this is a weird interface, but currently only streamable types can have additional files - // need to split up the base tarball outputs from the secondary stages - var tp string - if moby.Streamable(buildFormats[0]) { - tp = buildFormats[0] - } - err = moby.Build(m, w, moby.BuildOpts{Pull: *buildPull, BuilderType: tp, DecompressKernel: *buildDecompressKernel, CacheDir: cacheDir.String(), DockerCache: *buildDocker, Arch: *buildArch}) - if err != nil { - log.Fatalf("%v", err) - } - - if outputFile == nil { - image := tf.Name() - if err := tf.Close(); err != nil { - log.Fatalf("Error closing tempfile: %v", err) - } - - log.Infof("Create outputs:") - err = moby.Formats(filepath.Join(*buildDir, name), image, buildFormats, size, cacheDir.String()) - if err != nil { - log.Fatalf("Error writing outputs: %v", err) - } - } +func (f *formatList) Type() string { + return "[]string" +} + +func buildCmd() *cobra.Command { + + var ( + name string + dir string + outputFile string + sizeString string + pull bool + docker bool + decompressKernel bool + arch string + cacheDir flagOverEnvVarOverDefaultString + buildFormats formatList + outputTypes = moby.OutputTypes() + ) + cmd := &cobra.Command{ + Use: "build", + Short: "Build a bootable OS image from a yaml configuration file", + Long: `Build a bootable OS image from a yaml configuration file. + +The generated image can be in one of multiple formats which can be run on various platforms. +`, + Example: ` linuxkit build [options] [.yml]`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if name == "" { + conf := args[len(args)-1] + if conf == "-" { + name = defaultNameForStdin + } else { + name = strings.TrimSuffix(filepath.Base(conf), filepath.Ext(conf)) + } + } + + // There are two types of output, they will probably be split into "build" and "package" later + // the basic outputs are tarballs, while the packaged ones are the LinuxKit out formats that + // cannot be streamed but we do allow multiple ones to be built. + + if len(buildFormats) == 0 { + if outputFile == "" { + buildFormats = formatList{"kernel+initrd"} + } else { + buildFormats = formatList{"tar"} + } + } + + log.Debugf("Formats selected: %s", buildFormats.String()) + + if len(buildFormats) > 1 { + for _, o := range buildFormats { + if moby.Streamable(o) { + return fmt.Errorf("Format type %s must be the only format specified", o) + } + } + } + + if len(buildFormats) == 1 && moby.Streamable(buildFormats[0]) { + if outputFile == "" { + outputFile = filepath.Join(dir, name+"."+buildFormats[0]) + // stop the errors in the validation below + name = "" + dir = "" + } + } else { + err := moby.ValidateFormats(buildFormats, cacheDir.String()) + if err != nil { + return fmt.Errorf("Error parsing formats: %v", err) + } + } + + var outfile *os.File + if outputFile != "" { + if len(buildFormats) > 1 { + return fmt.Errorf("The -output option can only be specified when generating a single output format") + } + if name != "" { + return fmt.Errorf("The -output option cannot be specified with -name") + } + if dir != "" { + return fmt.Errorf("The -output option cannot be specified with -dir") + } + if !moby.Streamable(buildFormats[0]) { + return fmt.Errorf("The -output option cannot be specified for build type %s as it cannot be streamed", buildFormats[0]) + } + if outputFile == "-" { + outfile = os.Stdout + } else { + var err error + outfile, err = os.Create(outputFile) + if err != nil { + log.Fatalf("Cannot open output file: %v", err) + } + defer outfile.Close() + } + } + + size, err := getDiskSizeMB(sizeString) + if err != nil { + log.Fatalf("Unable to parse disk size: %v", err) + } + + var m moby.Moby + for _, arg := range args { + var config []byte + if conf := arg; conf == "-" { + var err error + config, err = io.ReadAll(os.Stdin) + if err != nil { + return fmt.Errorf("Cannot read stdin: %v", err) + } + } else if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") { + buffer := new(bytes.Buffer) + response, err := http.Get(arg) + if err != nil { + return fmt.Errorf("Cannot fetch remote yaml file: %v", err) + } + defer response.Body.Close() + _, err = io.Copy(buffer, response.Body) + if err != nil { + return fmt.Errorf("Error reading http body: %v", err) + } + config = buffer.Bytes() + } else { + var err error + config, err = os.ReadFile(conf) + if err != nil { + return fmt.Errorf("Cannot open config file: %v", err) + } + } + + c, err := moby.NewConfig(config) + if err != nil { + return fmt.Errorf("Invalid config: %v", err) + } + m, err = moby.AppendConfig(m, c) + if err != nil { + return fmt.Errorf("Cannot append config files: %v", err) + } + } + + var tf *os.File + var w io.Writer + if outfile != nil { + w = outfile + } else { + if tf, err = os.CreateTemp("", ""); err != nil { + log.Fatalf("Error creating tempfile: %v", err) + } + defer os.Remove(tf.Name()) + w = tf + } + + // this is a weird interface, but currently only streamable types can have additional files + // need to split up the base tarball outputs from the secondary stages + var tp string + if moby.Streamable(buildFormats[0]) { + tp = buildFormats[0] + } + err = moby.Build(m, w, moby.BuildOpts{Pull: pull, BuilderType: tp, DecompressKernel: decompressKernel, CacheDir: cacheDir.String(), DockerCache: docker, Arch: arch}) + if err != nil { + return fmt.Errorf("%v", err) + } + + if outfile == nil { + image := tf.Name() + if err := tf.Close(); err != nil { + return fmt.Errorf("Error closing tempfile: %v", err) + } + + log.Infof("Create outputs:") + err = moby.Formats(filepath.Join(dir, name), image, buildFormats, size, cacheDir.String()) + if err != nil { + return fmt.Errorf("Error writing outputs: %v", err) + } + } + return nil + }, + } + + cmd.Flags().StringVar(&name, "name", "", "Name to use for output files") + cmd.Flags().StringVar(&dir, "dir", "", "Directory for output files, default current directory") + cmd.Flags().StringVar(&outputFile, "o", "", "File to use for a single output, or '-' for stdout") + cmd.Flags().StringVar(&sizeString, "size", "1024M", "Size for output image, if supported and fixed size") + cmd.Flags().BoolVar(&pull, "pull", false, "Always pull images") + cmd.Flags().BoolVar(&docker, "docker", false, "Check for images in docker before linuxkit cache") + cmd.Flags().BoolVar(&decompressKernel, "decompress-kernel", false, "Decompress the Linux kernel (default false)") + cmd.Flags().StringVar(&arch, "arch", runtime.GOARCH, "target architecture for which to build") + cmd.Flags().VarP(&buildFormats, "format", "f", "Formats to create [ "+strings.Join(outputTypes, " ")+" ]") + cacheDir = flagOverEnvVarOverDefaultString{def: defaultLinuxkitCache(), envVar: envVarCacheDir} + cmd.Flags().Var(&cacheDir, "cache", fmt.Sprintf("Directory for caching and finding cached image, overrides env var %s", envVarCacheDir)) + + return cmd } diff --git a/src/cmd/linuxkit/cache.go b/src/cmd/linuxkit/cache.go index 5195d06ae..26950d5f9 100644 --- a/src/cmd/linuxkit/cache.go +++ b/src/cmd/linuxkit/cache.go @@ -1,53 +1,29 @@ package main import ( - "fmt" - "os" "path/filepath" "github.com/linuxkit/linuxkit/src/cmd/linuxkit/util" - log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) -func cacheUsage() { - invoked := filepath.Base(os.Args[0]) - fmt.Printf("USAGE: %s cache command [options]\n\n", invoked) - fmt.Printf("Supported commands are\n") - // Please keep these in alphabetical order - fmt.Printf(" clean\n") - fmt.Printf(" export\n") - fmt.Printf(" ls\n") - fmt.Printf("\n") - fmt.Printf("'options' are the backend specific options.\n") - fmt.Printf("See '%s cache [command] --help' for details.\n\n", invoked) -} - -// Process the cache -func cache(args []string) { - if len(args) < 1 { - cacheUsage() - os.Exit(1) - } - switch args[0] { - // Please keep cases in alphabetical order - case "clean": - cacheClean(args[1:]) - case "rm": - cacheRm(args[1:]) - case "ls": - cacheList(args[1:]) - case "export": - cacheExport(args[1:]) - case "help", "-h", "-help", "--help": - cacheUsage() - os.Exit(0) - default: - log.Errorf("No 'cache' command specified.") - } -} - func defaultLinuxkitCache() string { lktDir := ".linuxkit" home := util.HomeDir() return filepath.Join(home, lktDir, "cache") } + +func cacheCmd() *cobra.Command { + + cmd := &cobra.Command{ + Use: "cache", + Short: "manage the linuxkit cache", + Long: `manage the linuxkit cache.`, + } + + cmd.AddCommand(cacheCleanCmd()) + cmd.AddCommand(cacheRmCmd()) + cmd.AddCommand(cacheLsCmd()) + cmd.AddCommand(cacheExportCmd()) + return cmd +} diff --git a/src/cmd/linuxkit/cache_clean.go b/src/cmd/linuxkit/cache_clean.go index a497f391c..e5ccd28c1 100644 --- a/src/cmd/linuxkit/cache_clean.go +++ b/src/cmd/linuxkit/cache_clean.go @@ -1,7 +1,6 @@ package main import ( - "flag" "fmt" "os" @@ -9,39 +8,45 @@ import ( "github.com/google/go-containerregistry/pkg/v1/remote" cachepkg "github.com/linuxkit/linuxkit/src/cmd/linuxkit/cache" log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) -func cacheClean(args []string) { - flags := flag.NewFlagSet("clean", flag.ExitOnError) +func cacheCleanCmd() *cobra.Command { + var ( + publishedOnly bool + ) + cmd := &cobra.Command{ + Use: "clean", + Short: "empty the linuxkit cache", + Long: `Empty the linuxkit cache.`, + RunE: func(cmd *cobra.Command, args []string) error { + // did we limit to published only? + if !publishedOnly { + if err := os.RemoveAll(cacheDir); err != nil { + return fmt.Errorf("Unable to clean cache %s: %v", cacheDir, err) + } + log.Infof("Cache emptied: %s", cacheDir) + return nil + } - cacheDir := flagOverEnvVarOverDefaultString{def: defaultLinuxkitCache(), envVar: envVarCacheDir} - flags.Var(&cacheDir, "cache", fmt.Sprintf("Directory for caching and finding cached image, overrides env var %s", envVarCacheDir)) - publishedOnly := flags.Bool("published-only", false, "Only clean images that linuxkit can confirm at the time of running have been published to the registry") + // list all of the images and content in the cache + p, err := cachepkg.NewProvider(cacheDir) + if err != nil { + return fmt.Errorf("unable to read a local cache: %v", err) + } + images, err := p.List() - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") + if err != nil { + return fmt.Errorf("error reading image names: %v", err) + } + removeImagesFromCache(images, p, publishedOnly) + return nil + }, } - // did we limit to published only? - if !*publishedOnly { - if err := os.RemoveAll(cacheDir.String()); err != nil { - log.Fatalf("Unable to clean cache %s: %v", cacheDir, err) - } - log.Infof("Cache emptied: %s", cacheDir) - return - } + cmd.Flags().BoolVar(&publishedOnly, "published-only", false, "Only clean images that linuxkit can confirm at the time of running have been published to the registry") - // list all of the images and content in the cache - p, err := cachepkg.NewProvider(cacheDir.String()) - if err != nil { - log.Fatalf("unable to read a local cache: %v", err) - } - images, err := p.List() - - if err != nil { - log.Fatalf("error reading image names: %v", err) - } - removeImagesFromCache(images, p, *publishedOnly) + return cmd } // removeImagesFromCache removes images from the cache. diff --git a/src/cmd/linuxkit/cache_export.go b/src/cmd/linuxkit/cache_export.go index 19eb095ff..0aab01f06 100644 --- a/src/cmd/linuxkit/cache_export.go +++ b/src/cmd/linuxkit/cache_export.go @@ -1,8 +1,6 @@ package main import ( - "flag" - "fmt" "io" "os" "runtime" @@ -11,77 +9,83 @@ import ( cachepkg "github.com/linuxkit/linuxkit/src/cmd/linuxkit/cache" "github.com/linuxkit/linuxkit/src/cmd/linuxkit/util" log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) -func cacheExport(args []string) { - fs := flag.NewFlagSet("export", flag.ExitOnError) +func cacheExportCmd() *cobra.Command { + var ( + arch string + outputFile string + format string + tagName string + ) + cmd := &cobra.Command{ + Use: "export", + Short: "export individual images from the linuxkit cache", + Long: `Export individual images from the linuxkit cache. Supports exporting into multiple formats.`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + names := args + name := names[0] + fullname := util.ReferenceExpand(name) - cacheDir := flagOverEnvVarOverDefaultString{def: defaultLinuxkitCache(), envVar: envVarCacheDir} - fs.Var(&cacheDir, "cache", fmt.Sprintf("Directory for caching and finding cached image, overrides env var %s", envVarCacheDir)) - arch := fs.String("arch", runtime.GOARCH, "Architecture to resolve an index to an image, if the provided image name is an index") - outfile := fs.String("outfile", "", "Path to file to save output, '-' for stdout") - format := fs.String("format", "oci", "export format, one of 'oci', 'filesystem'") - tagName := fs.String("name", "", "override the provided image name in the exported tar file; useful only for format=oci") + p, err := cachepkg.NewProvider(cacheDir) + if err != nil { + log.Fatalf("unable to read a local cache: %v", err) + } + ref, err := reference.Parse(fullname) + if err != nil { + log.Fatalf("invalid image name %s: %v", name, err) + } + desc, err := p.FindDescriptor(&ref) + if err != nil { + log.Fatalf("unable to find image named %s: %v", name, err) + } - if err := fs.Parse(args); err != nil { - log.Fatal("Unable to parse args") + src := p.NewSource(&ref, arch, desc) + var reader io.ReadCloser + switch format { + case "oci": + fullTagName := fullname + if tagName != "" { + fullTagName = util.ReferenceExpand(tagName) + } + reader, err = src.V1TarReader(fullTagName) + case "filesystem": + reader, err = src.TarReader() + default: + log.Fatalf("requested unknown format %s: %v", name, err) + } + if err != nil { + log.Fatalf("error getting reader for image %s: %v", name, err) + } + defer reader.Close() + + // try to write the output file + var w io.Writer + switch { + case outputFile == "": + log.Fatal("'outfile' flag is required") + case outputFile == "-": + w = os.Stdout + default: + f, err := os.OpenFile(outputFile, os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + log.Fatalf("unable to open %s: %v", outputFile, err) + } + defer f.Close() + w = f + } + + _, err = io.Copy(w, reader) + return err + }, } - // get the requested images - if fs.NArg() < 1 { - log.Fatal("At least one image name is required") - } - names := fs.Args() - name := names[0] - fullname := util.ReferenceExpand(name) + cmd.Flags().StringVar(&arch, "arch", runtime.GOARCH, "Architecture to resolve an index to an image, if the provided image name is an index") + cmd.Flags().StringVar(&outputFile, "outfile", "", "Path to file to save output, '-' for stdout") + cmd.Flags().StringVar(&format, "format", "oci", "export format, one of 'oci', 'filesystem'") + cmd.Flags().StringVar(&tagName, "name", "", "override the provided image name in the exported tar file; useful only for format=oci") - p, err := cachepkg.NewProvider(cacheDir.String()) - if err != nil { - log.Fatalf("unable to read a local cache: %v", err) - } - ref, err := reference.Parse(fullname) - if err != nil { - log.Fatalf("invalid image name %s: %v", name, err) - } - desc, err := p.FindDescriptor(&ref) - if err != nil { - log.Fatalf("unable to find image named %s: %v", name, err) - } - - src := p.NewSource(&ref, *arch, desc) - var reader io.ReadCloser - switch *format { - case "oci": - fullTagName := fullname - if *tagName != "" { - fullTagName = util.ReferenceExpand(*tagName) - } - reader, err = src.V1TarReader(fullTagName) - case "filesystem": - reader, err = src.TarReader() - default: - log.Fatalf("requested unknown format %s: %v", name, err) - } - if err != nil { - log.Fatalf("error getting reader for image %s: %v", name, err) - } - defer reader.Close() - - // try to write the output file - var w io.Writer - switch { - case outfile == nil, *outfile == "": - log.Fatal("'outfile' flag is required") - case *outfile == "-": - w = os.Stdout - default: - f, err := os.OpenFile(*outfile, os.O_CREATE|os.O_RDWR, 0644) - if err != nil { - log.Fatalf("unable to open %s: %v", *outfile, err) - } - defer f.Close() - w = f - } - - _, _ = io.Copy(w, reader) + return cmd } diff --git a/src/cmd/linuxkit/cache_ls.go b/src/cmd/linuxkit/cache_ls.go index ca043fa5f..926440c48 100644 --- a/src/cmd/linuxkit/cache_ls.go +++ b/src/cmd/linuxkit/cache_ls.go @@ -1,30 +1,28 @@ package main import ( - "flag" - "fmt" - cachepkg "github.com/linuxkit/linuxkit/src/cmd/linuxkit/cache" log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) -func cacheList(args []string) { - flags := flag.NewFlagSet("list", flag.ExitOnError) - - cacheDir := flagOverEnvVarOverDefaultString{def: defaultLinuxkitCache(), envVar: envVarCacheDir} - flags.Var(&cacheDir, "cache", fmt.Sprintf("Directory for caching and finding cached image, overrides env var %s", envVarCacheDir)) - - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") - } - - // list all of the images and content in the cache - images, err := cachepkg.ListImages(cacheDir.String()) - if err != nil { - log.Fatalf("error reading image names: %v", err) - } - log.Printf("%-80s %s", "image name", "root manifest hash") - for name, hash := range images { - log.Printf("%-80s %s", name, hash) +func cacheLsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "ls", + Short: "list images in the linuxkit cache", + Long: `List images in the linuxkit cache.`, + RunE: func(cmd *cobra.Command, args []string) error { + // list all of the images and content in the cache + images, err := cachepkg.ListImages(cacheDir) + if err != nil { + log.Fatalf("error reading image names: %v", err) + } + log.Printf("%-80s %s", "image name", "root manifest hash") + for name, hash := range images { + log.Printf("%-80s %s", name, hash) + } + return nil + }, } + return cmd } diff --git a/src/cmd/linuxkit/cache_rm.go b/src/cmd/linuxkit/cache_rm.go index a50e782ea..a36a95279 100644 --- a/src/cmd/linuxkit/cache_rm.go +++ b/src/cmd/linuxkit/cache_rm.go @@ -1,48 +1,48 @@ package main import ( - "flag" - "fmt" - cachepkg "github.com/linuxkit/linuxkit/src/cmd/linuxkit/cache" log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) -func cacheRm(args []string) { - flags := flag.NewFlagSet("rm", flag.ExitOnError) +func cacheRmCmd() *cobra.Command { + var ( + publishedOnly bool + ) + cmd := &cobra.Command{ + Use: "rm", + Short: "remove individual images from the linuxkit cache", + Long: `Remove individual images from the linuxkit cache.`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + imageNames := args - cacheDir := flagOverEnvVarOverDefaultString{def: defaultLinuxkitCache(), envVar: envVarCacheDir} - flags.Var(&cacheDir, "cache", fmt.Sprintf("Directory for caching and finding cached image, overrides env var %s", envVarCacheDir)) - publishedOnly := flags.Bool("published-only", false, "Only remove the specified images if linuxkit can confirm at the time of running have been published to the registry") + // did we limit to published only? - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") + // list all of the images and content in the cache + p, err := cachepkg.NewProvider(cacheDir) + if err != nil { + log.Fatalf("unable to read a local cache: %v", err) + } + images := map[string]string{} + for _, imageName := range imageNames { + desc, err := p.FindRoot(imageName) + if err != nil { + log.Fatalf("error reading image %s: %v", imageName, err) + } + dig, err := desc.Digest() + if err != nil { + log.Fatalf("error reading digest for image %s: %v", imageName, err) + } + images[imageName] = dig.String() + } + removeImagesFromCache(images, p, publishedOnly) + return nil + }, } - if flags.NArg() == 0 { - log.Fatal("Please specify at least one image to remove") - } + cmd.Flags().BoolVar(&publishedOnly, "published-only", false, "Only clean images that linuxkit can confirm at the time of running have been published to the registry") - imageNames := flags.Args() - - // did we limit to published only? - - // list all of the images and content in the cache - p, err := cachepkg.NewProvider(cacheDir.String()) - if err != nil { - log.Fatalf("unable to read a local cache: %v", err) - } - images := map[string]string{} - for _, imageName := range imageNames { - desc, err := p.FindRoot(imageName) - if err != nil { - log.Fatalf("error reading image %s: %v", imageName, err) - } - dig, err := desc.Digest() - if err != nil { - log.Fatalf("error reading digest for image %s: %v", imageName, err) - } - images[imageName] = dig.String() - } - removeImagesFromCache(images, p, *publishedOnly) + return cmd } diff --git a/src/cmd/linuxkit/cmd.go b/src/cmd/linuxkit/cmd.go new file mode 100644 index 000000000..8b7e9e9c8 --- /dev/null +++ b/src/cmd/linuxkit/cmd.go @@ -0,0 +1,75 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/linuxkit/linuxkit/src/cmd/linuxkit/util" + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" +) + +var ( + cacheDir string + // Config is the global tool configuration + Config = GlobalConfig{} +) + +// GlobalConfig is the global tool configuration +type GlobalConfig struct { + Pkg PkgConfig `yaml:"pkg"` +} + +// PkgConfig is the config specific to the `pkg` subcommand +type PkgConfig struct { +} + +func readConfig() { + cfgPath := filepath.Join(os.Getenv("HOME"), ".moby", "linuxkit", "config.yml") + cfgBytes, err := os.ReadFile(cfgPath) + if err != nil { + if os.IsNotExist(err) { + return + } + fmt.Printf("Failed to read %q\n", cfgPath) + os.Exit(1) + } + if err := yaml.Unmarshal(cfgBytes, &Config); err != nil { + fmt.Printf("Failed to parse %q\n", cfgPath) + os.Exit(1) + } +} + +func newCmd() *cobra.Command { + var ( + flagQuiet bool + flagVerbose bool + ) + cmd := &cobra.Command{ + Use: "linuxkit", + DisableAutoGenTag: true, + SilenceUsage: true, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + readConfig() + + // Set up logging + return util.SetupLogging(flagQuiet, flagVerbose) + }, + } + + cmd.AddCommand(buildCmd()) // apko login + cmd.AddCommand(cacheCmd()) + cmd.AddCommand(metadataCmd()) + cmd.AddCommand(pkgCmd()) + cmd.AddCommand(pushCmd()) + cmd.AddCommand(runCmd()) + cmd.AddCommand(serveCmd()) + cmd.AddCommand(versionCmd()) + + cmd.PersistentFlags().StringVar(&cacheDir, "cache", defaultLinuxkitCache(), fmt.Sprintf("Directory for caching and finding cached image, overrides env var %s", envVarCacheDir)) + cmd.PersistentFlags().BoolVarP(&flagQuiet, "quiet", "q", false, "Quiet execution") + cmd.PersistentFlags().BoolVarP(&flagVerbose, "verbose", "v", false, "Verbose execution") + + return cmd +} diff --git a/src/cmd/linuxkit/go.mod b/src/cmd/linuxkit/go.mod index 886a4a0cd..1580d827a 100644 --- a/src/cmd/linuxkit/go.mod +++ b/src/cmd/linuxkit/go.mod @@ -98,6 +98,7 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/in-toto/in-toto-golang v0.3.4-0.20220709202702-fa494aaa0add // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/klauspost/compress v1.15.12 // indirect github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3 // indirect @@ -117,6 +118,8 @@ require ( github.com/prometheus/procfs v0.8.0 // indirect github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect + github.com/spf13/cobra v1.6.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/tonistiigi/fsutil v0.0.0-20221114235510-0127568185cf // indirect github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f // indirect diff --git a/src/cmd/linuxkit/go.sum b/src/cmd/linuxkit/go.sum index a88da1c29..7a50fda79 100644 --- a/src/cmd/linuxkit/go.sum +++ b/src/cmd/linuxkit/go.sum @@ -467,6 +467,7 @@ github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwc github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/goselect v0.0.0-20180501195510-58854f77ee8d h1:6o8WW5zZ+Ny9sbk69epnAPmBzrBaRnvci+l4+pqleeY= github.com/creack/goselect v0.0.0-20180501195510-58854f77ee8d/go.mod h1:gHrIcH/9UZDn2qgeTUeW5K9eZsVYCH6/60J/FHysWyE= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= @@ -921,6 +922,8 @@ github.com/in-toto/in-toto-golang v0.3.4-0.20220709202702-fa494aaa0add h1:DAh7mH github.com/in-toto/in-toto-golang v0.3.4-0.20220709202702-fa494aaa0add/go.mod h1:DQI8vlV6h6qSY/tCOoYKtxjWrkyiNpJ3WTV/WoBllmQ= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ= github.com/ishidawataru/sctp v0.0.0-20191218070446-00ab2ac2db07/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg= github.com/ishidawataru/sctp v0.0.0-20210226210310-f2269e66cdee/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg= @@ -1358,6 +1361,8 @@ github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHN github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= diff --git a/src/cmd/linuxkit/main.go b/src/cmd/linuxkit/main.go index 64424aa6f..b13adc30d 100644 --- a/src/cmd/linuxkit/main.go +++ b/src/cmd/linuxkit/main.go @@ -1,111 +1,11 @@ package main import ( - "flag" - "fmt" - "os" - "path/filepath" - - "github.com/linuxkit/linuxkit/src/cmd/linuxkit/util" - "github.com/linuxkit/linuxkit/src/cmd/linuxkit/version" - - "gopkg.in/yaml.v2" + "log" ) -// GlobalConfig is the global tool configuration -type GlobalConfig struct { - Pkg PkgConfig `yaml:"pkg"` -} - -// PkgConfig is the config specific to the `pkg` subcommand -type PkgConfig struct { -} - -var ( - // Config is the global tool configuration - Config = GlobalConfig{} -) - -func printVersion() { - fmt.Printf("%s version %s\n", filepath.Base(os.Args[0]), version.Version) - if version.GitCommit != "" { - fmt.Printf("commit: %s\n", version.GitCommit) - } - os.Exit(0) -} - -func readConfig() { - cfgPath := filepath.Join(os.Getenv("HOME"), ".moby", "linuxkit", "config.yml") - cfgBytes, err := os.ReadFile(cfgPath) - if err != nil { - if os.IsNotExist(err) { - return - } - fmt.Printf("Failed to read %q\n", cfgPath) - os.Exit(1) - } - if err := yaml.Unmarshal(cfgBytes, &Config); err != nil { - fmt.Printf("Failed to parse %q\n", cfgPath) - os.Exit(1) - } -} - func main() { - flag.Usage = func() { - fmt.Printf("USAGE: %s [options] COMMAND\n\n", filepath.Base(os.Args[0])) - fmt.Printf("Commands:\n") - fmt.Printf(" build Build an image from a YAML file\n") - fmt.Printf(" cache Manage the local cache\n") - fmt.Printf(" metadata Metadata utilities\n") - fmt.Printf(" pkg Package building\n") - fmt.Printf(" push Push a VM image to a cloud or image store\n") - fmt.Printf(" run Run a VM image on a local hypervisor or remote cloud\n") - fmt.Printf(" serve Run a local http server (for iPXE booting)\n") - fmt.Printf(" version Print version information\n") - fmt.Printf(" help Print this message\n") - fmt.Printf("\n") - fmt.Printf("Run '%s COMMAND --help' for more information on the command\n", filepath.Base(os.Args[0])) - fmt.Printf("\n") - fmt.Printf("Options:\n") - flag.PrintDefaults() - } - - readConfig() - - // Set up logging - util.AddLoggingFlags(nil) - flag.Parse() - util.SetupLogging() - - args := flag.Args() - if len(args) < 1 { - fmt.Printf("Please specify a command.\n\n") - flag.Usage() - os.Exit(1) - } - - switch args[0] { - case "build": - build(args[1:]) - case "cache": - cache(args[1:]) - case "metadata": - metadata(args[1:]) - case "pkg": - pkg(args[1:]) - case "push": - push(args[1:]) - case "run": - run(args[1:]) - case "serve": - serve(args[1:]) - case "version": - printVersion() - case "help": - flag.Usage() - default: - fmt.Printf("%q is not valid command.\n\n", args[0]) - flag.Usage() - os.Exit(1) + if err := newCmd().Execute(); err != nil { + log.Fatalf("error during command execution: %v", err) } } diff --git a/src/cmd/linuxkit/metadata.go b/src/cmd/linuxkit/metadata.go index f456f83d5..9f8686dad 100644 --- a/src/cmd/linuxkit/metadata.go +++ b/src/cmd/linuxkit/metadata.go @@ -1,12 +1,10 @@ package main import ( - "fmt" "os" - "path/filepath" "github.com/rn/iso9660wrap" - log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) // WriteMetadataISO writes a metadata ISO file in a format usable by pkg/metadata @@ -20,58 +18,35 @@ func WriteMetadataISO(path string, content []byte) error { return iso9660wrap.WriteBuffer(outfh, content, "config") } -func metadataCreateUsage() { - invoked := filepath.Base(os.Args[0]) - fmt.Printf("USAGE: %s metadata create [file.iso] [metadata]\n\n", invoked) +func metadataCreateCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "create an ISO with metadata", + Long: `Create an ISO file with metadata in it. + Provided metadata will be written to '/config' in the ISO. + This is compatible with the linuxkit/metadata package.`, + Args: cobra.ExactArgs(2), + Example: "linuxkit metadata create file.iso \"metadata\"", + RunE: func(cmd *cobra.Command, args []string) error { + isoImage := args[0] + metadata := args[1] - fmt.Printf("'file.iso' is the file to create.\n") - fmt.Printf("'metadata' will be written to '/config' in the ISO.\n") - fmt.Printf("This is compatible with the linuxkit/metadata package\n") + return WriteMetadataISO(isoImage, []byte(metadata)) + }, + } + + return cmd } -func metadataCreate(args []string) { - if len(args) != 2 { - metadataCreateUsage() - os.Exit(1) - } - switch args[0] { - case "help", "-h", "-help", "--help": - metadataCreateUsage() - os.Exit(0) +func metadataCmd() *cobra.Command { + + cmd := &cobra.Command{ + Use: "metadata", + Short: "manage ISO metadata", + Long: `Manage ISO metadata.`, } - isoImage := args[0] - metadata := args[1] + cmd.AddCommand(metadataCreateCmd()) - if err := WriteMetadataISO(isoImage, []byte(metadata)); err != nil { - log.Fatal("Failed to write user data ISO: ", err) - } -} - -func metadataUsage() { - invoked := filepath.Base(os.Args[0]) - fmt.Printf("USAGE: %s metadata COMMAND [options]\n\n", invoked) - fmt.Printf("Commands:\n") - fmt.Printf(" create Create a metadata ISO\n") -} - -func metadata(args []string) { - if len(args) < 1 { - metadataUsage() - os.Exit(1) - } - switch args[0] { - case "help", "-h", "-help", "--help": - metadataUsage() - os.Exit(0) - } - - switch args[0] { - case "create": - metadataCreate(args[1:]) - default: - fmt.Printf("%q is not a valid metadata command.\n\n", args[0]) - metadataUsage() - os.Exit(1) - } + return cmd } diff --git a/src/cmd/linuxkit/moby/linuxkit.go b/src/cmd/linuxkit/moby/linuxkit.go index 9a373f7b4..dc674743c 100644 --- a/src/cmd/linuxkit/moby/linuxkit.go +++ b/src/cmd/linuxkit/moby/linuxkit.go @@ -127,9 +127,9 @@ func outputLinuxKit(format string, filename string, kernel []byte, initrd []byte } commandLine := []string{ "-q", "run", "qemu", - "-disk", fmt.Sprintf("%s,size=%s,format=%s", filename, sizeString, format), - "-disk", fmt.Sprintf("%s,format=raw", tardisk), - "-kernel", imageFilename("mkimage"), + "--disk", fmt.Sprintf("%s,size=%s,format=%s", filename, sizeString, format), + "--disk", fmt.Sprintf("%s,format=raw", tardisk), + "--kernel", imageFilename("mkimage"), } log.Debugf("run %s: %v", linuxkit, commandLine) cmd := exec.Command(linuxkit, commandLine...) diff --git a/src/cmd/linuxkit/pkg.go b/src/cmd/linuxkit/pkg.go index cacf2c3e3..1a1ee8680 100644 --- a/src/cmd/linuxkit/pkg.go +++ b/src/cmd/linuxkit/pkg.go @@ -1,46 +1,94 @@ package main import ( - "fmt" - "os" - "path/filepath" + "errors" + + "github.com/linuxkit/linuxkit/src/cmd/linuxkit/pkglib" + "github.com/spf13/cobra" ) -func pkgUsage() { - invoked := filepath.Base(os.Args[0]) - fmt.Printf("USAGE: %s pkg [subcommand] [options] [prefix]\n\n", invoked) +var pkglibConfig pkglib.PkglibConfig - fmt.Printf("'subcommand' is one of:\n") - fmt.Printf(" build\n") - fmt.Printf(" builder\n") - fmt.Printf(" push\n") - fmt.Printf(" show-tag\n") - fmt.Printf("\n") - fmt.Printf("'options' are the command specific options.\n") - fmt.Printf("See '%s pkg [command] --help' for details.\n\n", invoked) -} +func pkgCmd() *cobra.Command { + var ( + argDisableCache bool + argEnableCache bool + argNoNetwork bool + argNetwork bool + argOrg string + buildYML string + hash string + hashCommit string + hashPath string + dirty bool + devMode bool + ) -func pkg(args []string) { - if len(args) < 1 { - pkgUsage() - os.Exit(1) + cmd := &cobra.Command{ + Use: "pkg", + Short: "package building and pushing", + Long: `Package building and pushing.`, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + pkglibConfig = pkglib.PkglibConfig{ + BuildYML: buildYML, + Hash: hash, + HashCommit: hashCommit, + HashPath: hashPath, + Dirty: dirty, + Dev: devMode, + } + if cmd.Flags().Changed("disable-cache") && cmd.Flags().Changed("enable-cache") { + return errors.New("cannot set but disable-cache and enable-cache") + } + + if cmd.Flags().Changed("nonetwork") && cmd.Flags().Changed("network") { + return errors.New("cannot set but nonetwork and network") + } + + // these should be set only for overrides + if cmd.Flags().Changed("disable-cache") { + pkglibConfig.DisableCache = &argDisableCache + } + if cmd.Flags().Changed("enable-cache") { + val := !argEnableCache + pkglibConfig.DisableCache = &val + } + if cmd.Flags().Changed("nonetwork") { + val := !argNoNetwork + pkglibConfig.Network = &val + } + if cmd.Flags().Changed("network") { + pkglibConfig.Network = &argNetwork + } + if cmd.Flags().Changed("org") { + pkglibConfig.Org = &argOrg + } + + return nil + }, } - switch args[0] { - case "build": - pkgBuild(args[1:]) - case "builder": - pkgBuilder(args[1:]) - case "push": - pkgPush(args[1:]) - case "show-tag": - pkgShowTag(args[1:]) - case "manifest": - pkgManifest(args[1:]) - case "index": - pkgIndex(args[1:]) - default: - fmt.Printf("Unknown subcommand %q\n\n", args[0]) - pkgUsage() - } + cmd.AddCommand(pkgBuildCmd()) + cmd.AddCommand(pkgBuilderCmd()) + cmd.AddCommand(pkgPushCmd()) + cmd.AddCommand(pkgShowTagCmd()) + cmd.AddCommand(pkgManifestCmd()) + + // These override fields in pkgInfo default below, bools are in both forms to allow user overrides in either direction. + // These will apply to all packages built. + piBase := pkglib.NewPkgInfo() + cmd.PersistentFlags().BoolVar(&argDisableCache, "disable-cache", piBase.DisableCache, "Disable build cache") + cmd.PersistentFlags().BoolVar(&argEnableCache, "enable-cache", !piBase.DisableCache, "Enable build cache") + cmd.PersistentFlags().BoolVar(&argNoNetwork, "nonetwork", !piBase.Network, "Disallow network use during build") + cmd.PersistentFlags().BoolVar(&argNetwork, "network", piBase.Network, "Allow network use during build") + + cmd.PersistentFlags().StringVar(&argOrg, "org", piBase.Org, "Override the hub org") + cmd.PersistentFlags().StringVar(&buildYML, "build-yml", "build.yml", "Override the name of the yml file") + cmd.PersistentFlags().StringVar(&hash, "hash", "", "Override the image hash (default is to query git for the package's tree-sh)") + cmd.PersistentFlags().StringVar(&hashCommit, "hash-commit", "HEAD", "Override the git commit to use for the hash") + cmd.PersistentFlags().StringVar(&hashPath, "hash-path", "", "Override the directory to use for the image hash, must be a parent of the package dir (default is to use the package dir)") + cmd.PersistentFlags().BoolVar(&dirty, "force-dirty", false, "Force the pkg(s) to be considered dirty") + cmd.PersistentFlags().BoolVar(&devMode, "dev", false, "Force org and hash to $USER and \"dev\" respectively") + + return cmd } diff --git a/src/cmd/linuxkit/pkg_build.go b/src/cmd/linuxkit/pkg_build.go index 73b8e1ddd..fb604b2ed 100644 --- a/src/cmd/linuxkit/pkg_build.go +++ b/src/cmd/linuxkit/pkg_build.go @@ -1,14 +1,14 @@ package main import ( - "flag" + "errors" "fmt" "os" - "path/filepath" "strings" "github.com/linuxkit/linuxkit/src/cmd/linuxkit/pkglib" imagespec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/spf13/cobra" ) const ( @@ -17,199 +17,199 @@ const ( defaultBuilderImage = "moby/buildkit:v0.11.0-rc2" ) -func pkgBuild(args []string) { - pkgBuildPush(args, false) -} - -func pkgBuildPush(args []string, withPush bool) { - flags := flag.NewFlagSet("pkg build", flag.ExitOnError) - flags.Usage = func() { - invoked := filepath.Base(os.Args[0]) - name := "build" - if withPush { - name = "push" - } - fmt.Fprintf(os.Stderr, "USAGE: %s pkg %s [options] path\n\n", name, invoked) - fmt.Fprintf(os.Stderr, "'path' specifies the path to the package source directory.\n") - fmt.Fprintf(os.Stderr, "\n") - flags.PrintDefaults() - } - - force := flags.Bool("force", false, "Force rebuild even if image is in local cache") - pull := flags.Bool("pull", false, "Pull image if in registry but not in local cache; conflicts with --force") - ignoreCache := flags.Bool("ignore-cached", false, "Ignore cached intermediate images, always pulling from registry") - docker := flags.Bool("docker", false, "Store the built image in the docker image cache instead of the default linuxkit cache") - platforms := flags.String("platforms", "", "Which platforms to build for, defaults to all of those for which the package can be built") - skipPlatforms := flags.String("skip-platforms", "", "Platforms that should be skipped, even if present in build.yml") - builders := flags.String("builders", "", "Which builders to use for which platforms, e.g. linux/arm64=docker-context-arm64, overrides defaults and environment variables, see https://github.com/linuxkit/linuxkit/blob/master/docs/packages.md#Providing-native-builder-nodes") - builderImage := flags.String("builder-image", defaultBuilderImage, "buildkit builder container image to use") - builderRestart := flags.Bool("builder-restart", false, "force restarting builder, even if container with correct name and image exists") - cacheDir := flagOverEnvVarOverDefaultString{def: defaultLinuxkitCache(), envVar: envVarCacheDir} - flags.Var(&cacheDir, "cache", fmt.Sprintf("Directory for caching and finding cached image, overrides env var %s", envVarCacheDir)) - - // some logic clarification: - // pkg build - builds unless is in cache or published in registry - // pkg build --pull - builds unless is in cache or published in registry; pulls from registry if not in cache - // pkg build --force - always builds even if is in cache or published in registry - // pkg build --force --pull - always builds even if is in cache or published in registry; --pull ignored - // pkg push - always builds unless is in cache - // pkg push --force - always builds even if is in cache - // pkg push --nobuild - skips build; if not in cache, fails - // pkg push --nobuild --force - nonsensical +// some logic clarification: +// pkg build - builds unless is in cache or published in registry +// pkg build --pull - builds unless is in cache or published in registry; pulls from registry if not in cache +// pkg build --force - always builds even if is in cache or published in registry +// pkg build --force --pull - always builds even if is in cache or published in registry; --pull ignored +// pkg push - always builds unless is in cache +// pkg push --force - always builds even if is in cache +// pkg push --nobuild - skips build; if not in cache, fails +// pkg push --nobuild --force - nonsensical +// addCmdRunPkgBuildPush adds the RunE function and flags to a cobra.Command +// for "pkg build" or "pkg push". +func addCmdRunPkgBuildPush(cmd *cobra.Command, withPush bool) *cobra.Command { var ( - release *string - nobuild, manifest *bool - nobuildRef = false + force bool + pull bool + ignoreCache bool + docker bool + platforms string + skipPlatforms string + builders string + builderImage string + builderRestart bool + release string + nobuild bool + manifest bool + cacheDir = flagOverEnvVarOverDefaultString{def: defaultLinuxkitCache(), envVar: envVarCacheDir} ) - nobuild = &nobuildRef - if withPush { - release = flags.String("release", "", "Release the given version") - nobuild = flags.Bool("nobuild", false, "Skip building the image before pushing, conflicts with -force") - manifest = flags.Bool("manifest", true, "Create and push multi-arch manifest") - } - pkgs, err := pkglib.NewFromCLI(flags, args...) - if err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) - os.Exit(1) - } - - if *nobuild && *force { - fmt.Fprint(os.Stderr, "flags -force and -nobuild conflict") - os.Exit(1) - } - if *pull && *force { - fmt.Fprint(os.Stderr, "flags -force and -pull conflict") - os.Exit(1) - } - - var opts []pkglib.BuildOpt - if *force { - opts = append(opts, pkglib.WithBuildForce()) - } - if *ignoreCache { - opts = append(opts, pkglib.WithBuildIgnoreCache()) - } - if *pull { - opts = append(opts, pkglib.WithBuildPull()) - } - - opts = append(opts, pkglib.WithBuildCacheDir(cacheDir.String())) - - if withPush { - opts = append(opts, pkglib.WithBuildPush()) - if *nobuild { - opts = append(opts, pkglib.WithBuildSkip()) + cmd.RunE = func(cmd *cobra.Command, args []string) error { + pkgs, err := pkglib.NewFromConfig(pkglibConfig, args...) + if err != nil { + return err } - if *release != "" { - opts = append(opts, pkglib.WithRelease(*release)) - } - if *manifest { - opts = append(opts, pkglib.WithBuildManifest()) - } - } - if *docker { - opts = append(opts, pkglib.WithBuildTargetDockerCache()) - } - // skipPlatformsMap contains platforms that should be skipped - skipPlatformsMap := make(map[string]bool) - if *skipPlatforms != "" { - for _, platform := range strings.Split(*skipPlatforms, ",") { - parts := strings.SplitN(platform, "/", 2) - if len(parts) != 2 || parts[0] == "" || parts[0] != "linux" || parts[1] == "" { - fmt.Fprintf(os.Stderr, "invalid target platform specification '%s'\n", platform) - os.Exit(1) + if nobuild && force { + return errors.New("flags -force and -nobuild conflict") + } + if pull && force { + return errors.New("flags -force and -pull conflict") + } + + var opts []pkglib.BuildOpt + if force { + opts = append(opts, pkglib.WithBuildForce()) + } + if ignoreCache { + opts = append(opts, pkglib.WithBuildIgnoreCache()) + } + if pull { + opts = append(opts, pkglib.WithBuildPull()) + } + + opts = append(opts, pkglib.WithBuildCacheDir(cacheDir.String())) + + if withPush { + opts = append(opts, pkglib.WithBuildPush()) + if nobuild { + opts = append(opts, pkglib.WithBuildSkip()) } - skipPlatformsMap[strings.Trim(parts[1], " ")] = true - } - } - // if requested specific platforms, build those. If not, then we will - // retrieve the defaults in the loop over each package. - var plats []imagespec.Platform - // don't allow the use of --skip-platforms with --platforms - if *platforms != "" && *skipPlatforms != "" { - fmt.Fprintln(os.Stderr, "--skip-platforms and --platforms may not be used together") - os.Exit(1) - } - // process the platforms if provided - if *platforms != "" { - for _, p := range strings.Split(*platforms, ",") { - parts := strings.SplitN(p, "/", 2) - if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - fmt.Fprintf(os.Stderr, "invalid target platform specification '%s'\n", p) - os.Exit(1) + if release != "" { + opts = append(opts, pkglib.WithRelease(release)) + } + if manifest { + opts = append(opts, pkglib.WithBuildManifest()) } - plats = append(plats, imagespec.Platform{OS: parts[0], Architecture: parts[1]}) } - } + if docker { + opts = append(opts, pkglib.WithBuildTargetDockerCache()) + } - // build the builders map - buildersMap := map[string]string{} - // look for builders env var - buildersMap, err = buildPlatformBuildersMap(os.Getenv(buildersEnvVar), buildersMap) - if err != nil { - fmt.Fprintf(os.Stderr, "%s in environment variable %s\n", err.Error(), buildersEnvVar) - os.Exit(1) - } - // any CLI options override env var - buildersMap, err = buildPlatformBuildersMap(*builders, buildersMap) - if err != nil { - fmt.Fprintf(os.Stderr, "%s in --builders flag\n", err.Error()) - os.Exit(1) - } - opts = append(opts, pkglib.WithBuildBuilders(buildersMap)) - opts = append(opts, pkglib.WithBuildBuilderImage(*builderImage)) - opts = append(opts, pkglib.WithBuildBuilderRestart(*builderRestart)) - - for _, p := range pkgs { - // things we need our own copies of - var ( - pkgOpts = make([]pkglib.BuildOpt, len(opts)) - pkgPlats = make([]imagespec.Platform, len(plats)) - ) - copy(pkgOpts, opts) - copy(pkgPlats, plats) - // unless overridden, platforms are specific to a package, so this needs to be inside the for loop - if len(pkgPlats) == 0 { - for _, a := range p.Arches() { - if _, ok := skipPlatformsMap[a]; ok { - continue + // skipPlatformsMap contains platforms that should be skipped + skipPlatformsMap := make(map[string]bool) + if skipPlatforms != "" { + for _, platform := range strings.Split(skipPlatforms, ",") { + parts := strings.SplitN(platform, "/", 2) + if len(parts) != 2 || parts[0] == "" || parts[0] != "linux" || parts[1] == "" { + return fmt.Errorf("invalid target platform specification '%s'\n", platform) } - pkgPlats = append(pkgPlats, imagespec.Platform{OS: "linux", Architecture: a}) + skipPlatformsMap[strings.Trim(parts[1], " ")] = true + } + } + // if requested specific platforms, build those. If not, then we will + // retrieve the defaults in the loop over each package. + var plats []imagespec.Platform + // don't allow the use of --skip-platforms with --platforms + if platforms != "" && skipPlatforms != "" { + return errors.New("--skip-platforms and --platforms may not be used together") + } + // process the platforms if provided + if platforms != "" { + for _, p := range strings.Split(platforms, ",") { + parts := strings.SplitN(p, "/", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + fmt.Fprintf(os.Stderr, "invalid target platform specification '%s'\n", p) + os.Exit(1) + } + plats = append(plats, imagespec.Platform{OS: parts[0], Architecture: parts[1]}) } } - // if there are no platforms to build for, do nothing. - // note that this is *not* an error; we simply skip it - if len(pkgPlats) == 0 { - fmt.Printf("Skipping %s with no architectures to build\n", p.Tag()) - continue + // build the builders map + buildersMap := map[string]string{} + // look for builders env var + buildersMap, err = buildPlatformBuildersMap(os.Getenv(buildersEnvVar), buildersMap) + if err != nil { + return fmt.Errorf("error in environment variable %s: %w\n", buildersEnvVar, err) } - - pkgOpts = append(pkgOpts, pkglib.WithBuildPlatforms(pkgPlats...)) - - var msg, action string - switch { - case !withPush: - msg = fmt.Sprintf("Building %q", p.Tag()) - action = "building" - case *nobuild: - msg = fmt.Sprintf("Pushing %q without building", p.Tag()) - action = "building and pushing" - default: - msg = fmt.Sprintf("Building and pushing %q", p.Tag()) - action = "building and pushing" + // any CLI options override env var + buildersMap, err = buildPlatformBuildersMap(builders, buildersMap) + if err != nil { + return fmt.Errorf("error in --builders flag: %w\n", err) } + opts = append(opts, pkglib.WithBuildBuilders(buildersMap)) + opts = append(opts, pkglib.WithBuildBuilderImage(builderImage)) + opts = append(opts, pkglib.WithBuildBuilderRestart(builderRestart)) - fmt.Println(msg) + for _, p := range pkgs { + // things we need our own copies of + var ( + pkgOpts = make([]pkglib.BuildOpt, len(opts)) + pkgPlats = make([]imagespec.Platform, len(plats)) + ) + copy(pkgOpts, opts) + copy(pkgPlats, plats) + // unless overridden, platforms are specific to a package, so this needs to be inside the for loop + if len(pkgPlats) == 0 { + for _, a := range p.Arches() { + if _, ok := skipPlatformsMap[a]; ok { + continue + } + pkgPlats = append(pkgPlats, imagespec.Platform{OS: "linux", Architecture: a}) + } + } - if err := p.Build(pkgOpts...); err != nil { - fmt.Fprintf(os.Stderr, "Error %s %q: %v\n", action, p.Tag(), err) - os.Exit(1) + // if there are no platforms to build for, do nothing. + // note that this is *not* an error; we simply skip it + if len(pkgPlats) == 0 { + fmt.Printf("Skipping %s with no architectures to build\n", p.Tag()) + continue + } + + pkgOpts = append(pkgOpts, pkglib.WithBuildPlatforms(pkgPlats...)) + + var msg, action string + switch { + case !withPush: + msg = fmt.Sprintf("Building %q", p.Tag()) + action = "building" + case nobuild: + msg = fmt.Sprintf("Pushing %q without building", p.Tag()) + action = "building and pushing" + default: + msg = fmt.Sprintf("Building and pushing %q", p.Tag()) + action = "building and pushing" + } + + fmt.Println(msg) + + if err := p.Build(pkgOpts...); err != nil { + return fmt.Errorf("error %s %q: %w", action, p.Tag(), err) + } } + return nil } + + cmd.Flags().BoolVar(&force, "force", false, "Force rebuild even if image is in local cache") + cmd.Flags().BoolVar(&pull, "pull", false, "Pull image if in registry but not in local cache; conflicts with --force") + cmd.Flags().BoolVar(&ignoreCache, "ignore-cached", false, "Ignore cached intermediate images, always pulling from registry") + cmd.Flags().BoolVar(&docker, "docker", false, "Store the built image in the docker image cache instead of the default linuxkit cache") + cmd.Flags().StringVar(&platforms, "platforms", "", "Which platforms to build for, defaults to all of those for which the package can be built") + cmd.Flags().StringVar(&skipPlatforms, "skip-platforms", "", "Platforms that should be skipped, even if present in build.yml") + cmd.Flags().StringVar(&builders, "builders", "", "Which builders to use for which platforms, e.g. linux/arm64=docker-context-arm64, overrides defaults and environment variables, see https://github.com/linuxkit/linuxkit/blob/master/docs/packages.md#Providing-native-builder-nodes") + cmd.Flags().StringVar(&builderImage, "builder-image", defaultBuilderImage, "buildkit builder container image to use") + cmd.Flags().BoolVar(&builderRestart, "builder-restart", false, "force restarting builder, even if container with correct name and image exists") + cmd.Flags().Var(&cacheDir, "cache", fmt.Sprintf("Directory for caching and finding cached image, overrides env var %s", envVarCacheDir)) + cmd.Flags().StringVar(&release, "release", "", "Release the given version") + cmd.Flags().BoolVar(&nobuild, "nobuild", false, "Skip building the image before pushing, conflicts with -force") + cmd.Flags().BoolVar(&manifest, "manifest", true, "Create and push multi-arch manifest") + + return cmd +} +func pkgBuildCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "build", + Short: "build an OCI package from a directory with a yaml configuration file", + Long: `Build an OCI package from a directory with a yaml configuration file. + 'path' specifies the path to the package source directory. +`, + Example: ` linuxkit pkg build [options] pkg/dir/`, + Args: cobra.MinimumNArgs(1), + } + return addCmdRunPkgBuildPush(cmd, false) } func buildPlatformBuildersMap(inputs string, existing map[string]string) (map[string]string, error) { diff --git a/src/cmd/linuxkit/pkg_builder.go b/src/cmd/linuxkit/pkg_builder.go index 6abb6db23..5eb7f3f09 100644 --- a/src/cmd/linuxkit/pkg_builder.go +++ b/src/cmd/linuxkit/pkg_builder.go @@ -1,84 +1,62 @@ package main import ( - "flag" "fmt" "os" - "path/filepath" "runtime" "strings" "github.com/linuxkit/linuxkit/src/cmd/linuxkit/pkglib" - log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) -func pkgBuilderUsage() { - invoked := filepath.Base(os.Args[0]) - fmt.Printf("USAGE: %s builder command [options]\n\n", invoked) - fmt.Printf("Supported commands are\n") - // Please keep these in alphabetical order - fmt.Printf(" du\n") - fmt.Printf(" prune\n") - fmt.Printf("\n") - fmt.Printf("'options' are the backend specific options.\n") - fmt.Printf("See '%s builder [command] --help' for details.\n\n", invoked) -} +func pkgBuilderCmd() *cobra.Command { + var ( + builders string + platforms string + builderImage string + ) + cmd := &cobra.Command{ + Use: "builder", + Short: "manage the pkg builder", + Long: `Manage the pkg builder. This normally is a container.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + command := args[0] + verbose := cmd.Flags().Lookup("verbose").Value.String() == "true" + // build the builders map + buildersMap := make(map[string]string) + // look for builders env var + buildersMap, err := buildPlatformBuildersMap(os.Getenv(buildersEnvVar), buildersMap) + if err != nil { + return fmt.Errorf("invalid environment variable %s: %w", buildersEnvVar, err) + } + // any CLI options override env var + buildersMap, err = buildPlatformBuildersMap(builders, buildersMap) + if err != nil { + return fmt.Errorf("invalid --builders flag: %w", err) + } -// Process the builder -func pkgBuilder(args []string) { - if len(args) < 1 { - pkgBuilderUsage() - os.Exit(1) - } - switch args[0] { - // Please keep cases in alphabetical order - case "du": - pkgBuilderCommands(args[0], args[1:]) - case "prune": - pkgBuilderCommands(args[0], args[1:]) - case "help", "-h", "-help", "--help": - pkgBuilderUsage() - os.Exit(0) - default: - log.Errorf("No 'builder' command specified.") - } -} - -func pkgBuilderCommands(command string, args []string) { - flags := flag.NewFlagSet(command, flag.ExitOnError) - builders := flags.String("builders", "", "Which builders to use for which platforms, e.g. linux/arm64=docker-context-arm64, overrides defaults and environment variables, see https://github.com/linuxkit/linuxkit/blob/master/docs/packages.md#Providing-native-builder-nodes") - platforms := flags.String("platforms", fmt.Sprintf("linux/%s", runtime.GOARCH), "Which platforms we built images for") - builderImage := flags.String("builder-image", defaultBuilderImage, "buildkit builder container image to use") - verbose := flags.Bool("v", false, "Verbose output") - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") - } - // build the builders map - buildersMap := make(map[string]string) - // look for builders env var - buildersMap, err := buildPlatformBuildersMap(os.Getenv(buildersEnvVar), buildersMap) - if err != nil { - log.Fatalf("%s in environment variable %s\n", err.Error(), buildersEnvVar) - } - // any CLI options override env var - buildersMap, err = buildPlatformBuildersMap(*builders, buildersMap) - if err != nil { - log.Fatalf("%s in --builders flag\n", err.Error()) + platformsToClean := strings.Split(platforms, ",") + switch command { + case "du": + if err := pkglib.DiskUsage(buildersMap, builderImage, platformsToClean, verbose); err != nil { + return fmt.Errorf("Unable to print disk usage of builder: %w", err) + } + case "prune": + if err := pkglib.PruneBuilder(buildersMap, builderImage, platformsToClean, verbose); err != nil { + return fmt.Errorf("Unable to prune builder: %w", err) + } + default: + return fmt.Errorf("unexpected command %s", command) + } + return nil + }, } - platformsToClean := strings.Split(*platforms, ",") - switch command { - case "du": - if err := pkglib.DiskUsage(buildersMap, *builderImage, platformsToClean, *verbose); err != nil { - log.Fatalf("Unable to print disk usage of builder: %v", err) - } - case "prune": - if err := pkglib.PruneBuilder(buildersMap, *builderImage, platformsToClean, *verbose); err != nil { - log.Fatalf("Unable to prune builder: %v", err) - } - default: - log.Errorf("unexpected command %s", command) - pkgBuilderUsage() - os.Exit(1) - } + cmd.PersistentFlags().StringVar(&builders, "builders", "", "Which builders to use for which platforms, e.g. linux/arm64=docker-context-arm64, overrides defaults and environment variables, see https://github.com/linuxkit/linuxkit/blob/master/docs/packages.md#Providing-native-builder-nodes") + cmd.PersistentFlags().StringVar(&platforms, "platforms", fmt.Sprintf("linux/%s", runtime.GOARCH), "Which platforms we built images for") + cmd.PersistentFlags().StringVar(&builderImage, "builder-image", defaultBuilderImage, "buildkit builder container image to use") + + return cmd } diff --git a/src/cmd/linuxkit/pkg_manifest.go b/src/cmd/linuxkit/pkg_manifest.go index 7f43f24a9..87d0ea374 100644 --- a/src/cmd/linuxkit/pkg_manifest.go +++ b/src/cmd/linuxkit/pkg_manifest.go @@ -1,50 +1,47 @@ package main import ( - "flag" "fmt" - "os" - "path/filepath" "github.com/linuxkit/linuxkit/src/cmd/linuxkit/pkglib" + "github.com/spf13/cobra" ) -func pkgManifest(args []string) { - pkgIndex(args) -} -func pkgIndex(args []string) { - flags := flag.NewFlagSet("pkg manifest", flag.ExitOnError) - flags.Usage = func() { - invoked := filepath.Base(os.Args[0]) - name := "manifest" - fmt.Fprintf(os.Stderr, "USAGE: %s pkg %s [options] path\n\n", name, invoked) - fmt.Fprintf(os.Stderr, "'path' specifies the path to the package source directory.\n") - fmt.Fprintf(os.Stderr, "\n") - fmt.Fprintf(os.Stderr, "Updates the manifest in the registry for the given path based on all known platforms. If none found, no manifest created.\n") - flags.PrintDefaults() - } - release := flags.String("release", "", "Release the given version") - - pkgs, err := pkglib.NewFromCLI(flags, args...) - if err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) - os.Exit(1) - } - - var opts []pkglib.BuildOpt - if *release != "" { - opts = append(opts, pkglib.WithRelease(*release)) - } - - for _, p := range pkgs { - msg := fmt.Sprintf("Updating index for %q", p.Tag()) - action := "building and pushing" - - fmt.Println(msg) - - if err := p.Index(opts...); err != nil { - fmt.Fprintf(os.Stderr, "Error %s %q: %v\n", action, p.Tag(), err) - os.Exit(1) - } - } +func pkgManifestCmd() *cobra.Command { + var release string + cmd := &cobra.Command{ + Use: "manifest", + Short: "update manifest in the registry for the given path based on all known platforms", + Long: `Updates the manifest in the registry for the given path based on all known platforms. If none found, no manifest created. + 'path' specifies the path to the package source directory. +`, + Args: cobra.ExactArgs(1), + Aliases: []string{"index"}, + RunE: func(cmd *cobra.Command, args []string) error { + pkgs, err := pkglib.NewFromConfig(pkglibConfig, args...) + if err != nil { + return err + } + + var opts []pkglib.BuildOpt + if release != "" { + opts = append(opts, pkglib.WithRelease(release)) + } + + for _, p := range pkgs { + msg := fmt.Sprintf("Updating index for %q", p.Tag()) + action := "building and pushing" + + fmt.Println(msg) + + if err := p.Index(opts...); err != nil { + return fmt.Errorf("error %s %q: %w", action, p.Tag(), err) + } + } + return nil + }, + } + cmd.Flags().StringVar(&release, "release", "", "Release the given version") + + return cmd } diff --git a/src/cmd/linuxkit/pkg_push.go b/src/cmd/linuxkit/pkg_push.go index 8ce24a13f..3274b1f89 100644 --- a/src/cmd/linuxkit/pkg_push.go +++ b/src/cmd/linuxkit/pkg_push.go @@ -1,5 +1,18 @@ package main -func pkgPush(args []string) { - pkgBuildPush(args, true) +import "github.com/spf13/cobra" + +func pkgPushCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "push", + Short: "build and push an OCI package from a directory with a yaml configuration file", + Long: `Build and push an OCI package from a directory with a yaml configuration file. + 'path' specifies the path to the package source directory. + + The package may or may not be built first, depending on options +`, + Example: ` linuxkit pkg push [options] pkg/dir/`, + Args: cobra.MinimumNArgs(1), + } + return addCmdRunPkgBuildPush(cmd, true) } diff --git a/src/cmd/linuxkit/pkg_showtag.go b/src/cmd/linuxkit/pkg_showtag.go index 1168409c1..59e0c4a0b 100644 --- a/src/cmd/linuxkit/pkg_showtag.go +++ b/src/cmd/linuxkit/pkg_showtag.go @@ -1,35 +1,37 @@ package main import ( - "flag" "fmt" - "os" - "path/filepath" "github.com/linuxkit/linuxkit/src/cmd/linuxkit/pkglib" + "github.com/spf13/cobra" ) -func pkgShowTag(args []string) { - flags := flag.NewFlagSet("pkg show-tag", flag.ExitOnError) - flags.Usage = func() { - invoked := filepath.Base(os.Args[0]) - fmt.Fprintf(os.Stderr, "USAGE: %s pkg show-tag [options] path\n\n", invoked) - fmt.Fprintf(os.Stderr, "'path' specifies the path to the package source directory.\n") - fmt.Fprintf(os.Stderr, "\n") - flags.PrintDefaults() +func pkgShowTagCmd() *cobra.Command { + var canonical bool + cmd := &cobra.Command{ + Use: "show-tag", + Short: "show the tag for a package based on its source directory", + Long: `Show the tag for a package based on its source directory. + 'path' specifies the path to the package source directory. +`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + pkgs, err := pkglib.NewFromConfig(pkglibConfig, args...) + if err != nil { + return err + } + for _, p := range pkgs { + tag := p.Tag() + if canonical { + tag = p.FullTag() + } + fmt.Println(tag) + } + return nil + }, } - canonical := flags.Bool("canonical", false, "Show canonical name, e.g. docker.io/linuxkit/foo:1234, instead of the default, e.g. linuxkit/foo:1234") + cmd.Flags().BoolVar(&canonical, "canonical", false, "Show canonical name, e.g. docker.io/linuxkit/foo:1234, instead of the default, e.g. linuxkit/foo:1234") - pkgs, err := pkglib.NewFromCLI(flags, args...) - if err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) - os.Exit(1) - } - for _, p := range pkgs { - tag := p.Tag() - if *canonical { - tag = p.FullTag() - } - fmt.Println(tag) - } + return cmd } diff --git a/src/cmd/linuxkit/pkglib/build.go b/src/cmd/linuxkit/pkglib/build.go index fdc767744..4537e24ba 100644 --- a/src/cmd/linuxkit/pkglib/build.go +++ b/src/cmd/linuxkit/pkglib/build.go @@ -564,7 +564,7 @@ type buildCtx struct { // Reader gets an io.Reader by iterating over the sources, tarring up the content after rewriting the paths. // It assumes that sources is sane, ie is well formed and the first part is an absolute path -// and that it exists. NewFromCLI() ensures that. +// and that it exists. func (c *buildCtx) Reader() io.ReadCloser { r, w := io.Pipe() tw := tar.NewWriter(w) diff --git a/src/cmd/linuxkit/pkglib/pkglib.go b/src/cmd/linuxkit/pkglib/pkglib.go index d9564254e..12cf90247 100644 --- a/src/cmd/linuxkit/pkglib/pkglib.go +++ b/src/cmd/linuxkit/pkglib/pkglib.go @@ -2,7 +2,6 @@ package pkglib import ( "crypto/sha1" - "flag" "fmt" "os" "path" @@ -36,6 +35,32 @@ type pkgInfo struct { } `yaml:"depends"` } +// PkglibConfig contains the configuration for the pkglib package. +// It is used to override the default behaviour of the package. +// Fields that are pointers are so that the caller can leave it as nil +// for "use whatever default pkglib has", while non-nil means "explicitly override". +type PkglibConfig struct { + DisableCache *bool + Network *bool + Org *string + BuildYML string + Hash string + HashCommit string + HashPath string + Dirty bool + Dev bool +} + +func NewPkgInfo() pkgInfo { + return pkgInfo{ + Org: "linuxkit", + Arches: []string{"amd64", "arm64", "s390x"}, + GitRepo: "https://github.com/linuxkit/linuxkit", + Network: false, + DisableCache: false, + } +} + // Specifies the source directory for a package and their destination in the build context. type pkgSource struct { src string @@ -65,16 +90,10 @@ type Pkg struct { git *git } -// NewFromCLI creates a range of Pkg from a set of CLI arguments. Calls fs.Parse() -func NewFromCLI(fs *flag.FlagSet, args ...string) ([]Pkg, error) { +// NewFromConfig creates a range of Pkg from a PkglibConfig and paths to packages. +func NewFromConfig(cfg PkglibConfig, args ...string) ([]Pkg, error) { // Defaults - piBase := pkgInfo{ - Org: "linuxkit", - Arches: []string{"amd64", "arm64", "s390x"}, - GitRepo: "https://github.com/linuxkit/linuxkit", - Network: false, - DisableCache: false, - } + piBase := NewPkgInfo() // TODO(ijc) look for "$(git rev-parse --show-toplevel)/.build-defaults.yml"? @@ -83,49 +102,23 @@ func NewFromCLI(fs *flag.FlagSet, args ...string) ([]Pkg, error) { // These override fields in pi below, bools are in both forms to allow user overrides in either direction. // These will apply to all packages built. - argDisableCache := fs.Bool("disable-cache", piBase.DisableCache, "Disable build cache") - argEnableCache := fs.Bool("enable-cache", !piBase.DisableCache, "Enable build cache") - argNoNetwork := fs.Bool("nonetwork", !piBase.Network, "Disallow network use during build") - argNetwork := fs.Bool("network", piBase.Network, "Allow network use during build") - - argOrg := fs.String("org", piBase.Org, "Override the hub org") - // Other arguments - var buildYML, hash, hashCommit, hashPath string - var dirty, devMode bool - - fs.StringVar(&buildYML, "build-yml", "build.yml", "Override the name of the yml file") - fs.StringVar(&hash, "hash", "", "Override the image hash (default is to query git for the package's tree-sh)") - fs.StringVar(&hashCommit, "hash-commit", "HEAD", "Override the git commit to use for the hash") - fs.StringVar(&hashPath, "hash-path", "", "Override the directory to use for the image hash, must be a parent of the package dir (default is to use the package dir)") - fs.BoolVar(&dirty, "force-dirty", false, "Force the pkg(s) to be considered dirty") - fs.BoolVar(&devMode, "dev", false, "Force org and hash to $USER and \"dev\" respectively") - - util.AddLoggingFlags(fs) - - _ = fs.Parse(args) - - util.SetupLogging() - - if fs.NArg() < 1 { - return nil, fmt.Errorf("At least one pkg directory is required") - } var pkgs []Pkg - for _, pkg := range fs.Args() { + for _, pkg := range args { var ( pkgHashPath string - pkgHash = hash + pkgHash = cfg.Hash ) pkgPath, err := filepath.Abs(pkg) if err != nil { return nil, err } - if hashPath == "" { + if cfg.HashPath == "" { pkgHashPath = pkgPath } else { - pkgHashPath, err = filepath.Abs(hashPath) + pkgHashPath, err = filepath.Abs(cfg.HashPath) if err != nil { return nil, err } @@ -147,7 +140,7 @@ func NewFromCLI(fs *flag.FlagSet, args ...string) ([]Pkg, error) { return nil, err } - b, err := os.ReadFile(filepath.Join(pkgPath, buildYML)) + b, err := os.ReadFile(filepath.Join(pkgPath, cfg.BuildYML)) if err != nil { return nil, err } @@ -165,7 +158,7 @@ func NewFromCLI(fs *flag.FlagSet, args ...string) ([]Pkg, error) { return nil, err } - if devMode { + if cfg.Dev { // If --org is also used then this will be overwritten // by argOrg when we iterate over the provided options // in the fs.Visit block below. @@ -179,21 +172,15 @@ func NewFromCLI(fs *flag.FlagSet, args ...string) ([]Pkg, error) { // apart from Visit which iterates over only those which were // set. This must be run here, rather than earlier, because we need to // have read it from the build.yml file first, then override based on CLI. - fs.Visit(func(f *flag.Flag) { - switch f.Name { - case "disable-cache": - pi.DisableCache = *argDisableCache - case "enable-cache": - pi.DisableCache = !*argEnableCache - case "network": - pi.Network = *argNetwork - case "nonetwork": - pi.Network = !*argNoNetwork - case "org": - pi.Org = *argOrg - } - }) - + if cfg.DisableCache != nil { + pi.DisableCache = *cfg.DisableCache + } + if cfg.Network != nil { + pi.Network = *cfg.Network + } + if cfg.Org != nil { + pi.Org = *cfg.Org + } var srcHashes string sources := []pkgSource{{src: pkgPath, dst: "/"}} @@ -216,7 +203,7 @@ func NewFromCLI(fs *flag.FlagSet, args ...string) ([]Pkg, error) { if g == nil { return nil, fmt.Errorf("Source %s not in a git repository", srcPath) } - h, err := g.treeHash(srcPath, hashCommit) + h, err := g.treeHash(srcPath, cfg.HashCommit) if err != nil { return nil, err } @@ -230,16 +217,17 @@ func NewFromCLI(fs *flag.FlagSet, args ...string) ([]Pkg, error) { return nil, err } + var dirty bool if git != nil { - gitDirty, err := git.isDirty(pkgHashPath, hashCommit) + gitDirty, err := git.isDirty(pkgHashPath, cfg.HashCommit) if err != nil { return nil, err } - dirty = dirty || gitDirty + dirty = cfg.Dirty || gitDirty if pkgHash == "" { - if pkgHash, err = git.treeHash(pkgHashPath, hashCommit); err != nil { + if pkgHash, err = git.treeHash(pkgHashPath, cfg.HashCommit); err != nil { return nil, err } @@ -266,7 +254,7 @@ func NewFromCLI(fs *flag.FlagSet, args ...string) ([]Pkg, error) { image: pi.Image, org: pi.Org, hash: pkgHash, - commitHash: hashCommit, + commitHash: cfg.HashCommit, arches: pi.Arches, sources: sources, gitRepo: pi.GitRepo, diff --git a/src/cmd/linuxkit/pkglib/pkglib_test.go b/src/cmd/linuxkit/pkglib/pkglib_test.go index e702c31de..628b1992f 100644 --- a/src/cmd/linuxkit/pkglib/pkglib_test.go +++ b/src/cmd/linuxkit/pkglib/pkglib_test.go @@ -1,9 +1,11 @@ package pkglib import ( - "flag" + "fmt" "os" + "path" "path/filepath" + "reflect" "regexp" "testing" @@ -21,66 +23,84 @@ func dummyPackage(t *testing.T, tmpDir, yml string) string { return d } -func testBool(t *testing.T, key string, inv bool, forceOn, forceOff string, get func(p Pkg) bool) { +// testGetBoolPkg given a combination of field, fileKey, fileSetting and override setting, +// create a Pkg that reflects it. +func testGetBoolPkg(t *testing.T, fileKey, cfgKey string, fileSetting, cfgSetting *bool) Pkg { + // create a git-enabled temporary working directory cwd, err := os.Getwd() require.NoError(t, err) - - tmpDir := filepath.Join(cwd, t.Name()) - err = os.Mkdir(tmpDir, 0755) + tmpdirBase := path.Join(cwd, "testdatadir") + err = os.MkdirAll(tmpdirBase, 0755) require.NoError(t, err) - defer os.RemoveAll(tmpDir) - - check := func(pkgDir, override string, f func(t *testing.T, p Pkg)) func(t *testing.T) { - return func(t *testing.T) { - flags := flag.NewFlagSet(t.Name(), flag.ExitOnError) - args := []string{"-hash-path=" + cwd} - if override != "" { - args = append(args, override) - } - args = append(args, pkgDir) - pkgs, err := NewFromCLI(flags, args...) - require.NoError(t, err) - pkg := pkgs[0] - t.Logf("override %q produced %t", override, get(pkg)) - f(t, pkg) - } + tmpDir, err := os.MkdirTemp(tmpdirBase, "pkglib_test") + require.NoError(t, err) + defer os.RemoveAll(tmpdirBase) + var value string + if fileSetting != nil { + value = fmt.Sprintf("%s: %v\n", fileKey, *fileSetting) } - - setting := func(name, cfg string, def bool) { - var value string - if cfg != "" { - value = key + ": " + cfg + "\n" - } - pkgDir := dummyPackage(t, tmpDir, ` + pkgDir := dummyPackage(t, tmpDir, ` image: dummy `+value) - t.Run(name, func(t *testing.T) { - t.Run("None", check(pkgDir, "", func(t *testing.T, p Pkg) { - assert.Equal(t, def, get(p)) - })) - t.Run("ForceOn", check(pkgDir, forceOn, func(t *testing.T, p Pkg) { - assert.True(t, get(p)) - })) - t.Run("ForceOff", check(pkgDir, forceOff, func(t *testing.T, p Pkg) { - assert.False(t, get(p)) - })) + cfg := PkglibConfig{ + HashPath: cwd, + BuildYML: "build.yml", + HashCommit: "HEAD", + } + if cfgSetting != nil { + cfgField := reflect.ValueOf(&cfg).Elem().FieldByName(cfgKey) + cfgField.Set(reflect.ValueOf(cfgSetting)) + } + pkgs, err := NewFromConfig(cfg, pkgDir) + require.NoError(t, err) + return pkgs[0] +} + +func TestBoolSettings(t *testing.T) { + // this is false, because the default is to disable network. The option "Network" + // is aligned with the value p.network + var ( + trueVal = true + falseVal = false + ) + tests := []struct { + testName string + cfgKey string + fileKey string + fileSetting *bool + cfgSetting *bool + pkgField string + expectation bool + }{ + {"Network/Default/None", "Network", "network", nil, nil, "network", false}, + {"Network/Default/ForceOn", "Network", "network", nil, &trueVal, "network", true}, + {"Network/Default/ForceOff", "Network", "network", nil, &falseVal, "network", false}, + {"Network/SetTrue/None", "Network", "network", &trueVal, nil, "network", true}, + {"Network/SetTrue/ForceOn", "Network", "network", &trueVal, &trueVal, "network", true}, + {"Network/SetTrue/ForceOff", "Network", "network", &trueVal, &falseVal, "network", false}, + {"Network/SetFalse/None", "Network", "network", &falseVal, nil, "network", false}, + {"Network/SetFalse/ForceOn", "Network", "network", &falseVal, &trueVal, "network", true}, + {"Network/SetFalse/ForceOff", "Network", "network", &falseVal, &falseVal, "network", false}, + {"Cache/Default/None", "DisableCache", "disable-cache", nil, nil, "cache", true}, + {"Cache/Default/ForceOn", "DisableCache", "disable-cache", nil, &trueVal, "cache", false}, + {"Cache/Default/ForceOff", "DisableCache", "disable-cache", nil, &falseVal, "cache", true}, + {"Cache/SetTrue/None", "DisableCache", "disable-cache", &trueVal, nil, "cache", false}, + {"Cache/SetTrue/ForceOn", "DisableCache", "disable-cache", &trueVal, &trueVal, "cache", false}, + {"Cache/SetTrue/ForceOff", "DisableCache", "disable-cache", &trueVal, &falseVal, "cache", true}, + {"Cache/SetFalse/None", "DisableCache", "disable-cache", &falseVal, nil, "cache", true}, + {"Cache/SetFalse/ForceOn", "DisableCache", "disable-cache", &falseVal, &trueVal, "cache", false}, + {"Cache/SetFalse/ForceOff", "DisableCache", "disable-cache", &falseVal, &falseVal, "cache", true}, + } + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + pkg := testGetBoolPkg(t, tt.fileKey, tt.cfgKey, tt.fileSetting, tt.cfgSetting) + returned := reflect.ValueOf(&pkg).Elem().FieldByName(tt.pkgField) + + t.Logf("override field %s value %v produced %t", tt.cfgKey, tt.cfgSetting, returned.Bool()) + assert.Equal(t, tt.expectation, returned.Bool()) }) } - - // `inv` indicates that the sense of the boolean in - // `build.yml` is inverted, booleans default to false. - setting("Default", "", inv) - setting("SetTrue", "true", !inv) - setting("SetFalse", "false", inv) -} - -func TestNetwork(t *testing.T) { - testBool(t, "network", false, "-network", "-nonetwork", func(p Pkg) bool { return p.network }) -} - -func TestCache(t *testing.T) { - testBool(t, "disable-cache", true, "-enable-cache", "-disable-cache", func(p Pkg) bool { return p.cache }) } func testBadBuildYML(t *testing.T, build, expect string) { @@ -93,9 +113,11 @@ func testBadBuildYML(t *testing.T, build, expect string) { defer os.RemoveAll(tmpDir) pkgDir := dummyPackage(t, tmpDir, build) - flags := flag.NewFlagSet(t.Name(), flag.ExitOnError) - args := []string{"-hash-path=" + cwd, pkgDir} - _, err = NewFromCLI(flags, args...) + _, err = NewFromConfig(PkglibConfig{ + HashPath: cwd, + BuildYML: "build.yml", + HashCommit: "HEAD", + }, pkgDir) require.Error(t, err) assert.Regexp(t, regexp.MustCompile(expect), err.Error()) } diff --git a/src/cmd/linuxkit/push.go b/src/cmd/linuxkit/push.go index 866c0121d..62ad9de52 100644 --- a/src/cmd/linuxkit/push.go +++ b/src/cmd/linuxkit/push.go @@ -1,58 +1,25 @@ package main import ( - "fmt" - "os" - "path/filepath" - - log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) -func pushUsage() { - invoked := filepath.Base(os.Args[0]) - fmt.Printf("USAGE: %s push [backend] [options] [prefix]\n\n", invoked) - fmt.Printf("'backend' specifies the push backend.\n") - fmt.Printf("Supported backends are\n") - // Please keep these in alphabetical order - fmt.Printf(" aws\n") - fmt.Printf(" azure\n") - fmt.Printf(" gcp\n") - fmt.Printf(" openstack\n") - fmt.Printf(" packet\n") - fmt.Printf(" scaleway\n") - fmt.Printf(" vcenter\n") - fmt.Printf("\n") - fmt.Printf("'options' are the backend specific options.\n") - fmt.Printf("See '%s push [backend] --help' for details.\n\n", invoked) - fmt.Printf("'prefix' specifies the path to the VM image.\n") -} +func pushCmd() *cobra.Command { -func push(args []string) { - if len(args) < 1 { - pushUsage() - os.Exit(1) + cmd := &cobra.Command{ + Use: "push", + Short: "push a VM image to a cloud provider", + Long: `Push a VM image to a cloud provider.`, } - switch args[0] { // Please keep cases in alphabetical order - case "aws": - pushAWS(args[1:]) - case "azure": - pushAzure(args[1:]) - case "gcp": - pushGcp(args[1:]) - case "openstack": - pushOpenstack(args[1:]) - case "packet": - pushPacket(args[1:]) - case "scaleway": - pushScaleway(args[1:]) - case "vcenter": - pushVCenter(args[1:]) - case "help", "-h", "-help", "--help": - pushUsage() - os.Exit(0) - default: - log.Errorf("No 'push' backend specified.") - } + cmd.AddCommand(pushAWSCmd()) + cmd.AddCommand(pushAzureCmd()) + cmd.AddCommand(pushGCPCmd()) + cmd.AddCommand(pushOpenstackCmd()) + cmd.AddCommand(pushPacketCmd()) + cmd.AddCommand(pushScalewayCmd()) + cmd.AddCommand(pushVCenterCmd()) + + return cmd } diff --git a/src/cmd/linuxkit/push_aws.go b/src/cmd/linuxkit/push_aws.go index 5fbb265eb..9eda3093f 100644 --- a/src/cmd/linuxkit/push_aws.go +++ b/src/cmd/linuxkit/push_aws.go @@ -2,7 +2,6 @@ package main import ( "context" - "flag" "fmt" "os" "path/filepath" @@ -14,174 +13,181 @@ import ( "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/s3" log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) const timeoutVar = "LINUXKIT_UPLOAD_TIMEOUT" -func pushAWS(args []string) { - flags := flag.NewFlagSet("aws", flag.ExitOnError) - invoked := filepath.Base(os.Args[0]) - flags.Usage = func() { - fmt.Printf("USAGE: %s push aws [options] path\n\n", invoked) - fmt.Printf("'path' specifies the full path of an AWS image. It will be uploaded to S3 and an AMI will be created from it.\n") - fmt.Printf("Options:\n\n") - flags.PrintDefaults() - } - timeoutFlag := flags.Int("timeout", 0, "Upload timeout in seconds") - bucketFlag := flags.String("bucket", "", "S3 Bucket to upload to. *Required*") - nameFlag := flags.String("img-name", "", "Overrides the name used to identify the file in Amazon S3 and the VM image. Defaults to the base of 'path' with the file extension removed.") - enaFlag := flags.Bool("ena", false, "Enable ENA networking") - sriovNetFlag := flags.String("sriov", "", "SRIOV network support, set to 'simple' to enable 82599 VF networking") - uefiFlag := flags.Bool("uefi", false, "Enable uefi boot mode.") - tpmFlag := flags.Bool("tpm", false, "Enable tpm device.") +func pushAWSCmd() *cobra.Command { + var ( + timeoutFlag int + bucketFlag string + nameFlag string + ena bool + sriovNet string + uefi bool + tpm bool + ) + cmd := &cobra.Command{ + Use: "aws", + Short: "push image to AWS", + Long: `Push image to AWS. + Single argument specifies the full path of an AWS image. It will be uploaded to S3 and an AMI will be created from it. + `, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + path := args[0] - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") - } + timeout := getIntValue(timeoutVar, timeoutFlag, 600) + bucket := getStringValue(bucketVar, bucketFlag, "") + name := getStringValue(nameVar, nameFlag, "") - remArgs := flags.Args() - if len(remArgs) == 0 { - fmt.Printf("Please specify the path to the image to push\n") - flags.Usage() - os.Exit(1) - } - path := remArgs[0] - - timeout := getIntValue(timeoutVar, *timeoutFlag, 600) - bucket := getStringValue(bucketVar, *bucketFlag, "") - name := getStringValue(nameVar, *nameFlag, "") - if *sriovNetFlag == "" { - sriovNetFlag = nil - } - - if !*uefiFlag && *tpmFlag { - log.Fatal("Cannot use tpm without uefi mode") - } - - sess := session.Must(session.NewSession()) - storage := s3.New(sess) - - ctx, cancelFn := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) - defer cancelFn() - - if bucket == "" { - log.Fatalf("Please provide the bucket to use") - } - - f, err := os.Open(path) - if err != nil { - log.Fatalf("Error opening file: %v", err) - } - defer f.Close() - - if name == "" { - name = strings.TrimSuffix(path, filepath.Ext(path)) - name = filepath.Base(name) - } - - fi, err := f.Stat() - if err != nil { - log.Fatalf("Error reading file information: %v", err) - } - - dst := name + filepath.Ext(path) - putParams := &s3.PutObjectInput{ - Bucket: aws.String(bucket), - Key: aws.String(dst), - Body: f, - ContentLength: aws.Int64(fi.Size()), - ContentType: aws.String("application/octet-stream"), - } - log.Debugf("PutObject:\n%v", putParams) - - _, err = storage.PutObjectWithContext(ctx, putParams) - if err != nil { - log.Fatalf("Error uploading to S3: %v", err) - } - - compute := ec2.New(sess) - - importParams := &ec2.ImportSnapshotInput{ - Description: aws.String(fmt.Sprintf("LinuxKit: %s", name)), - DiskContainer: &ec2.SnapshotDiskContainer{ - Description: aws.String(fmt.Sprintf("LinuxKit: %s disk", name)), - Format: aws.String("raw"), - UserBucket: &ec2.UserBucket{ - S3Bucket: aws.String(bucket), - S3Key: aws.String(dst), - }, - }, - } - log.Debugf("ImportSnapshot:\n%v", importParams) - - resp, err := compute.ImportSnapshot(importParams) - if err != nil { - log.Fatalf("Error importing snapshot: %v", err) - } - - var snapshotID *string - for { - describeParams := &ec2.DescribeImportSnapshotTasksInput{ - ImportTaskIds: []*string{ - resp.ImportTaskId, - }, - } - log.Debugf("DescribeImportSnapshotTask:\n%v", describeParams) - status, err := compute.DescribeImportSnapshotTasks(describeParams) - if err != nil { - log.Fatalf("Error getting import snapshot status: %v", err) - } - if len(status.ImportSnapshotTasks) == 0 { - log.Fatalf("Unable to get import snapshot task status") - } - if *status.ImportSnapshotTasks[0].SnapshotTaskDetail.Status != "completed" { - progress := "0" - if status.ImportSnapshotTasks[0].SnapshotTaskDetail.Progress != nil { - progress = *status.ImportSnapshotTasks[0].SnapshotTaskDetail.Progress + var sriovNetFlag *string + if sriovNet != "" { + *sriovNetFlag = sriovNet } - log.Debugf("Task %s is %s%% complete. Waiting 60 seconds...\n", *resp.ImportTaskId, progress) - time.Sleep(60 * time.Second) - continue - } - snapshotID = status.ImportSnapshotTasks[0].SnapshotTaskDetail.SnapshotId - break - } - if snapshotID == nil { - log.Fatalf("SnapshotID unavailable after import completed") - } else { - log.Debugf("SnapshotID: %s", *snapshotID) - } + if !uefi && tpm { + return fmt.Errorf("Cannot use tpm without uefi mode") + } - regParams := &ec2.RegisterImageInput{ - Name: aws.String(name), // Required - Architecture: aws.String("x86_64"), - BlockDeviceMappings: []*ec2.BlockDeviceMapping{ - { - DeviceName: aws.String("/dev/sda1"), - Ebs: &ec2.EbsBlockDevice{ - DeleteOnTermination: aws.Bool(true), - SnapshotId: snapshotID, - VolumeType: aws.String("standard"), + sess := session.Must(session.NewSession()) + storage := s3.New(sess) + + ctx, cancelFn := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancelFn() + + if bucket == "" { + return fmt.Errorf("Please provide the bucket to use") + } + + f, err := os.Open(path) + if err != nil { + return fmt.Errorf("Error opening file: %v", err) + } + defer f.Close() + + if name == "" { + name = strings.TrimSuffix(path, filepath.Ext(path)) + name = filepath.Base(name) + } + + fi, err := f.Stat() + if err != nil { + return fmt.Errorf("Error reading file information: %v", err) + } + + dst := name + filepath.Ext(path) + putParams := &s3.PutObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(dst), + Body: f, + ContentLength: aws.Int64(fi.Size()), + ContentType: aws.String("application/octet-stream"), + } + log.Debugf("PutObject:\n%v", putParams) + + _, err = storage.PutObjectWithContext(ctx, putParams) + if err != nil { + return fmt.Errorf("Error uploading to S3: %v", err) + } + + compute := ec2.New(sess) + + importParams := &ec2.ImportSnapshotInput{ + Description: aws.String(fmt.Sprintf("LinuxKit: %s", name)), + DiskContainer: &ec2.SnapshotDiskContainer{ + Description: aws.String(fmt.Sprintf("LinuxKit: %s disk", name)), + Format: aws.String("raw"), + UserBucket: &ec2.UserBucket{ + S3Bucket: aws.String(bucket), + S3Key: aws.String(dst), + }, }, - }, + } + log.Debugf("ImportSnapshot:\n%v", importParams) + + resp, err := compute.ImportSnapshot(importParams) + if err != nil { + return fmt.Errorf("Error importing snapshot: %v", err) + } + + var snapshotID *string + for { + describeParams := &ec2.DescribeImportSnapshotTasksInput{ + ImportTaskIds: []*string{ + resp.ImportTaskId, + }, + } + log.Debugf("DescribeImportSnapshotTask:\n%v", describeParams) + status, err := compute.DescribeImportSnapshotTasks(describeParams) + if err != nil { + return fmt.Errorf("Error getting import snapshot status: %v", err) + } + if len(status.ImportSnapshotTasks) == 0 { + return fmt.Errorf("Unable to get import snapshot task status") + } + if *status.ImportSnapshotTasks[0].SnapshotTaskDetail.Status != "completed" { + progress := "0" + if status.ImportSnapshotTasks[0].SnapshotTaskDetail.Progress != nil { + progress = *status.ImportSnapshotTasks[0].SnapshotTaskDetail.Progress + } + log.Debugf("Task %s is %s%% complete. Waiting 60 seconds...\n", *resp.ImportTaskId, progress) + time.Sleep(60 * time.Second) + continue + } + snapshotID = status.ImportSnapshotTasks[0].SnapshotTaskDetail.SnapshotId + break + } + + if snapshotID == nil { + return fmt.Errorf("SnapshotID unavailable after import completed") + } else { + log.Debugf("SnapshotID: %s", *snapshotID) + } + + regParams := &ec2.RegisterImageInput{ + Name: aws.String(name), // Required + Architecture: aws.String("x86_64"), + BlockDeviceMappings: []*ec2.BlockDeviceMapping{ + { + DeviceName: aws.String("/dev/sda1"), + Ebs: &ec2.EbsBlockDevice{ + DeleteOnTermination: aws.Bool(true), + SnapshotId: snapshotID, + VolumeType: aws.String("standard"), + }, + }, + }, + Description: aws.String(fmt.Sprintf("LinuxKit: %s image", name)), + RootDeviceName: aws.String("/dev/sda1"), + VirtualizationType: aws.String("hvm"), + EnaSupport: &ena, + SriovNetSupport: sriovNetFlag, + } + if uefi { + regParams.BootMode = aws.String("uefi") + if tpm { + regParams.TpmSupport = aws.String("v2.0") + } + } + log.Debugf("RegisterImage:\n%v", regParams) + regResp, err := compute.RegisterImage(regParams) + if err != nil { + return fmt.Errorf("Error registering the image: %s; %v", name, err) + } + log.Infof("Created AMI: %s", *regResp.ImageId) + return nil }, - Description: aws.String(fmt.Sprintf("LinuxKit: %s image", name)), - RootDeviceName: aws.String("/dev/sda1"), - VirtualizationType: aws.String("hvm"), - EnaSupport: enaFlag, - SriovNetSupport: sriovNetFlag, } - if *uefiFlag { - regParams.BootMode = aws.String("uefi") - if *tpmFlag { - regParams.TpmSupport = aws.String("v2.0") - } - } - log.Debugf("RegisterImage:\n%v", regParams) - regResp, err := compute.RegisterImage(regParams) - if err != nil { - log.Fatalf("Error registering the image: %s; %v", name, err) - } - log.Infof("Created AMI: %s", *regResp.ImageId) + + cmd.Flags().IntVar(&timeoutFlag, "timeout", 0, "Upload timeout in seconds") + cmd.Flags().StringVar(&bucketFlag, "bucket", "", "S3 Bucket to upload to. *Required*") + cmd.Flags().StringVar(&nameFlag, "img-name", "", "Overrides the name used to identify the file in Amazon S3 and the VM image. Defaults to the base of 'path' with the file extension removed.") + cmd.Flags().BoolVar(&ena, "ena", false, "Enable ENA networking") + cmd.Flags().StringVar(&sriovNet, "sriov", "", "SRIOV network support, set to 'simple' to enable 82599 VF networking") + cmd.Flags().BoolVar(&uefi, "uefi", false, "Enable uefi boot mode.") + cmd.Flags().BoolVar(&tpm, "tpm", false, "Enable tpm device.") + + return cmd } diff --git a/src/cmd/linuxkit/push_azure.go b/src/cmd/linuxkit/push_azure.go index 64da44424..d7b58d4ae 100644 --- a/src/cmd/linuxkit/push_azure.go +++ b/src/cmd/linuxkit/push_azure.go @@ -1,47 +1,46 @@ package main import ( - "flag" - "fmt" - "log" - "os" - "path/filepath" + "github.com/spf13/cobra" ) -// Process the run arguments and execute run -func pushAzure(args []string) { - flags := flag.NewFlagSet("azure", flag.ExitOnError) - invoked := filepath.Base(os.Args[0]) - flags.Usage = func() { - fmt.Printf("USAGE: %s push azure [options] path\n\n", invoked) - fmt.Printf("Push a disk image to Azure\n") - fmt.Printf("'path' specifies the path to a VHD. It will be uploaded to an Azure Storage Account.\n") - fmt.Printf("Options:\n\n") - flags.PrintDefaults() +func pushAzureCmd() *cobra.Command { + var ( + resourceGroup string + accountName string + ) + cmd := &cobra.Command{ + Use: "azure", + Short: "push image to Azure", + Long: `Push image to Azure. + First argument specifies the path to a VHD. It will be uploaded to an Azure Storage Account. + Relies on the following environment variables: + + AZURE_SUBSCRIPTION_ID + AZURE_TENANT_ID + AZURE_CLIENT_ID + AZURE_CLIENT_SECRET + + `, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + path := args[0] + + subscriptionID := getEnvVarOrExit("AZURE_SUBSCRIPTION_ID") + tenantID := getEnvVarOrExit("AZURE_TENANT_ID") + + clientID := getEnvVarOrExit("AZURE_CLIENT_ID") + clientSecret := getEnvVarOrExit("AZURE_CLIENT_SECRET") + + initializeAzureClients(subscriptionID, tenantID, clientID, clientSecret) + + uploadVMImage(resourceGroup, accountName, path) + return nil + }, } - resourceGroup := flags.String("resource-group", "", "Name of resource group to be used for VM") - accountName := flags.String("storage-account", "", "Name of the storage account") + cmd.Flags().StringVar(&resourceGroup, "resource-group", "", "Name of resource group to be used for VM") + cmd.Flags().StringVar(&accountName, "storage-account", "", "Name of the storage account") - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") - } - - remArgs := flags.Args() - if len(remArgs) == 0 { - fmt.Printf("Please specify the path to the image to push\n") - flags.Usage() - os.Exit(1) - } - path := remArgs[0] - - subscriptionID := getEnvVarOrExit("AZURE_SUBSCRIPTION_ID") - tenantID := getEnvVarOrExit("AZURE_TENANT_ID") - - clientID := getEnvVarOrExit("AZURE_CLIENT_ID") - clientSecret := getEnvVarOrExit("AZURE_CLIENT_SECRET") - - initializeAzureClients(subscriptionID, tenantID, clientID, clientSecret) - - uploadVMImage(*resourceGroup, *accountName, path) + return cmd } diff --git a/src/cmd/linuxkit/push_gcp.go b/src/cmd/linuxkit/push_gcp.go index b3d8d75d5..0579a0cf8 100644 --- a/src/cmd/linuxkit/push_gcp.go +++ b/src/cmd/linuxkit/push_gcp.go @@ -1,73 +1,78 @@ package main import ( - "flag" "fmt" - "os" "path/filepath" "strings" - log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) -func pushGcp(args []string) { - flags := flag.NewFlagSet("gcp", flag.ExitOnError) - invoked := filepath.Base(os.Args[0]) - flags.Usage = func() { - fmt.Printf("USAGE: %s push gcp [options] path\n\n", invoked) - fmt.Printf("'path' is the full path to a GCP image. It will be uploaded to GCS and GCP VM image will be created from it.\n") - fmt.Printf("Options:\n\n") - flags.PrintDefaults() - } - keysFlag := flags.String("keys", "", "Path to Service Account JSON key file") - projectFlag := flags.String("project", "", "GCP Project Name") - bucketFlag := flags.String("bucket", "", "GCS Bucket to upload to. *Required*") - publicFlag := flags.Bool("public", false, "Select if file on GCS should be public. *Optional*") - familyFlag := flags.String("family", "", "GCP Image Family. A group of images where the family name points to the most recent image. *Optional*") - nameFlag := flags.String("img-name", "", "Overrides the name used to identify the file in Google Storage and the VM image. Defaults to the base of 'path' with the '.img.tar.gz' suffix removed") - nestedVirt := flags.Bool("nested-virt", false, "Enabled nested virtualization for the image") - uefiCompatible := flags.Bool("uefi-compatible", false, "Enable UEFI_COMPATIBLE feature for the image, required to enable vTPM.") +func pushGCPCmd() *cobra.Command { + var ( + keysFlag string + projectFlag string + bucketFlag string + publicFlag bool + familyFlag string + nameFlag string + nestedVirt bool + uefi bool + ) + cmd := &cobra.Command{ + Use: "gcp", + Short: "push image to GCP", + Long: `Push image to GCP. + First argument specifies the path to a disk file. + It will be uploaded to GCS and GCP VM image will be created from it. + `, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + path := args[0] - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") + keys := getStringValue(keysVar, keysFlag, "") + project := getStringValue(projectVar, projectFlag, "") + bucket := getStringValue(bucketVar, bucketFlag, "") + public := getBoolValue(publicVar, publicFlag) + family := getStringValue(familyVar, familyFlag, "") + name := getStringValue(nameVar, nameFlag, "") + + const suffix = ".img.tar.gz" + if name == "" { + name = strings.TrimSuffix(path, suffix) + name = filepath.Base(name) + } + + client, err := NewGCPClient(keys, project) + if err != nil { + return fmt.Errorf("Unable to connect to GCP: %v", err) + } + + if bucket == "" { + return fmt.Errorf("Please specify the bucket to use") + } + + err = client.UploadFile(path, name+suffix, bucket, public) + if err != nil { + return fmt.Errorf("Error copying to Google Storage: %v", err) + } + err = client.CreateImage(name, "https://storage.googleapis.com/"+bucket+"/"+name+suffix, family, nestedVirt, uefi, true) + if err != nil { + return fmt.Errorf("Error creating Google Compute Image: %v", err) + } + + return nil + }, } - remArgs := flags.Args() - if len(remArgs) == 0 { - fmt.Printf("Please specify the path to the image to push\n") - flags.Usage() - os.Exit(1) - } - path := remArgs[0] + cmd.Flags().StringVar(&keysFlag, "keys", "", "Path to Service Account JSON key file") + cmd.Flags().StringVar(&projectFlag, "project", "", "GCP Project Name") + cmd.Flags().StringVar(&bucketFlag, "bucket", "", "GCS Bucket to upload to. *Required*") + cmd.Flags().BoolVar(&publicFlag, "public", false, "Select if file on GCS should be public. *Optional*") + cmd.Flags().StringVar(&familyFlag, "family", "", "GCP Image Family. A group of images where the family name points to the most recent image. *Optional*") + cmd.Flags().StringVar(&nameFlag, "img-name", "", "Overrides the name used to identify the file in Google Storage and the VM image. Defaults to the base of 'path' with the '.img.tar.gz' suffix removed") + cmd.Flags().BoolVar(&nestedVirt, "nested-virt", false, "Enabled nested virtualization for the image") + cmd.Flags().BoolVar(&uefi, "uefi-compatible", false, "Enable UEFI_COMPATIBLE feature for the image, required to enable vTPM.") - keys := getStringValue(keysVar, *keysFlag, "") - project := getStringValue(projectVar, *projectFlag, "") - bucket := getStringValue(bucketVar, *bucketFlag, "") - public := getBoolValue(publicVar, *publicFlag) - family := getStringValue(familyVar, *familyFlag, "") - name := getStringValue(nameVar, *nameFlag, "") - - const suffix = ".img.tar.gz" - if name == "" { - name = strings.TrimSuffix(path, suffix) - name = filepath.Base(name) - } - - client, err := NewGCPClient(keys, project) - if err != nil { - log.Fatalf("Unable to connect to GCP: %v", err) - } - - if bucket == "" { - log.Fatalf("Please specify the bucket to use") - } - - err = client.UploadFile(path, name+suffix, bucket, public) - if err != nil { - log.Fatalf("Error copying to Google Storage: %v", err) - } - err = client.CreateImage(name, "https://storage.googleapis.com/"+bucket+"/"+name+suffix, family, *nestedVirt, *uefiCompatible, true) - if err != nil { - log.Fatalf("Error creating Google Compute Image: %v", err) - } + return cmd } diff --git a/src/cmd/linuxkit/push_openstack.go b/src/cmd/linuxkit/push_openstack.go index 5fbd46a56..de4406ff5 100644 --- a/src/cmd/linuxkit/push_openstack.go +++ b/src/cmd/linuxkit/push_openstack.go @@ -1,7 +1,6 @@ package main import ( - "flag" "fmt" "os" "path" @@ -12,44 +11,11 @@ import ( "github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata" "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" "github.com/gophercloud/utils/openstack/clientconfig" + "github.com/spf13/cobra" log "github.com/sirupsen/logrus" ) -// Process the run arguments and execute run -func pushOpenstack(args []string) { - flags := flag.NewFlagSet("openstack", flag.ExitOnError) - invoked := filepath.Base(os.Args[0]) - flags.Usage = func() { - fmt.Printf("USAGE: %s push openstack [options] path\n\n", invoked) - fmt.Printf("'path' is the full path to an image that will be uploaded to an OpenStack Image store (Glance)\n") - fmt.Printf("Options:\n\n") - flags.PrintDefaults() - } - imageName := flags.String("img-name", "", "A unique name for the image, if blank the filename will be used") - - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") - } - - remArgs := flags.Args() - if len(remArgs) == 0 { - fmt.Printf("Please specify the path to the image to push\n") - flags.Usage() - os.Exit(1) - } - filePath := remArgs[0] - // Check that the file both exists, and can be read - checkFile(filePath) - - client, err := clientconfig.NewServiceClient("image", nil) - if err != nil { - log.Fatalf("Error connecting to your OpenStack cloud: %s", err) - } - - createOpenStackImage(filePath, *imageName, client) -} - func createOpenStackImage(filePath string, imageName string, client *gophercloud.ServiceClient) { // Image formats that are supported by both LinuxKit and OpenStack Glance V2 formats := []string{"ami", "vhd", "vhdx", "vmdk", "raw", "qcow2", "iso"} @@ -103,3 +69,35 @@ func createOpenStackImage(filePath string, imageName string, client *gophercloud fmt.Println(image.ID) } } + +func pushOpenstackCmd() *cobra.Command { + var ( + imageName string + ) + cmd := &cobra.Command{ + Use: "openstack", + Short: "push image to OpenStack Image store (Glance)", + Long: `Push image to OpenStack Image store (Glance). + First argument specifies the path to a disk file. + `, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + path := args[0] + + // Check that the file both exists, and can be read + checkFile(path) + + client, err := clientconfig.NewServiceClient("image", nil) + if err != nil { + log.Fatalf("Error connecting to your OpenStack cloud: %s", err) + } + + createOpenStackImage(path, imageName, client) + return nil + }, + } + + cmd.Flags().StringVar(&imageName, "img-name", "", "A unique name for the image, if blank the filename will be used") + + return cmd +} diff --git a/src/cmd/linuxkit/push_packet.go b/src/cmd/linuxkit/push_packet.go index c360884e8..47dc03dda 100644 --- a/src/cmd/linuxkit/push_packet.go +++ b/src/cmd/linuxkit/push_packet.go @@ -1,7 +1,6 @@ package main import ( - "flag" "fmt" "io" "net/url" @@ -12,6 +11,7 @@ import ( // drop-in 100% compatible replacement and 17% faster than compress/gzip. gzip "github.com/klauspost/pgzip" log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) var ( @@ -28,70 +28,77 @@ func init() { } } -// Process the run arguments and execute run -func pushPacket(args []string) { - flags := flag.NewFlagSet("packet", flag.ExitOnError) - invoked := filepath.Base(os.Args[0]) - flags.Usage = func() { - fmt.Printf("USAGE: %s push packet [options] [name]\n\n", invoked) - fmt.Printf("Options:\n\n") - flags.PrintDefaults() - } - baseURLFlag := flags.String("base-url", "", "Base URL that the kernel, initrd and iPXE script are served from (or "+packetBaseURL+")") - nameFlag := flags.String("img-name", "", "Overrides the prefix used to identify the files. Defaults to [name] (or "+packetNameVar+")") - archFlag := flags.String("arch", packetDefaultArch, "Image architecture (x86_64 or aarch64)") - decompressFlag := flags.Bool("decompress", packetDefaultDecompress, "Decompress kernel/initrd before pushing") - dstFlag := flags.String("destination", "", "URL where to push the image to. Currently only 'file' is supported as a scheme (which is also the default if omitted)") +func pushPacketCmd() *cobra.Command { + var ( + baseURLFlag string + nameFlag string + arch string + dst string + decompress bool + ) + cmd := &cobra.Command{ + Use: "packet", + Short: "push image to Equinix Metal / Packet", + Long: `Push image to Equinix Metal / Packet. + Single argument is the prefix to use for the image, defualts to "packet". + `, + Example: "linuxkit push packet [options] [name]", + RunE: func(cmd *cobra.Command, args []string) error { + prefix := "packet" + if len(args) > 0 { + prefix = args[0] + } - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") + baseURL := getStringValue(packetBaseURL, baseURLFlag, "") + if baseURL == "" { + return fmt.Errorf("Need to specify a value for --base-url from where the kernel, initrd and iPXE script will be loaded from.") + } + + if dst == "" { + return fmt.Errorf("Need to specify the destination where to push to.") + } + + name := getStringValue(packetNameVar, nameFlag, prefix) + + if _, err := os.Stat(fmt.Sprintf("%s-kernel", name)); os.IsNotExist(err) { + return fmt.Errorf("kernel file does not exist: %v", err) + } + if _, err := os.Stat(fmt.Sprintf("%s-initrd.img", name)); os.IsNotExist(err) { + return fmt.Errorf("initrd file does not exist: %v", err) + } + + // Read kernel command line + var cmdline string + if c, err := os.ReadFile(prefix + "-cmdline"); err != nil { + return fmt.Errorf("Cannot open cmdline file: %v", err) + } else { + cmdline = string(c) + } + + ipxeScript := packetIPXEScript(name, baseURL, cmdline, arch) + + // Parse the destination + dst, err := url.Parse(dst) + if err != nil { + return fmt.Errorf("Cannot parse destination: %v", err) + } + switch dst.Scheme { + case "", "file": + packetPushFile(dst, decompress, name, cmdline, ipxeScript) + default: + return fmt.Errorf("Unknown destination format: %s", dst.Scheme) + } + return nil + }, } - remArgs := flags.Args() - prefix := "packet" - if len(remArgs) > 0 { - prefix = remArgs[0] - } + cmd.Flags().StringVar(&baseURLFlag, "base-url", "", "Base URL that the kernel, initrd and iPXE script are served from (or "+packetBaseURL+")") + cmd.Flags().StringVar(&nameFlag, "img-name", "", "Overrides the prefix used to identify the files. Defaults to [name] (or "+packetNameVar+")") + cmd.Flags().StringVar(&arch, "arch", packetDefaultArch, "Image architecture (x86_64 or aarch64)") + cmd.Flags().BoolVar(&decompress, "decompress", packetDefaultDecompress, "Decompress kernel/initrd before pushing") + cmd.Flags().StringVar(&dst, "destination", "", "URL where to push the image to. Currently only 'file' is supported as a scheme (which is also the default if omitted)") - baseURL := getStringValue(packetBaseURL, *baseURLFlag, "") - if baseURL == "" { - log.Fatal("Need to specify a value for --base-url from where the kernel, initrd and iPXE script will be loaded from.") - } - - if *dstFlag == "" { - log.Fatal("Need to specify the destination where to push to.") - } - - name := getStringValue(packetNameVar, *nameFlag, prefix) - - if _, err := os.Stat(fmt.Sprintf("%s-kernel", name)); os.IsNotExist(err) { - log.Fatalf("kernel file does not exist: %v", err) - } - if _, err := os.Stat(fmt.Sprintf("%s-initrd.img", name)); os.IsNotExist(err) { - log.Fatalf("initrd file does not exist: %v", err) - } - - // Read kernel command line - var cmdline string - if c, err := os.ReadFile(prefix + "-cmdline"); err != nil { - log.Fatalf("Cannot open cmdline file: %v", err) - } else { - cmdline = string(c) - } - - ipxeScript := packetIPXEScript(name, baseURL, cmdline, *archFlag) - - // Parse the destination - dst, err := url.Parse(*dstFlag) - if err != nil { - log.Fatalf("Cannot parse destination: %v", err) - } - switch dst.Scheme { - case "", "file": - packetPushFile(dst, *decompressFlag, name, cmdline, ipxeScript) - default: - log.Fatalf("Unknown destination format: %s", dst.Scheme) - } + return cmd } func packetPushFile(dst *url.URL, decompress bool, name, cmdline, ipxeScript string) { diff --git a/src/cmd/linuxkit/push_scaleway.go b/src/cmd/linuxkit/push_scaleway.go index affcc35ed..10c1abbb2 100644 --- a/src/cmd/linuxkit/push_scaleway.go +++ b/src/cmd/linuxkit/push_scaleway.go @@ -1,7 +1,6 @@ package main import ( - "flag" "fmt" "math" "os" @@ -9,116 +8,124 @@ import ( "strings" log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) const defaultScalewayVolumeSize = 10 // GB -func pushScaleway(args []string) { - flags := flag.NewFlagSet("scaleway", flag.ExitOnError) - invoked := filepath.Base(os.Args[0]) - flags.Usage = func() { - fmt.Printf("USAGE: %s push scaleway [options] path\n\n", invoked) - fmt.Printf("'path' is the full path to an EFI ISO image. It will be copied to a new Scaleway instance in order to create a Scaeway image out of it.\n") - fmt.Printf("Options:\n\n") - flags.PrintDefaults() - } - nameFlag := flags.String("img-name", "", "Overrides the name used to identify the image name in Scaleway's images. Defaults to the base of 'path' with the '.iso' suffix removed") - accessKeyFlag := flags.String("access-key", "", "Access Key to connect to Scaleway API") - secretKeyFlag := flags.String("secret-key", "", "Secret Key to connect to Scaleway API") - sshKeyFlag := flags.String("ssh-key", os.Getenv("HOME")+"/.ssh/id_rsa", "SSH key file") - instanceIDFlag := flags.String("instance-id", "", "Instance ID of a running Scaleway instance, with a second volume.") - deviceNameFlag := flags.String("device-name", "/dev/vdb", "Device name on which the image will be copied") - volumeSizeFlag := flags.Int("volume-size", 0, "Size of the volume to use (in GB). Defaults to size of the ISO file rounded up to GB") - zoneFlag := flags.String("zone", defaultScalewayZone, "Select Scaleway zone") - organizationIDFlag := flags.String("organization-id", "", "Select Scaleway's organization ID") - noCleanFlag := flags.Bool("no-clean", false, "Do not remove temporary instance and volumes") +func pushScalewayCmd() *cobra.Command { + var ( + nameFlag string + accessKeyFlag string + secretKeyFlag string + sshKeyFlag string + instanceIDFlag string + deviceNameFlag string + volumeSizeFlag int + zoneFlag string + organizationIDFlag string + noCleanFlag bool + ) + cmd := &cobra.Command{ + Use: "scaleway", + Short: "push image to Scaleway", + Long: `Push image to Scaleway. + First argument specifies the path to an EFI ISO image. It will be copied to a new Scaleway instance in order to create a Scaeway image out of it. + `, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + path := args[0] - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") + name := getStringValue(scalewayNameVar, nameFlag, "") + accessKey := getStringValue(accessKeyVar, accessKeyFlag, "") + secretKey := getStringValue(secretKeyVar, secretKeyFlag, "") + sshKeyFile := getStringValue(sshKeyVar, sshKeyFlag, "") + instanceID := getStringValue(instanceIDVar, instanceIDFlag, "") + deviceName := getStringValue(deviceNameVar, deviceNameFlag, "") + volumeSize := getIntValue(volumeSizeVar, volumeSizeFlag, 0) + zone := getStringValue(zoneVar, zoneFlag, defaultScalewayZone) + organizationID := getStringValue(organizationIDVar, organizationIDFlag, "") + + const suffix = ".iso" + if name == "" { + name = strings.TrimSuffix(path, suffix) + name = filepath.Base(name) + } + + client, err := NewScalewayClient(accessKey, secretKey, zone, organizationID) + if err != nil { + return fmt.Errorf("Unable to connect to Scaleway: %v", err) + } + + // if volume size not set, try to calculate it from file size + if volumeSize == 0 { + if fi, err := os.Stat(path); err == nil { + volumeSize = int(math.Ceil(float64(fi.Size()) / 1000000000)) // / 1 GB + } else { + // fallback to default + log.Warnf("Unable to calculate volume size, using default of %d GB: %v", defaultScalewayVolumeSize, err) + volumeSize = defaultScalewayVolumeSize + } + } + + // if no instanceID is provided, we create the instance + if instanceID == "" { + instanceID, err = client.CreateInstance(volumeSize) + if err != nil { + return fmt.Errorf("Error creating a Scaleway instance: %v", err) + } + + err = client.BootInstanceAndWait(instanceID) + if err != nil { + return fmt.Errorf("Error booting instance: %v", err) + } + } + + volumeID, err := client.GetSecondVolumeID(instanceID) + if err != nil { + return fmt.Errorf("Error retrieving second volume ID: %v", err) + } + + err = client.CopyImageToInstance(instanceID, path, sshKeyFile) + if err != nil { + return fmt.Errorf("Error copying ISO file to Scaleway's instance: %v", err) + } + + err = client.WriteImageToVolume(instanceID, deviceName) + if err != nil { + return fmt.Errorf("Error writing ISO file to additional volume: %v", err) + } + + err = client.TerminateInstance(instanceID) + if err != nil { + return fmt.Errorf("Error terminating Scaleway's instance: %v", err) + } + + err = client.CreateScalewayImage(instanceID, volumeID, name) + if err != nil { + return fmt.Errorf("Error creating Scaleway image: %v", err) + } + + if !noCleanFlag { + err = client.DeleteInstanceAndVolumes(instanceID) + if err != nil { + return fmt.Errorf("Error deleting Scaleway instance and volumes: %v", err) + } + } + return nil + }, } - remArgs := flags.Args() - if len(remArgs) == 0 { - fmt.Printf("Please specify the path to the image to push\n") - flags.Usage() - os.Exit(1) - } - path := remArgs[0] + cmd.Flags().StringVar(&nameFlag, "img-name", "", "Overrides the name used to identify the image name in Scaleway's images. Defaults to the base of 'path' with the '.iso' suffix removed") + cmd.Flags().StringVar(&accessKeyFlag, "access-key", "", "Access Key to connect to Scaleway API") + cmd.Flags().StringVar(&secretKeyFlag, "secret-key", "", "Secret Key to connect to Scaleway API") + cmd.Flags().StringVar(&sshKeyFlag, "ssh-key", os.Getenv("HOME")+"/.ssh/id_rsa", "SSH key file") + cmd.Flags().StringVar(&instanceIDFlag, "instance-id", "", "Instance ID of a running Scaleway instance, with a second volume.") + cmd.Flags().StringVar(&deviceNameFlag, "device-name", "/dev/vdb", "Device name on which the image will be copied") + cmd.Flags().IntVar(&volumeSizeFlag, "volume-size", 0, "Size of the volume to use (in GB). Defaults to size of the ISO file rounded up to GB") + cmd.Flags().StringVar(&zoneFlag, "zone", defaultScalewayZone, "Select Scaleway zone") + cmd.Flags().StringVar(&organizationIDFlag, "organization-id", "", "Select Scaleway's organization ID") + cmd.Flags().BoolVar(&noCleanFlag, "no-clean", false, "Do not remove temporary instance and volumes") - name := getStringValue(scalewayNameVar, *nameFlag, "") - accessKey := getStringValue(accessKeyVar, *accessKeyFlag, "") - secretKey := getStringValue(secretKeyVar, *secretKeyFlag, "") - sshKeyFile := getStringValue(sshKeyVar, *sshKeyFlag, "") - instanceID := getStringValue(instanceIDVar, *instanceIDFlag, "") - deviceName := getStringValue(deviceNameVar, *deviceNameFlag, "") - volumeSize := getIntValue(volumeSizeVar, *volumeSizeFlag, 0) - zone := getStringValue(zoneVar, *zoneFlag, defaultScalewayZone) - organizationID := getStringValue(organizationIDVar, *organizationIDFlag, "") - - const suffix = ".iso" - if name == "" { - name = strings.TrimSuffix(path, suffix) - name = filepath.Base(name) - } - - client, err := NewScalewayClient(accessKey, secretKey, zone, organizationID) - if err != nil { - log.Fatalf("Unable to connect to Scaleway: %v", err) - } - - // if volume size not set, try to calculate it from file size - if volumeSize == 0 { - if fi, err := os.Stat(path); err == nil { - volumeSize = int(math.Ceil(float64(fi.Size()) / 1000000000)) // / 1 GB - } else { - // fallback to default - log.Warnf("Unable to calculate volume size, using default of %d GB: %v", defaultScalewayVolumeSize, err) - volumeSize = defaultScalewayVolumeSize - } - } - - // if no instanceID is provided, we create the instance - if instanceID == "" { - instanceID, err = client.CreateInstance(volumeSize) - if err != nil { - log.Fatalf("Error creating a Scaleway instance: %v", err) - } - - err = client.BootInstanceAndWait(instanceID) - if err != nil { - log.Fatalf("Error booting instance: %v", err) - } - } - - volumeID, err := client.GetSecondVolumeID(instanceID) - if err != nil { - log.Fatalf("Error retrieving second volume ID: %v", err) - } - - err = client.CopyImageToInstance(instanceID, path, sshKeyFile) - if err != nil { - log.Fatalf("Error copying ISO file to Scaleway's instance: %v", err) - } - - err = client.WriteImageToVolume(instanceID, deviceName) - if err != nil { - log.Fatalf("Error writing ISO file to additional volume: %v", err) - } - - err = client.TerminateInstance(instanceID) - if err != nil { - log.Fatalf("Error terminating Scaleway's instance: %v", err) - } - - err = client.CreateScalewayImage(instanceID, volumeID, name) - if err != nil { - log.Fatalf("Error creating Scaleway image: %v", err) - } - - if !*noCleanFlag { - err = client.DeleteInstanceAndVolumes(instanceID) - if err != nil { - log.Fatalf("Error deleting Scaleway instance and volumes: %v", err) - } - } + return cmd } diff --git a/src/cmd/linuxkit/push_vcenter.go b/src/cmd/linuxkit/push_vcenter.go index a433270b1..4452e8268 100644 --- a/src/cmd/linuxkit/push_vcenter.go +++ b/src/cmd/linuxkit/push_vcenter.go @@ -2,73 +2,77 @@ package main import ( "context" - "flag" "fmt" "os" "path" - "path/filepath" "strings" log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" "github.com/vmware/govmomi" "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/vim25/soap" ) -// Process the push arguments and execute push -func pushVCenter(args []string) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() +func pushVCenterCmd() *cobra.Command { + var ( + url string + datacenter string + datastore string + hostname string + folder string + ) + cmd := &cobra.Command{ + Use: "vcenter", + Short: "push image to Azure", + Long: `Push image to Azure. + First argument specifies the full path of an ISO image. It will be pushed to a vCenter cluster. + `, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + filepath := args[0] - var newVM vmConfig + newVM := vmConfig{ + vCenterURL: &url, + dcName: &datacenter, + dsName: &datastore, + vSphereHost: &hostname, + path: &filepath, + vmFolder: &folder, + } - flags := flag.NewFlagSet("vCenter", flag.ExitOnError) - invoked := filepath.Base(os.Args[0]) - flags.Usage = func() { - fmt.Printf("USAGE: %s push vcenter [options] path \n\n", invoked) - fmt.Printf("'path' specifies the full path of an ISO image. It will be pushed to a vCenter cluster.\n") - fmt.Printf("Options:\n\n") - flags.PrintDefaults() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + // Ensure an iso has been passed to the vCenter push Command + if !strings.HasSuffix(*newVM.path, ".iso") { + log.Fatalln("Please specify an '.iso' file") + } + + // Test any passed in files before uploading image + checkFile(*newVM.path) + + // Connect to VMware vCenter and return the values needed to upload image + c, dss, _, _, _, _ := vCenterConnect(ctx, newVM) + + // Create a folder from the uploaded image name if needed + if *newVM.vmFolder == "" { + *newVM.vmFolder = strings.TrimSuffix(path.Base(*newVM.path), ".iso") + } + + // The CreateFolder method isn't necessary as the *newVM.vmname will be created automatically + uploadFile(c, newVM, dss) + + return nil + }, } - newVM.vCenterURL = flags.String("url", os.Getenv("VCURL"), "URL of VMware vCenter in the format of https://username:password@VCaddress/sdk") - newVM.dcName = flags.String("datacenter", os.Getenv("VCDATACENTER"), "The name of the DataCenter to host the image") - newVM.dsName = flags.String("datastore", os.Getenv("VCDATASTORE"), "The name of the DataStore to host the image") - newVM.vSphereHost = flags.String("hostname", os.Getenv("VCHOST"), "The server that will host the image") - newVM.path = flags.String("path", "", "Path to a specific image") + cmd.Flags().StringVar(&url, "url", os.Getenv("VCURL"), "URL of VMware vCenter in the format of https://username:password@VCaddress/sdk") + cmd.Flags().StringVar(&datacenter, "datacenter", os.Getenv("VCDATACENTER"), "The name of the DataCenter to host the image") + cmd.Flags().StringVar(&datastore, "datastore", os.Getenv("VCDATASTORE"), "The name of the DataStore to host the image") + cmd.Flags().StringVar(&hostname, "hostname", os.Getenv("VCHOST"), "The server that will host the image") + cmd.Flags().StringVar(&folder, "folder", "", "A folder on the datastore to push the image too") - newVM.vmFolder = flags.String("folder", "", "A folder on the datastore to push the image too") - - if err := flags.Parse(args); err != nil { - log.Fatalln("Unable to parse args") - } - - remArgs := flags.Args() - if len(remArgs) == 0 { - fmt.Printf("Please specify the path to the image to push\n") - flags.Usage() - os.Exit(1) - } - *newVM.path = remArgs[0] - - // Ensure an iso has been passed to the vCenter push Command - if !strings.HasSuffix(*newVM.path, ".iso") { - log.Fatalln("Please specify an '.iso' file") - } - - // Test any passed in files before uploading image - checkFile(*newVM.path) - - // Connect to VMware vCenter and return the values needed to upload image - c, dss, _, _, _, _ := vCenterConnect(ctx, newVM) - - // Create a folder from the uploaded image name if needed - if *newVM.vmFolder == "" { - *newVM.vmFolder = strings.TrimSuffix(path.Base(*newVM.path), ".iso") - } - - // The CreateFolder method isn't necessary as the *newVM.vmname will be created automatically - uploadFile(c, newVM, dss) + return cmd } func checkFile(file string) { diff --git a/src/cmd/linuxkit/run.go b/src/cmd/linuxkit/run.go index 3d9296615..4dce487b8 100644 --- a/src/cmd/linuxkit/run.go +++ b/src/cmd/linuxkit/run.go @@ -2,88 +2,72 @@ package main import ( "fmt" - "os" - "path/filepath" "runtime" - log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) -func runUsage() { - invoked := filepath.Base(os.Args[0]) - fmt.Printf("USAGE: %s run [backend] [options] [prefix]\n\n", invoked) +var ( + cpus int + mem int + disks Disks +) - fmt.Printf("'backend' specifies the run backend.\n") - fmt.Printf("If not specified the platform specific default will be used\n") - fmt.Printf("Supported backends are (default platform in brackets):\n") - // Please keep these in alphabetical order - fmt.Printf(" aws\n") - fmt.Printf(" azure\n") - fmt.Printf(" gcp\n") - fmt.Printf(" virtualization [macOS]\n") - fmt.Printf(" hyperkit\n") - fmt.Printf(" hyperv [Windows]\n") - fmt.Printf(" openstack\n") - fmt.Printf(" packet\n") - fmt.Printf(" qemu [linux]\n") - fmt.Printf(" scaleway\n") - fmt.Printf(" vbox\n") - fmt.Printf(" vcenter\n") - fmt.Printf(" vmware\n") - fmt.Printf("\n") - fmt.Printf("'options' are the backend specific options.\n") - fmt.Printf("See '%s run [backend] --help' for details.\n\n", invoked) - fmt.Printf("'prefix' specifies the path to the VM image.\n") - fmt.Printf("It defaults to './image'.\n") -} +func runCmd() *cobra.Command { -func run(args []string) { - if len(args) < 1 { - runUsage() - os.Exit(1) + cmd := &cobra.Command{ + Use: "run", + Short: "run a VM image", + Long: `Run a VM image. + + 'backend' specifies the run backend. + If the backend is not specified, the platform specific default will be used. + + 'prefix' specifies the path to the image. + If the image is not specified, the default is './image'. + `, + Example: `run [options] [backend] [prefix]`, + RunE: func(cmd *cobra.Command, args []string) error { + var target string + switch runtime.GOOS { + case "darwin": + target = "virtualization" + case "linux": + target = "qemu" + case "windows": + target = "hyperv" + default: + return fmt.Errorf("there currently is no default 'run' backend for your platform.") + } + children := cmd.Commands() + for _, child := range children { + if child.Name() == target { + return child.RunE(cmd, args) + } + } + + return fmt.Errorf("could not find default for your platform: %s", target) + }, } - switch args[0] { // Please keep cases in alphabetical order - case "aws": - runAWS(args[1:]) - case "azure": - runAzure(args[1:]) - case "gcp": - runGcp(args[1:]) - case "help", "-h", "-help", "--help": - runUsage() - os.Exit(0) - case "hyperkit": - runHyperKit(args[1:]) - case "virtualization": - runVirtualizationFramework(args[1:]) - case "hyperv": - runHyperV(args[1:]) - case "openstack": - runOpenStack(args[1:]) - case "packet": - runPacket(args[1:]) - case "qemu": - runQemu(args[1:]) - case "scaleway": - runScaleway(args[1:]) - case "vmware": - runVMware(args[1:]) - case "vbox": - runVbox(args[1:]) - case "vcenter": - runVcenter(args[1:]) - default: - switch runtime.GOOS { - case "darwin": - runVirtualizationFramework(args) - case "linux": - runQemu(args) - case "windows": - runHyperV(args) - default: - log.Errorf("There currently is no default 'run' backend for your platform.") - } - } + cmd.AddCommand(runAWSCmd()) + cmd.AddCommand(runAzureCmd()) + cmd.AddCommand(runGCPCmd()) + cmd.AddCommand(runHyperkitCmd()) + cmd.AddCommand(runVirtualizationFrameworkCmd()) + cmd.AddCommand(runHyperVCmd()) + cmd.AddCommand(runOpenStackCmd()) + cmd.AddCommand(runPacketCmd()) + cmd.AddCommand(runQEMUCmd()) + cmd.AddCommand(runScalewayCmd()) + cmd.AddCommand(runVMWareCmd()) + cmd.AddCommand(runVBoxCmd()) + cmd.AddCommand(runVCenterCmd()) + + cmd.PersistentFlags().IntVar(&cpus, "cpus", 1, "Number of CPUs") + cmd.PersistentFlags().IntVar(&mem, "mem", 1024, "Amount of memory in MB") + cmd.PersistentFlags().Var(&disks, "disk", "Disk config. [file=]path[,size=1G]") + + return cmd } diff --git a/src/cmd/linuxkit/run_aws.go b/src/cmd/linuxkit/run_aws.go index a8738fe1e..d19d1c986 100644 --- a/src/cmd/linuxkit/run_aws.go +++ b/src/cmd/linuxkit/run_aws.go @@ -2,15 +2,14 @@ package main import ( "encoding/base64" - "flag" "fmt" "os" - "path/filepath" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ec2" log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) const ( @@ -25,188 +24,191 @@ const ( awsZoneVar = "AWS_ZONE" // non-standard ) -// Process the run arguments and execute run -func runAWS(args []string) { - flags := flag.NewFlagSet("aws", flag.ExitOnError) - invoked := filepath.Base(os.Args[0]) - flags.Usage = func() { - fmt.Printf("USAGE: %s run aws [options] [name]\n\n", invoked) - fmt.Printf("'name' is the name of an AWS image that has already been\n") - fmt.Printf(" uploaded using 'linuxkit push'\n\n") - fmt.Printf("Options:\n\n") - flags.PrintDefaults() - } - machineFlag := flags.String("machine", defaultAWSMachine, "AWS Machine Type") - diskSizeFlag := flags.Int("disk-size", 0, "Size of system disk in GB") - diskTypeFlag := flags.String("disk-type", defaultAWSDiskType, "AWS Disk Type") - zoneFlag := flags.String("zone", defaultAWSZone, "AWS Availability Zone") - sgFlag := flags.String("security-group", "", "Security Group ID") +func runAWSCmd() *cobra.Command { + var ( + machineFlag string + diskSizeFlag int + diskTypeFlag string + zoneFlag string + sgFlag string - data := flags.String("data", "", "String of metadata to pass to VM; error to specify both -data and -data-file") - dataPath := flags.String("data-file", "", "Path to file containing metadata to pass to VM; error to specify both -data and -data-file") + data string + dataPath string + ) + cmd := &cobra.Command{ + Use: "aws", + Short: "launch an AWS ec2 instance using an existing image", + Long: `Launch an AWS ec2 instance using an existing image. + 'name' is the name of an AWS image that has already been uploaded to S3. + `, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + name := args[0] - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") - } + if data != "" && dataPath != "" { + log.Fatal("Cannot specify both -data and -data-file") + } - remArgs := flags.Args() - if len(remArgs) == 0 { - fmt.Printf("Please specify the name of the image to boot\n") - flags.Usage() - os.Exit(1) - } - name := remArgs[0] + if dataPath != "" { + dataB, err := os.ReadFile(dataPath) + if err != nil { + return fmt.Errorf("Unable to read metadata file: %v", err) + } + data = string(dataB) + } + // data must be base64 encoded + data = base64.StdEncoding.EncodeToString([]byte(data)) - if *data != "" && *dataPath != "" { - log.Fatal("Cannot specify both -data and -data-file") - } + machine := getStringValue(awsMachineVar, machineFlag, defaultAWSMachine) + diskSize := getIntValue(awsDiskSizeVar, diskSizeFlag, defaultAWSDiskSize) + diskType := getStringValue(awsDiskTypeVar, diskTypeFlag, defaultAWSDiskType) + zone := os.Getenv("AWS_REGION") + getStringValue(awsZoneVar, zoneFlag, defaultAWSZone) - if *dataPath != "" { - dataB, err := os.ReadFile(*dataPath) - if err != nil { - log.Fatalf("Unable to read metadata file: %v", err) - } - *data = string(dataB) - } - // data must be base64 encoded - *data = base64.StdEncoding.EncodeToString([]byte(*data)) + sess := session.Must(session.NewSession()) + compute := ec2.New(sess) - machine := getStringValue(awsMachineVar, *machineFlag, defaultAWSMachine) - diskSize := getIntValue(awsDiskSizeVar, *diskSizeFlag, defaultAWSDiskSize) - diskType := getStringValue(awsDiskTypeVar, *diskTypeFlag, defaultAWSDiskType) - zone := os.Getenv("AWS_REGION") + getStringValue(awsZoneVar, *zoneFlag, defaultAWSZone) - - sess := session.Must(session.NewSession()) - compute := ec2.New(sess) - - // 1. Find AMI - filter := &ec2.DescribeImagesInput{ - Filters: []*ec2.Filter{ - { - Name: aws.String("name"), - Values: []*string{aws.String(name)}, - }, - }, - } - results, err := compute.DescribeImages(filter) - if err != nil { - log.Fatalf("Unable to describe images: %s", err) - } - if len(results.Images) < 1 { - log.Fatalf("Unable to find image with name %s", name) - } - if len(results.Images) > 1 { - log.Warnf("Found multiple images with the same name, using the first one") - } - imageID := results.Images[0].ImageId - - // 2. Create Instance - params := &ec2.RunInstancesInput{ - ImageId: imageID, - InstanceType: aws.String(machine), - MinCount: aws.Int64(1), - MaxCount: aws.Int64(1), - Placement: &ec2.Placement{ - AvailabilityZone: aws.String(zone), - }, - SecurityGroupIds: []*string{sgFlag}, - UserData: data, - } - runResult, err := compute.RunInstances(params) - if err != nil { - log.Fatalf("Unable to run instance: %s", err) - - } - instanceID := runResult.Instances[0].InstanceId - log.Infof("Created instance %s", *instanceID) - - instanceFilter := &ec2.DescribeInstancesInput{ - Filters: []*ec2.Filter{ - { - Name: aws.String("instance-id"), - Values: []*string{instanceID}, - }, - }, - } - - if err = compute.WaitUntilInstanceRunning(instanceFilter); err != nil { - log.Fatalf("Error waiting for instance to start: %s", err) - } - log.Infof("Instance %s is running", *instanceID) - - if diskSize > 0 { - // 3. Create EBS Volume - diskParams := &ec2.CreateVolumeInput{ - AvailabilityZone: aws.String(zone), - Size: aws.Int64(int64(diskSize)), - VolumeType: aws.String(diskType), - } - log.Debugf("CreateVolume:\n%v\n", diskParams) - - volume, err := compute.CreateVolume(diskParams) - if err != nil { - log.Fatalf("Error creating volume: %s", err) - } - - waitVol := &ec2.DescribeVolumesInput{ - Filters: []*ec2.Filter{ - { - Name: aws.String("volume-id"), - Values: []*string{volume.VolumeId}, + // 1. Find AMI + filter := &ec2.DescribeImagesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("name"), + Values: []*string{aws.String(name)}, + }, }, - }, - } + } + results, err := compute.DescribeImages(filter) + if err != nil { + return fmt.Errorf("Unable to describe images: %s", err) + } + if len(results.Images) < 1 { + return fmt.Errorf("Unable to find image with name %s", name) + } + if len(results.Images) > 1 { + log.Warnf("Found multiple images with the same name, using the first one") + } + imageID := results.Images[0].ImageId - log.Infof("Waiting for volume %s to be available", *volume.VolumeId) + // 2. Create Instance + params := &ec2.RunInstancesInput{ + ImageId: imageID, + InstanceType: aws.String(machine), + MinCount: aws.Int64(1), + MaxCount: aws.Int64(1), + Placement: &ec2.Placement{ + AvailabilityZone: aws.String(zone), + }, + SecurityGroupIds: []*string{&sgFlag}, + UserData: &data, + } + runResult, err := compute.RunInstances(params) + if err != nil { + return fmt.Errorf("Unable to run instance: %s", err) - if err := compute.WaitUntilVolumeAvailable(waitVol); err != nil { - log.Fatalf("Error waiting for volume to be available: %s", err) - } + } + instanceID := runResult.Instances[0].InstanceId + log.Infof("Created instance %s", *instanceID) - log.Infof("Attaching volume %s to instance %s", *volume.VolumeId, *instanceID) - volParams := &ec2.AttachVolumeInput{ - Device: aws.String("/dev/sda2"), - InstanceId: instanceID, - VolumeId: volume.VolumeId, - } - _, err = compute.AttachVolume(volParams) - if err != nil { - log.Fatalf("Error attaching volume to instance: %s", err) - } + instanceFilter := &ec2.DescribeInstancesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("instance-id"), + Values: []*string{instanceID}, + }, + }, + } + + if err = compute.WaitUntilInstanceRunning(instanceFilter); err != nil { + return fmt.Errorf("Error waiting for instance to start: %s", err) + } + log.Infof("Instance %s is running", *instanceID) + + if diskSize > 0 { + // 3. Create EBS Volume + diskParams := &ec2.CreateVolumeInput{ + AvailabilityZone: aws.String(zone), + Size: aws.Int64(int64(diskSize)), + VolumeType: aws.String(diskType), + } + log.Debugf("CreateVolume:\n%v\n", diskParams) + + volume, err := compute.CreateVolume(diskParams) + if err != nil { + return fmt.Errorf("Error creating volume: %s", err) + } + + waitVol := &ec2.DescribeVolumesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("volume-id"), + Values: []*string{volume.VolumeId}, + }, + }, + } + + log.Infof("Waiting for volume %s to be available", *volume.VolumeId) + + if err := compute.WaitUntilVolumeAvailable(waitVol); err != nil { + return fmt.Errorf("Error waiting for volume to be available: %s", err) + } + + log.Infof("Attaching volume %s to instance %s", *volume.VolumeId, *instanceID) + volParams := &ec2.AttachVolumeInput{ + Device: aws.String("/dev/sda2"), + InstanceId: instanceID, + VolumeId: volume.VolumeId, + } + _, err = compute.AttachVolume(volParams) + if err != nil { + return fmt.Errorf("Error attaching volume to instance: %s", err) + } + } + + log.Warnf("AWS doesn't stream serial console output.\n Please use the AWS Management Console to obtain this output \n Console output will be displayed when the instance has been stopped.") + log.Warn("Waiting for instance to stop...") + + if err = compute.WaitUntilInstanceStopped(instanceFilter); err != nil { + return fmt.Errorf("Error waiting for instance to stop: %s", err) + } + + consoleParams := &ec2.GetConsoleOutputInput{ + InstanceId: instanceID, + } + output, err := compute.GetConsoleOutput(consoleParams) + if err != nil { + return fmt.Errorf("Error getting output from instance %s: %s", *instanceID, err) + } + + if output.Output == nil { + log.Warn("No Console Output found") + } else { + out, err := base64.StdEncoding.DecodeString(*output.Output) + if err != nil { + return fmt.Errorf("Error decoding output: %s", err) + } + fmt.Printf(string(out) + "\n") + } + log.Infof("Terminating instance %s", *instanceID) + terminateParams := &ec2.TerminateInstancesInput{ + InstanceIds: []*string{instanceID}, + } + if _, err := compute.TerminateInstances(terminateParams); err != nil { + return fmt.Errorf("Error terminating instance %s", *instanceID) + } + if err = compute.WaitUntilInstanceTerminated(instanceFilter); err != nil { + return fmt.Errorf("Error waiting for instance to terminate: %s", err) + } + + return nil + }, } - log.Warnf("AWS doesn't stream serial console output.\n Please use the AWS Management Console to obtain this output \n Console output will be displayed when the instance has been stopped.") - log.Warn("Waiting for instance to stop...") + cmd.Flags().StringVar(&machineFlag, "machine", defaultAWSMachine, "AWS Machine Type") + cmd.Flags().IntVar(&diskSizeFlag, "disk-size", 0, "Size of system disk in GB") + cmd.Flags().StringVar(&diskTypeFlag, "disk-type", defaultAWSDiskType, "AWS Disk Type") + cmd.Flags().StringVar(&zoneFlag, "zone", defaultAWSZone, "AWS Availability Zone") + cmd.Flags().StringVar(&sgFlag, "security-group", "", "Security Group ID") + cmd.Flags().StringVar(&data, "data", "", "String of metadata to pass to VM; error to specify both -data and -data-file") + cmd.Flags().StringVar(&dataPath, "data-file", "", "Path to file containing metadata to pass to VM; error to specify both -data and -data-file") - if err = compute.WaitUntilInstanceStopped(instanceFilter); err != nil { - log.Fatalf("Error waiting for instance to stop: %s", err) - } - - consoleParams := &ec2.GetConsoleOutputInput{ - InstanceId: instanceID, - } - output, err := compute.GetConsoleOutput(consoleParams) - if err != nil { - log.Fatalf("Error getting output from instance %s: %s", *instanceID, err) - } - - if output.Output == nil { - log.Warn("No Console Output found") - } else { - out, err := base64.StdEncoding.DecodeString(*output.Output) - if err != nil { - log.Fatalf("Error decoding output: %s", err) - } - fmt.Printf(string(out) + "\n") - } - log.Infof("Terminating instance %s", *instanceID) - terminateParams := &ec2.TerminateInstancesInput{ - InstanceIds: []*string{instanceID}, - } - if _, err := compute.TerminateInstances(terminateParams); err != nil { - log.Fatalf("Error terminating instance %s", *instanceID) - } - if err = compute.WaitUntilInstanceTerminated(instanceFilter); err != nil { - log.Fatalf("Error waiting for instance to terminate: %s", err) - } + return cmd } diff --git a/src/cmd/linuxkit/run_azure.go b/src/cmd/linuxkit/run_azure.go index 7048389f1..a8d2deb2e 100644 --- a/src/cmd/linuxkit/run_azure.go +++ b/src/cmd/linuxkit/run_azure.go @@ -1,13 +1,11 @@ package main import ( - "flag" "fmt" - "log" "math/rand" - "os" - "path/filepath" "time" + + "github.com/spf13/cobra" ) // This program requires that the following environment vars are set: @@ -19,61 +17,68 @@ import ( const defaultStorageAccountName = "linuxkit" -func runAzure(args []string) { - flags := flag.NewFlagSet("azure", flag.ExitOnError) - invoked := filepath.Base(os.Args[0]) - flags.Usage = func() { - fmt.Printf("USAGE: %s run azure [options] imagePath\n\n", invoked) - fmt.Printf("'imagePath' specifies the path (absolute or relative) of a\n") - fmt.Printf("VHD image be used as the OS image for the VM\n") - fmt.Printf("Options:\n\n") - flags.PrintDefaults() +func runAzureCmd() *cobra.Command { + var ( + resourceGroupName string + location string + accountName string + ) + + cmd := &cobra.Command{ + Use: "azure", + Short: "launch an Azure instance using an existing image", + Long: `Launch an Azure instance using an existing image. + 'imagePath' specifies the path (absolute or relative) of a VHD image to be used as the OS image for the VM. + + Relies on the following environment variables: + + AZURE_SUBSCRIPTION_ID + AZURE_TENANT_ID + AZURE_CLIENT_ID + AZURE_CLIENT_SECRET + + `, + Args: cobra.ExactArgs(1), + Example: "linuxkit run azure [options] imagePath", + RunE: func(cmd *cobra.Command, args []string) error { + imagePath := args[0] + subscriptionID := getEnvVarOrExit("AZURE_SUBSCRIPTION_ID") + tenantID := getEnvVarOrExit("AZURE_TENANT_ID") + + clientID := getEnvVarOrExit("AZURE_CLIENT_ID") + clientSecret := getEnvVarOrExit("AZURE_CLIENT_SECRET") + + rand.Seed(time.Now().UTC().UnixNano()) + virtualNetworkName := fmt.Sprintf("linuxkitvirtualnetwork%d", rand.Intn(1000)) + subnetName := fmt.Sprintf("linuxkitsubnet%d", rand.Intn(1000)) + publicIPAddressName := fmt.Sprintf("publicip%d", rand.Intn(1000)) + networkInterfaceName := fmt.Sprintf("networkinterface%d", rand.Intn(1000)) + virtualMachineName := fmt.Sprintf("linuxkitvm%d", rand.Intn(1000)) + + initializeAzureClients(subscriptionID, tenantID, clientID, clientSecret) + + group := createResourceGroup(resourceGroupName, location) + createStorageAccount(accountName, location, *group) + uploadVMImage(*group.Name, accountName, imagePath) + createVirtualNetwork(*group, virtualNetworkName, location) + subnet := createSubnet(*group, virtualNetworkName, subnetName) + publicIPAddress := createPublicIPAddress(*group, publicIPAddressName, location) + networkInterface := createNetworkInterface(*group, networkInterfaceName, *publicIPAddress, *subnet, location) + go createVirtualMachine(*group, accountName, virtualMachineName, *networkInterface, *publicIPAddress, location) + + fmt.Printf("\nStarted deployment of virtual machine %s in resource group %s", virtualMachineName, *group.Name) + + time.Sleep(time.Second * 5) + + fmt.Printf("\nNOTE: Since you created a minimal VM without the Azure Linux Agent, the portal will notify you that the deployment failed. After around 50 seconds try connecting to the VM") + fmt.Printf("\nssh -i path-to-key root@%s\n", *publicIPAddress.DNSSettings.Fqdn) + return nil + }, } - resourceGroupName := flags.String("resourceGroupName", "", "Name of resource group to be used for VM") - location := flags.String("location", "westus", "Location of the VM") - accountName := flags.String("accountName", defaultStorageAccountName, "Name of the storage account") + cmd.Flags().StringVar(&resourceGroupName, "resourceGroupName", "", "Name of resource group to be used for VM") + cmd.Flags().StringVar(&location, "location", "westus", "Location of the VM") + cmd.Flags().StringVar(&accountName, "accountName", defaultStorageAccountName, "Name of the storage account") - subscriptionID := getEnvVarOrExit("AZURE_SUBSCRIPTION_ID") - tenantID := getEnvVarOrExit("AZURE_TENANT_ID") - - clientID := getEnvVarOrExit("AZURE_CLIENT_ID") - clientSecret := getEnvVarOrExit("AZURE_CLIENT_SECRET") - - if err := flags.Parse(args); err != nil { - log.Fatalf("Unable to parse args: %s", err.Error()) - } - - remArgs := flags.Args() - if len(remArgs) == 0 { - fmt.Printf("Please specify the image to run\n") - flags.Usage() - os.Exit(1) - } - imagePath := remArgs[0] - - rand.Seed(time.Now().UTC().UnixNano()) - virtualNetworkName := fmt.Sprintf("linuxkitvirtualnetwork%d", rand.Intn(1000)) - subnetName := fmt.Sprintf("linuxkitsubnet%d", rand.Intn(1000)) - publicIPAddressName := fmt.Sprintf("publicip%d", rand.Intn(1000)) - networkInterfaceName := fmt.Sprintf("networkinterface%d", rand.Intn(1000)) - virtualMachineName := fmt.Sprintf("linuxkitvm%d", rand.Intn(1000)) - - initializeAzureClients(subscriptionID, tenantID, clientID, clientSecret) - - group := createResourceGroup(*resourceGroupName, *location) - createStorageAccount(*accountName, *location, *group) - uploadVMImage(*group.Name, *accountName, imagePath) - createVirtualNetwork(*group, virtualNetworkName, *location) - subnet := createSubnet(*group, virtualNetworkName, subnetName) - publicIPAddress := createPublicIPAddress(*group, publicIPAddressName, *location) - networkInterface := createNetworkInterface(*group, networkInterfaceName, *publicIPAddress, *subnet, *location) - go createVirtualMachine(*group, *accountName, virtualMachineName, *networkInterface, *publicIPAddress, *location) - - fmt.Printf("\nStarted deployment of virtual machine %s in resource group %s", virtualMachineName, *group.Name) - - time.Sleep(time.Second * 5) - - fmt.Printf("\nNOTE: Since you created a minimal VM without the Azure Linux Agent, the portal will notify you that the deployment failed. After around 50 seconds try connecting to the VM") - fmt.Printf("\nssh -i path-to-key root@%s\n", *publicIPAddress.DNSSettings.Fqdn) + return cmd } diff --git a/src/cmd/linuxkit/run_gcp.go b/src/cmd/linuxkit/run_gcp.go index f3580f176..4ec1d8ecf 100644 --- a/src/cmd/linuxkit/run_gcp.go +++ b/src/cmd/linuxkit/run_gcp.go @@ -1,12 +1,11 @@ package main import ( - "flag" + "errors" "fmt" "os" - "path/filepath" - log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) const ( @@ -23,81 +22,87 @@ const ( nameVar = "CLOUDSDK_IMAGE_NAME" // non-standard ) -// Process the run arguments and execute run -func runGcp(args []string) { - flags := flag.NewFlagSet("gcp", flag.ExitOnError) - invoked := filepath.Base(os.Args[0]) - flags.Usage = func() { - fmt.Printf("USAGE: %s run gcp [options] [image]\n\n", invoked) - fmt.Printf("'image' specifies either the name of an already uploaded\n") - fmt.Printf("GCP image or the full path to a image file which will be\n") - fmt.Printf("uploaded before it is run.\n\n") - fmt.Printf("Options:\n\n") - flags.PrintDefaults() - } - name := flags.String("name", "", "Machine name") - zoneFlag := flags.String("zone", defaultZone, "GCP Zone") - machineFlag := flags.String("machine", defaultMachine, "GCP Machine Type") - keysFlag := flags.String("keys", "", "Path to Service Account JSON key file") - projectFlag := flags.String("project", "", "GCP Project Name") - var disks Disks - flags.Var(&disks, "disk", "Disk config, may be repeated. [file=]diskName[,size=1G]") +func runGCPCmd() *cobra.Command { + var ( + name string + zoneFlag string + machineFlag string + keysFlag string + projectFlag string + skipCleanup bool + nestedVirt bool + vTPM bool + data string + dataPath string + ) - skipCleanup := flags.Bool("skip-cleanup", false, "Don't remove images or VMs") - nestedVirt := flags.Bool("nested-virt", false, "Enabled nested virtualization") - vTPM := flags.Bool("vtpm", false, "Enable vTPM device") + cmd := &cobra.Command{ + Use: "gcp", + Short: "launch a GCP instance", + Long: `Launch a GCP instance. + 'image' specifies either the name of an already uploaded GCP image, + or the full path to a image file which will be uploaded before it is run. + `, + Args: cobra.ExactArgs(1), + Example: "linuxkit run gcp [options] [image]", + RunE: func(cmd *cobra.Command, args []string) error { + image := args[0] - data := flags.String("data", "", "String of metadata to pass to VM; error to specify both -data and -data-file") - dataPath := flags.String("data-file", "", "Path to file containing metadata to pass to VM; error to specify both -data and -data-file") + if data != "" && dataPath != "" { + return errors.New("Cannot specify both -data and -data-file") + } - if *data != "" && *dataPath != "" { - log.Fatal("Cannot specify both -data and -data-file") + if name == "" { + name = image + } + + if dataPath != "" { + dataB, err := os.ReadFile(dataPath) + if err != nil { + return fmt.Errorf("Unable to read metadata file: %v", err) + } + data = string(dataB) + } + + zone := getStringValue(zoneVar, zoneFlag, defaultZone) + machine := getStringValue(machineVar, machineFlag, defaultMachine) + keys := getStringValue(keysVar, keysFlag, "") + project := getStringValue(projectVar, projectFlag, "") + + client, err := NewGCPClient(keys, project) + if err != nil { + return fmt.Errorf("Unable to connect to GCP: %v", err) + } + + if err = client.CreateInstance(name, image, zone, machine, disks, &data, nestedVirt, vTPM, true); err != nil { + return err + } + + if err = client.ConnectToInstanceSerialPort(name, zone); err != nil { + return err + } + + if !skipCleanup { + if err = client.DeleteInstance(name, zone, true); err != nil { + return err + } + } + + return nil + }, } - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") - } + cmd.Flags().StringVar(&name, "name", "", "Machine name") + cmd.Flags().StringVar(&zoneFlag, "zone", defaultZone, "GCP Zone") + cmd.Flags().StringVar(&machineFlag, "machine", defaultMachine, "GCP Machine Type") + cmd.Flags().StringVar(&keysFlag, "keys", "", "Path to Service Account JSON key file") + cmd.Flags().StringVar(&projectFlag, "project", "", "GCP Project Name") + cmd.Flags().BoolVar(&skipCleanup, "skip-cleanup", false, "Don't remove images or VMs") + cmd.Flags().BoolVar(&nestedVirt, "nested-virt", false, "Enabled nested virtualization") + cmd.Flags().BoolVar(&vTPM, "vtpm", false, "Enable vTPM device") - remArgs := flags.Args() - if len(remArgs) == 0 { - fmt.Printf("Please specify the name of the image to boot\n") - flags.Usage() - os.Exit(1) - } - image := remArgs[0] - if *name == "" { - *name = image - } + cmd.Flags().StringVar(&data, "data", "", "String of metadata to pass to VM; error to specify both -data and -data-file") + cmd.Flags().StringVar(&dataPath, "data-file", "", "Path to file containing metadata to pass to VM; error to specify both -data and -data-file") - if *dataPath != "" { - dataB, err := os.ReadFile(*dataPath) - if err != nil { - log.Fatalf("Unable to read metadata file: %v", err) - } - *data = string(dataB) - } - - zone := getStringValue(zoneVar, *zoneFlag, defaultZone) - machine := getStringValue(machineVar, *machineFlag, defaultMachine) - keys := getStringValue(keysVar, *keysFlag, "") - project := getStringValue(projectVar, *projectFlag, "") - - client, err := NewGCPClient(keys, project) - if err != nil { - log.Fatalf("Unable to connect to GCP: %v", err) - } - - if err = client.CreateInstance(*name, image, zone, machine, disks, data, *nestedVirt, *vTPM, true); err != nil { - log.Fatal(err) - } - - if err = client.ConnectToInstanceSerialPort(*name, zone); err != nil { - log.Fatal(err) - } - - if !*skipCleanup { - if err = client.DeleteInstance(*name, zone, true); err != nil { - log.Fatal(err) - } - } + return cmd } diff --git a/src/cmd/linuxkit/run_hyperkit.go b/src/cmd/linuxkit/run_hyperkit.go index 6ec8371f1..6790e7150 100644 --- a/src/cmd/linuxkit/run_hyperkit.go +++ b/src/cmd/linuxkit/run_hyperkit.go @@ -2,7 +2,7 @@ package main import ( "context" - "flag" + "errors" "fmt" "net" "os" @@ -16,6 +16,7 @@ import ( "github.com/moby/vpnkit/go/pkg/vmnet" "github.com/moby/vpnkit/go/pkg/vpnkit" log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) const ( @@ -30,320 +31,327 @@ func init() { hyperkit.SetLogger(log.StandardLogger()) } -// Process the run arguments and execute run -func runHyperKit(args []string) { - flags := flag.NewFlagSet("hyperkit", flag.ExitOnError) - invoked := filepath.Base(os.Args[0]) - flags.Usage = func() { - fmt.Printf("USAGE: %s run hyperkit [options] prefix\n\n", invoked) - fmt.Printf("'prefix' specifies the path to the VM image.\n") - fmt.Printf("\n") - fmt.Printf("Options:\n") - flags.PrintDefaults() - } - hyperkitPath := flags.String("hyperkit", "", "Path to hyperkit binary (if not in default location)") - cpus := flags.Int("cpus", 1, "Number of CPUs") - mem := flags.Int("mem", 1024, "Amount of memory in MB") - var disks Disks - flags.Var(&disks, "disk", "Disk config. [file=]path[,size=1G]") - data := flags.String("data", "", "String of metadata to pass to VM; error to specify both -data and -data-file") - dataPath := flags.String("data-file", "", "Path to file containing metadata to pass to VM; error to specify both -data and -data-file") +func runHyperkitCmd() *cobra.Command { + var ( + hyperkitPath string + data string + dataPath string + ipStr string + state string + vsockports string + networking string + vpnkitUUID string + vpnkitPath string + uefiBoot bool + isoBoot bool + squashFSBoot bool + kernelBoot bool + consoleToFile bool + fw string + publishFlags multipleFlag + ) + cmd := &cobra.Command{ + Use: "hyperkit", + Short: "launch a VM using hyperkit", + Long: `Launch a VM using hyperkit. + 'prefix' specifies the path to the VM image. + `, + Args: cobra.ExactArgs(1), + Example: "linuxkit run hyperkit [options] prefix", + RunE: func(cmd *cobra.Command, args []string) error { + path := args[0] - if *data != "" && *dataPath != "" { - log.Fatal("Cannot specify both -data and -data-file") + if data != "" && dataPath != "" { + return errors.New("Cannot specify both -data and -data-file") + } + + prefix := path + + _, err := os.Stat(path + "-kernel") + statKernel := err == nil + + var isoPaths []string + + switch { + case squashFSBoot: + if kernelBoot || isoBoot { + return fmt.Errorf("Please specify only one boot method") + } + if !statKernel { + return fmt.Errorf("Booting a SquashFS root filesystem requires a kernel at %s", path+"-kernel") + } + _, err = os.Stat(path + "-squashfs.img") + statSquashFS := err == nil + if !statSquashFS { + return fmt.Errorf("Cannot find SquashFS image (%s): %v", path+"-squashfs.img", err) + } + case isoBoot: + if kernelBoot { + return fmt.Errorf("Please specify only one boot method") + } + if !uefiBoot { + return fmt.Errorf("Hyperkit requires --uefi to be set to boot an ISO") + } + // We used to auto-detect ISO boot. For backwards compat, append .iso if not present + isoPath := path + if !strings.HasSuffix(isoPath, ".iso") { + isoPath += ".iso" + } + _, err = os.Stat(isoPath) + statISO := err == nil + if !statISO { + return fmt.Errorf("Cannot find ISO image (%s): %v", isoPath, err) + } + prefix = strings.TrimSuffix(path, ".iso") + isoPaths = append(isoPaths, isoPath) + default: + // Default to kernel+initrd + if !statKernel { + return fmt.Errorf("Cannot find kernel file: %s", path+"-kernel") + } + _, err = os.Stat(path + "-initrd.img") + statInitrd := err == nil + if !statInitrd { + return fmt.Errorf("Cannot find initrd file (%s): %v", path+"-initrd.img", err) + } + kernelBoot = true + } + + if uefiBoot { + _, err := os.Stat(fw) + if err != nil { + return fmt.Errorf("Cannot open UEFI firmware file (%s): %v", fw, err) + } + } + + if state == "" { + state = prefix + "-state" + } + if err := os.MkdirAll(state, 0755); err != nil { + return fmt.Errorf("Could not create state directory: %v", err) + } + + metadataPaths, err := CreateMetadataISO(state, data, dataPath) + if err != nil { + return fmt.Errorf("%v", err) + } + isoPaths = append(isoPaths, metadataPaths...) + + // Create UUID for VPNKit or reuse an existing one from state dir. IP addresses are + // assigned to the UUID, so to get the same IP we have to store the initial UUID. If + // has specified a VPNKit UUID the file is ignored. + if vpnkitUUID == "" { + vpnkitUUIDFile := filepath.Join(state, "vpnkit.uuid") + if _, err := os.Stat(vpnkitUUIDFile); os.IsNotExist(err) { + vpnkitUUID = uuid.New().String() + if err := os.WriteFile(vpnkitUUIDFile, []byte(vpnkitUUID), 0600); err != nil { + return fmt.Errorf("Unable to write to %s: %v", vpnkitUUIDFile, err) + } + } else { + uuidBytes, err := os.ReadFile(vpnkitUUIDFile) + if err != nil { + return fmt.Errorf("Unable to read VPNKit UUID from %s: %v", vpnkitUUIDFile, err) + } + if tmp, err := uuid.ParseBytes(uuidBytes); err != nil { + return fmt.Errorf("Unable to parse VPNKit UUID from %s: %v", vpnkitUUIDFile, err) + } else { + vpnkitUUID = tmp.String() + } + } + } + + // Generate new UUID, otherwise /sys/class/dmi/id/product_uuid is identical on all VMs + vmUUID := uuid.New().String() + + // Run + var cmdline string + if kernelBoot || squashFSBoot { + cmdlineBytes, err := os.ReadFile(prefix + "-cmdline") + if err != nil { + return fmt.Errorf("Cannot open cmdline file: %v", err) + } + cmdline = string(cmdlineBytes) + } + + // Create new HyperKit instance (w/o networking for now) + h, err := hyperkit.New(hyperkitPath, "", state) + if err != nil { + return fmt.Errorf("Error creating hyperkit: %w", err) + } + + if consoleToFile { + h.Console = hyperkit.ConsoleFile + } + + h.UUID = vmUUID + h.ISOImages = isoPaths + h.VSock = true + h.CPUs = cpus + h.Memory = mem + + switch { + case kernelBoot: + h.Kernel = prefix + "-kernel" + h.Initrd = prefix + "-initrd.img" + case squashFSBoot: + h.Kernel = prefix + "-kernel" + // Make sure the SquashFS image is the first disk, raw, and virtio + var rootDisk hyperkit.RawDisk + rootDisk.Path = prefix + "-squashfs.img" + rootDisk.Trim = false // This happens to select 'virtio-blk' + h.Disks = append(h.Disks, &rootDisk) + cmdline = cmdline + " root=/dev/vda" + default: + h.Bootrom = fw + } + + for i, d := range disks { + id := "" + if i != 0 { + id = strconv.Itoa(i) + } + if d.Size != 0 && d.Path == "" { + d.Path = filepath.Join(state, "disk"+id+".raw") + } + if d.Path == "" { + return fmt.Errorf("disk specified with no size or name") + } + hd, err := hyperkit.NewDisk(d.Path, d.Size) + if err != nil { + return fmt.Errorf("NewDisk failed: %v", err) + } + h.Disks = append(h.Disks, hd) + } + + if h.VSockPorts, err = stringToIntArray(vsockports, ","); err != nil { + return fmt.Errorf("Unable to parse vsock-ports: %w", err) + } + + // Select network mode + var vpnkitProcess *os.Process + var vpnkitPortSocket string + if networking == "" || networking == "default" { + dflt := hyperkitNetworkingDefault + networking = dflt + } + netMode := strings.SplitN(networking, ",", 3) + switch netMode[0] { + case hyperkitNetworkingDockerForMac: + oldEthSock := filepath.Join(os.Getenv("HOME"), "Library/Containers/com.docker.docker/Data/s50") + oldPortSock := filepath.Join(os.Getenv("HOME"), "Library/Containers/com.docker.docker/Data/s51") + newEthSock := filepath.Join(os.Getenv("HOME"), "Library/Containers/com.docker.docker/Data/vpnkit.eth.sock") + newPortSock := filepath.Join(os.Getenv("HOME"), "Library/Containers/com.docker.docker/Data/vpnkit.port.sock") + _, err := os.Stat(oldEthSock) + if err == nil { + h.VPNKitSock = oldEthSock + vpnkitPortSocket = oldPortSock + } else { + _, err = os.Stat(newEthSock) + if err != nil { + return errors.New("Cannot find Docker for Mac network sockets. Install Docker or use a different network mode.") + } + h.VPNKitSock = newEthSock + vpnkitPortSocket = newPortSock + } + case hyperkitNetworkingVPNKit: + if len(netMode) > 1 { + // Socket path specified, try to use existing VPNKit instance + h.VPNKitSock = netMode[1] + if len(netMode) > 2 { + vpnkitPortSocket = netMode[2] + } + // The guest will use this 9P mount to configure which ports to forward + h.Sockets9P = []hyperkit.Socket9P{{Path: vpnkitPortSocket, Tag: "port"}} + // VSOCK port 62373 is used to pass traffic from host->guest + h.VSockPorts = append(h.VSockPorts, 62373) + } else { + // Start new VPNKit instance + h.VPNKitSock = filepath.Join(state, "vpnkit_eth.sock") + vpnkitPortSocket = filepath.Join(state, "vpnkit_port.sock") + vsockSocket := filepath.Join(state, "connect") + vpnkitProcess, err = launchVPNKit(vpnkitPath, h.VPNKitSock, vsockSocket, vpnkitPortSocket) + if err != nil { + return fmt.Errorf("Unable to start vpnkit: %w", err) + } + defer shutdownVPNKit(vpnkitProcess) + log.RegisterExitHandler(func() { + shutdownVPNKit(vpnkitProcess) + }) + // The guest will use this 9P mount to configure which ports to forward + h.Sockets9P = []hyperkit.Socket9P{{Path: vpnkitPortSocket, Tag: "port"}} + // VSOCK port 62373 is used to pass traffic from host->guest + h.VSockPorts = append(h.VSockPorts, 62373) + } + case hyperkitNetworkingVMNet: + h.VPNKitSock = "" + h.VMNet = true + case hyperkitNetworkingNone: + h.VPNKitSock = "" + default: + return fmt.Errorf("Invalid networking mode: %s", netMode[0]) + } + + h.VPNKitUUID = vpnkitUUID + if ipStr != "" { + if ip := net.ParseIP(ipStr); len(ip) > 0 && ip.To4() != nil { + h.VPNKitPreferredIPv4 = ip.String() + } else { + return fmt.Errorf("Unable to parse IPv4 address: %v", ipStr) + } + } + + // Publish ports if requested and VPNKit is used + if len(publishFlags) != 0 { + switch netMode[0] { + case hyperkitNetworkingDockerForMac, hyperkitNetworkingVPNKit: + if vpnkitPortSocket == "" { + return fmt.Errorf("The VPNKit Port socket path is required to publish ports") + } + f, err := vpnkitPublishPorts(h, publishFlags, vpnkitPortSocket) + if err != nil { + return fmt.Errorf("Publish ports failed with: %v", err) + } + defer f() + log.RegisterExitHandler(f) + default: + return fmt.Errorf("Port publishing requires %q or %q networking mode", hyperkitNetworkingDockerForMac, hyperkitNetworkingVPNKit) + } + } + + err = h.Run(cmdline) + if err != nil { + return fmt.Errorf("Cannot run hyperkit: %v", err) + } + return nil + }, } - ipStr := flags.String("ip", "", "Preferred IPv4 address for the VM.") - state := flags.String("state", "", "Path to directory to keep VM state in") - vsockports := flags.String("vsock-ports", "", "List of vsock ports to forward from the guest on startup (comma separated). A unix domain socket for each port will be created in the state directory") - networking := flags.String("networking", hyperkitNetworkingDefault, "Networking mode. Valid options are 'default', 'docker-for-mac', 'vpnkit[,eth-socket-path[,port-socket-path]]', 'vmnet' and 'none'. 'docker-for-mac' connects to the network used by Docker for Mac. 'vpnkit' connects to the VPNKit socket(s) specified. If no socket path is provided a new VPNKit instance will be started and 'vpnkit_eth.sock' and 'vpnkit_port.sock' will be created in the state directory. 'port-socket-path' is only needed if you want to publish ports on localhost using an existing VPNKit instance. 'vmnet' uses the Apple vmnet framework, requires root/sudo. 'none' disables networking.`") + cmd.Flags().StringVar(&hyperkitPath, "hyperkit", "", "Path to hyperkit binary (if not in default location)") + cmd.Flags().StringVar(&data, "data", "", "String of metadata to pass to VM; error to specify both -data and -data-file") + cmd.Flags().StringVar(&dataPath, "data-file", "", "Path to file containing metadata to pass to VM; error to specify both -data and -data-file") + cmd.Flags().StringVar(&ipStr, "ip", "", "Preferred IPv4 address for the VM.") + cmd.Flags().StringVar(&state, "state", "", "Path to directory to keep VM state in") + cmd.Flags().StringVar(&vsockports, "vsock-ports", "", "List of vsock ports to forward from the guest on startup (comma separated). A unix domain socket for each port will be created in the state directory") + cmd.Flags().StringVar(&networking, "networking", hyperkitNetworkingDefault, "Networking mode. Valid options are 'default', 'docker-for-mac', 'vpnkit[,eth-socket-path[,port-socket-path]]', 'vmnet' and 'none'. 'docker-for-mac' connects to the network used by Docker for Mac. 'vpnkit' connects to the VPNKit socket(s) specified. If no socket path is provided a new VPNKit instance will be started and 'vpnkit_eth.sock' and 'vpnkit_port.sock' will be created in the state directory. 'port-socket-path' is only needed if you want to publish ports on localhost using an existing VPNKit instance. 'vmnet' uses the Apple vmnet framework, requires root/sudo. 'none' disables networking.`") - vpnkitUUID := flags.String("vpnkit-uuid", "", "Optional UUID used to identify the VPNKit connection. Overrides 'vpnkit.uuid' in the state directory.") - vpnkitPath := flags.String("vpnkit", "", "Path to vpnkit binary") - publishFlags := multipleFlag{} - flags.Var(&publishFlags, "publish", "Publish a vm's port(s) to the host (default [])") + cmd.Flags().StringVar(&vpnkitUUID, "vpnkit-uuid", "", "Optional UUID used to identify the VPNKit connection. Overrides 'vpnkit.uuid' in the state directory.") + cmd.Flags().StringVar(&vpnkitPath, "vpnkit", "", "Path to vpnkit binary") + cmd.Flags().Var(&publishFlags, "publish", "Publish a vm's port(s) to the host (default [])") // Boot type; we try to determine automatically - uefiBoot := flags.Bool("uefi", false, "Use UEFI boot") - isoBoot := flags.Bool("iso", false, "Boot image is an ISO") - squashFSBoot := flags.Bool("squashfs", false, "Boot image is a kernel+squashfs+cmdline") - kernelBoot := flags.Bool("kernel", false, "Boot image is kernel+initrd+cmdline 'path'-kernel/-initrd/-cmdline") + cmd.Flags().BoolVar(&uefiBoot, "uefi", false, "Use UEFI boot") + cmd.Flags().BoolVar(&isoBoot, "iso", false, "Boot image is an ISO") + cmd.Flags().BoolVar(&squashFSBoot, "squashfs", false, "Boot image is a kernel+squashfs+cmdline") + cmd.Flags().BoolVar(&kernelBoot, "kernel", false, "Boot image is kernel+initrd+cmdline 'path'-kernel/-initrd/-cmdline") // Hyperkit settings - consoleToFile := flags.Bool("console-file", false, "Output the console to a tty file") + cmd.Flags().BoolVar(&consoleToFile, "console-file", false, "Output the console to a tty file") // Paths and settings for UEFI firmware // Note, the default uses the firmware shipped with Docker for Mac - fw := flags.String("fw", "/Applications/Docker.app/Contents/Resources/uefi/UEFI.fd", "Path to OVMF firmware for UEFI boot") + cmd.Flags().StringVar(&fw, "fw", "/Applications/Docker.app/Contents/Resources/uefi/UEFI.fd", "Path to OVMF firmware for UEFI boot") - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") - } - remArgs := flags.Args() - if len(remArgs) == 0 { - fmt.Println("Please specify the prefix to the image to boot") - flags.Usage() - os.Exit(1) - } - path := remArgs[0] - prefix := path - - _, err := os.Stat(path + "-kernel") - statKernel := err == nil - - var isoPaths []string - - switch { - case *squashFSBoot: - if *kernelBoot || *isoBoot { - log.Fatalf("Please specify only one boot method") - } - if !statKernel { - log.Fatalf("Booting a SquashFS root filesystem requires a kernel at %s", path+"-kernel") - } - _, err = os.Stat(path + "-squashfs.img") - statSquashFS := err == nil - if !statSquashFS { - log.Fatalf("Cannot find SquashFS image (%s): %v", path+"-squashfs.img", err) - } - case *isoBoot: - if *kernelBoot { - log.Fatalf("Please specify only one boot method") - } - if !*uefiBoot { - log.Fatalf("Hyperkit requires --uefi to be set to boot an ISO") - } - // We used to auto-detect ISO boot. For backwards compat, append .iso if not present - isoPath := path - if !strings.HasSuffix(isoPath, ".iso") { - isoPath += ".iso" - } - _, err = os.Stat(isoPath) - statISO := err == nil - if !statISO { - log.Fatalf("Cannot find ISO image (%s): %v", isoPath, err) - } - prefix = strings.TrimSuffix(path, ".iso") - isoPaths = append(isoPaths, isoPath) - default: - // Default to kernel+initrd - if !statKernel { - log.Fatalf("Cannot find kernel file: %s", path+"-kernel") - } - _, err = os.Stat(path + "-initrd.img") - statInitrd := err == nil - if !statInitrd { - log.Fatalf("Cannot find initrd file (%s): %v", path+"-initrd.img", err) - } - *kernelBoot = true - } - - if *uefiBoot { - _, err := os.Stat(*fw) - if err != nil { - log.Fatalf("Cannot open UEFI firmware file (%s): %v", *fw, err) - } - } - - if *state == "" { - *state = prefix + "-state" - } - if err := os.MkdirAll(*state, 0755); err != nil { - log.Fatalf("Could not create state directory: %v", err) - } - - metadataPaths, err := CreateMetadataISO(*state, *data, *dataPath) - if err != nil { - log.Fatalf("%v", err) - } - isoPaths = append(isoPaths, metadataPaths...) - - // Create UUID for VPNKit or reuse an existing one from state dir. IP addresses are - // assigned to the UUID, so to get the same IP we have to store the initial UUID. If - // has specified a VPNKit UUID the file is ignored. - if *vpnkitUUID == "" { - vpnkitUUIDFile := filepath.Join(*state, "vpnkit.uuid") - if _, err := os.Stat(vpnkitUUIDFile); os.IsNotExist(err) { - *vpnkitUUID = uuid.New().String() - if err := os.WriteFile(vpnkitUUIDFile, []byte(*vpnkitUUID), 0600); err != nil { - log.Fatalf("Unable to write to %s: %v", vpnkitUUIDFile, err) - } - } else { - uuidBytes, err := os.ReadFile(vpnkitUUIDFile) - if err != nil { - log.Fatalf("Unable to read VPNKit UUID from %s: %v", vpnkitUUIDFile, err) - } - if tmp, err := uuid.ParseBytes(uuidBytes); err != nil { - log.Fatalf("Unable to parse VPNKit UUID from %s: %v", vpnkitUUIDFile, err) - } else { - *vpnkitUUID = tmp.String() - } - - } - } - - // Generate new UUID, otherwise /sys/class/dmi/id/product_uuid is identical on all VMs - vmUUID := uuid.New().String() - - // Run - var cmdline string - if *kernelBoot || *squashFSBoot { - cmdlineBytes, err := os.ReadFile(prefix + "-cmdline") - if err != nil { - log.Fatalf("Cannot open cmdline file: %v", err) - } - cmdline = string(cmdlineBytes) - } - - // Create new HyperKit instance (w/o networking for now) - h, err := hyperkit.New(*hyperkitPath, "", *state) - if err != nil { - log.Fatalln("Error creating hyperkit: ", err) - } - - if *consoleToFile { - h.Console = hyperkit.ConsoleFile - } - - h.UUID = vmUUID - h.ISOImages = isoPaths - h.VSock = true - h.CPUs = *cpus - h.Memory = *mem - - switch { - case *kernelBoot: - h.Kernel = prefix + "-kernel" - h.Initrd = prefix + "-initrd.img" - case *squashFSBoot: - h.Kernel = prefix + "-kernel" - // Make sure the SquashFS image is the first disk, raw, and virtio - var rootDisk hyperkit.RawDisk - rootDisk.Path = prefix + "-squashfs.img" - rootDisk.Trim = false // This happens to select 'virtio-blk' - h.Disks = append(h.Disks, &rootDisk) - cmdline = cmdline + " root=/dev/vda" - default: - h.Bootrom = *fw - } - - for i, d := range disks { - id := "" - if i != 0 { - id = strconv.Itoa(i) - } - if d.Size != 0 && d.Path == "" { - d.Path = filepath.Join(*state, "disk"+id+".raw") - } - if d.Path == "" { - log.Fatalf("disk specified with no size or name") - } - hd, err := hyperkit.NewDisk(d.Path, d.Size) - if err != nil { - log.Fatalf("NewDisk failed: %v", err) - } - h.Disks = append(h.Disks, hd) - } - - if h.VSockPorts, err = stringToIntArray(*vsockports, ","); err != nil { - log.Fatalln("Unable to parse vsock-ports: ", err) - } - - // Select network mode - var vpnkitProcess *os.Process - var vpnkitPortSocket string - if *networking == "" || *networking == "default" { - dflt := hyperkitNetworkingDefault - networking = &dflt - } - netMode := strings.SplitN(*networking, ",", 3) - switch netMode[0] { - case hyperkitNetworkingDockerForMac: - oldEthSock := filepath.Join(os.Getenv("HOME"), "Library/Containers/com.docker.docker/Data/s50") - oldPortSock := filepath.Join(os.Getenv("HOME"), "Library/Containers/com.docker.docker/Data/s51") - newEthSock := filepath.Join(os.Getenv("HOME"), "Library/Containers/com.docker.docker/Data/vpnkit.eth.sock") - newPortSock := filepath.Join(os.Getenv("HOME"), "Library/Containers/com.docker.docker/Data/vpnkit.port.sock") - _, err := os.Stat(oldEthSock) - if err == nil { - h.VPNKitSock = oldEthSock - vpnkitPortSocket = oldPortSock - } else { - _, err = os.Stat(newEthSock) - if err != nil { - log.Fatalln("Cannot find Docker for Mac network sockets. Install Docker or use a different network mode.") - } - h.VPNKitSock = newEthSock - vpnkitPortSocket = newPortSock - } - case hyperkitNetworkingVPNKit: - if len(netMode) > 1 { - // Socket path specified, try to use existing VPNKit instance - h.VPNKitSock = netMode[1] - if len(netMode) > 2 { - vpnkitPortSocket = netMode[2] - } - // The guest will use this 9P mount to configure which ports to forward - h.Sockets9P = []hyperkit.Socket9P{{Path: vpnkitPortSocket, Tag: "port"}} - // VSOCK port 62373 is used to pass traffic from host->guest - h.VSockPorts = append(h.VSockPorts, 62373) - } else { - // Start new VPNKit instance - h.VPNKitSock = filepath.Join(*state, "vpnkit_eth.sock") - vpnkitPortSocket = filepath.Join(*state, "vpnkit_port.sock") - vsockSocket := filepath.Join(*state, "connect") - vpnkitProcess, err = launchVPNKit(*vpnkitPath, h.VPNKitSock, vsockSocket, vpnkitPortSocket) - if err != nil { - log.Fatalln("Unable to start vpnkit: ", err) - } - defer shutdownVPNKit(vpnkitProcess) - log.RegisterExitHandler(func() { - shutdownVPNKit(vpnkitProcess) - }) - // The guest will use this 9P mount to configure which ports to forward - h.Sockets9P = []hyperkit.Socket9P{{Path: vpnkitPortSocket, Tag: "port"}} - // VSOCK port 62373 is used to pass traffic from host->guest - h.VSockPorts = append(h.VSockPorts, 62373) - } - case hyperkitNetworkingVMNet: - h.VPNKitSock = "" - h.VMNet = true - case hyperkitNetworkingNone: - h.VPNKitSock = "" - default: - log.Fatalf("Invalid networking mode: %s", netMode[0]) - } - - h.VPNKitUUID = *vpnkitUUID - if *ipStr != "" { - if ip := net.ParseIP(*ipStr); len(ip) > 0 && ip.To4() != nil { - h.VPNKitPreferredIPv4 = ip.String() - } else { - log.Fatalf("Unable to parse IPv4 address: %v", *ipStr) - } - } - - // Publish ports if requested and VPNKit is used - if len(publishFlags) != 0 { - switch netMode[0] { - case hyperkitNetworkingDockerForMac, hyperkitNetworkingVPNKit: - if vpnkitPortSocket == "" { - log.Fatalf("The VPNKit Port socket path is required to publish ports") - } - f, err := vpnkitPublishPorts(h, publishFlags, vpnkitPortSocket) - if err != nil { - log.Fatalf("Publish ports failed with: %v", err) - } - defer f() - log.RegisterExitHandler(f) - default: - log.Fatalf("Port publishing requires %q or %q networking mode", hyperkitNetworkingDockerForMac, hyperkitNetworkingVPNKit) - } - } - - err = h.Run(cmdline) - if err != nil { - log.Fatalf("Cannot run hyperkit: %v", err) - } + return cmd } func shutdownVPNKit(process *os.Process) { diff --git a/src/cmd/linuxkit/run_hyperv.go b/src/cmd/linuxkit/run_hyperv.go index 2f11d3f35..ae0057bd5 100644 --- a/src/cmd/linuxkit/run_hyperv.go +++ b/src/cmd/linuxkit/run_hyperv.go @@ -2,7 +2,6 @@ package main import ( "bytes" - "flag" "fmt" "os" "os/exec" @@ -11,169 +10,165 @@ import ( "strings" log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) -// Process the run arguments and execute run -func runHyperV(args []string) { - flags := flag.NewFlagSet("hyperv", flag.ExitOnError) - invoked := filepath.Base(os.Args[0]) - flags.Usage = func() { - fmt.Printf("USAGE: %s run hyperv [options] path\n\n", invoked) - fmt.Printf("'path' specifies the path to a EFI ISO file.\n") - fmt.Printf("\n") - fmt.Printf("Options:\n") - flags.PrintDefaults() +func runHyperVCmd() *cobra.Command { + var ( + vmName string + keep bool + switchName string + ) + + cmd := &cobra.Command{ + Use: "hyperv", + Short: "launch a VM in Hyper-V", + Long: `Launch a VM in Hyper-V. + 'path' specifies the path to a EFI ISO file. + `, + Args: cobra.ExactArgs(1), + Example: "linuxkit run hyperv [options] path", + RunE: func(cmd *cobra.Command, args []string) error { + isoPath := args[0] + // Sanity checks. Errors out on failure + hypervChecks() + + vmSwitch, err := hypervGetSwitch(switchName) + if err != nil { + return err + } + log.Debugf("Using switch: %s", vmSwitch) + + if vmName == "" { + vmName = filepath.Base(isoPath) + vmName = strings.TrimSuffix(vmName, ".iso") + // Also strip -efi in case it is present + vmName = strings.TrimSuffix(vmName, "-efi") + } + + log.Infof("Creating VM: %s", vmName) + _, out, err := poshCmd("New-VM", "-Name", fmt.Sprintf("'%s'", vmName), + "-Generation", "2", + "-NoVHD", + "-SwitchName", fmt.Sprintf("'%s'", vmSwitch)) + if err != nil { + return fmt.Errorf("Failed to create new VM: %w\n%s", err, out) + } + log.Infof("Configure VM: %s", vmName) + _, out, err = poshCmd("Set-VM", "-Name", fmt.Sprintf("'%s'", vmName), + "-AutomaticStartAction", "Nothing", + "-AutomaticStopAction", "ShutDown", + "-CheckpointType", "Disabled", + "-MemoryStartupBytes", fmt.Sprintf("%dMB", mem), + "-StaticMemory", + "-ProcessorCount", fmt.Sprintf("%d", cpus)) + if err != nil { + return fmt.Errorf("Failed to configure new VM: %w\n%s", err, out) + } + + for i, d := range disks { + id := "" + if i != 0 { + id = strconv.Itoa(i) + } + if d.Size != 0 && d.Path == "" { + d.Path = vmName + "-disk" + id + ".vhdx" + } + if d.Path == "" { + return fmt.Errorf("disk specified with no size or name") + } + + if _, err := os.Stat(d.Path); err != nil { + if os.IsNotExist(err) { + log.Infof("Creating new disk %s %dMB", d.Path, d.Size) + _, out, err = poshCmd("New-VHD", + "-Path", fmt.Sprintf("'%s'", d.Path), + "-SizeBytes", fmt.Sprintf("%dMB", d.Size), + "-Dynamic") + if err != nil { + return fmt.Errorf("Failed to create VHD %s: %w\n%s", d.Path, err, out) + } + } else { + return fmt.Errorf("Problem accessing disk %s. %w", d.Path, err) + } + } else { + log.Infof("Using existing disk %s", d.Path) + } + + _, out, err = poshCmd("Add-VMHardDiskDrive", + "-VMName", fmt.Sprintf("'%s'", vmName), + "-Path", fmt.Sprintf("'%s'", d.Path)) + if err != nil { + return fmt.Errorf("Failed to add VHD %s: %w\n%s", d.Path, err, out) + } + } + + log.Info("Setting up boot from ISO") + _, out, err = poshCmd("Add-VMDvdDrive", + "-VMName", fmt.Sprintf("'%s'", vmName), + "-Path", fmt.Sprintf("'%s'", isoPath)) + if err != nil { + return fmt.Errorf("Failed add DVD: %w\n%s", err, out) + } + _, out, err = poshCmd( + fmt.Sprintf("$cdrom = Get-VMDvdDrive -vmname '%s';", vmName), + "Set-VMFirmware", "-VMName", fmt.Sprintf("'%s'", vmName), + "-EnableSecureBoot", "Off", + "-FirstBootDevice", "$cdrom") + if err != nil { + return fmt.Errorf("Failed set DVD as boot device: %w\n%s", err, out) + } + + log.Info("Set up COM port") + _, out, err = poshCmd("Set-VMComPort", + "-VMName", fmt.Sprintf("'%s'", vmName), + "-number", "1", + "-Path", fmt.Sprintf(`\\.\pipe\%s-com1`, vmName)) + if err != nil { + return fmt.Errorf("Failed set up COM port: %w\n%s", err, out) + } + + log.Info("Start the VM") + _, out, err = poshCmd("Start-VM", "-Name", fmt.Sprintf("'%s'", vmName)) + if err != nil { + return fmt.Errorf("Failed start the VM: %w\n%s", err, out) + } + + err = hypervStartConsole(vmName) + if err != nil { + log.Infof("Console returned: %v\n", err) + } + hypervRestoreConsole() + + if keep { + return nil + } + + log.Info("Stop the VM") + _, out, err = poshCmd("Stop-VM", + "-Name", fmt.Sprintf("'%s'", vmName), "-Force") + if err != nil { + // Don't error out, could get an error if VM is already stopped + log.Infof("Stop-VM error: %v\n%s", err, out) + } + + log.Info("Remove the VM") + _, out, err = poshCmd("Remove-VM", + "-Name", fmt.Sprintf("'%s'", vmName), "-Force") + if err != nil { + log.Infof("Remove-VM error: %v\n%s", err, out) + } + return nil + }, } + //nolint:staticcheck // I honestly have no idea why this is complaining, as this does get called on // L159, but anything to get the linter to stop complaining. - keep := flags.Bool("keep", false, "Keep the VM after finishing") - vmName := flags.String("name", "", "Name of the Hyper-V VM") - cpus := flags.Int("cpus", 1, "Number of CPUs") - mem := flags.Int("mem", 1024, "Amount of memory in MB") - var disks Disks - flags.Var(&disks, "disk", "Disk config. [file=]path[,size=1G]") + cmd.Flags().BoolVar(&keep, "keep", false, "Keep the VM after finishing") + cmd.Flags().StringVar(&vmName, "name", "", "Name of the Hyper-V VM") + cmd.Flags().StringVar(&switchName, "switch", "", "Which Hyper-V switch to attache the VM to. If left empty, either 'Default Switch' or the first external switch found is used.") - switchName := flags.String("switch", "", "Which Hyper-V switch to attache the VM to. If left empty, either 'Default Switch' or the first external switch found is used.") - - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") - } - remArgs := flags.Args() - if len(remArgs) == 0 { - fmt.Println("Please specify the path to the ISO image to boot") - flags.Usage() - os.Exit(1) - } - isoPath := remArgs[0] - - // Sanity checks. Errors out on failure - hypervChecks() - - vmSwitch, err := hypervGetSwitch(*switchName) - if err != nil { - log.Fatalf("%v", err) - } - log.Debugf("Using switch: %s", vmSwitch) - - if *vmName == "" { - *vmName = filepath.Base(isoPath) - *vmName = strings.TrimSuffix(*vmName, ".iso") - // Also strip -efi in case it is present - *vmName = strings.TrimSuffix(*vmName, "-efi") - } - - log.Infof("Creating VM: %s", *vmName) - _, out, err := poshCmd("New-VM", "-Name", fmt.Sprintf("'%s'", *vmName), - "-Generation", "2", - "-NoVHD", - "-SwitchName", fmt.Sprintf("'%s'", vmSwitch)) - if err != nil { - log.Fatalf("Failed to create new VM: %v\n%s", err, out) - } - log.Infof("Configure VM: %s", *vmName) - _, out, err = poshCmd("Set-VM", "-Name", fmt.Sprintf("'%s'", *vmName), - "-AutomaticStartAction", "Nothing", - "-AutomaticStopAction", "ShutDown", - "-CheckpointType", "Disabled", - "-MemoryStartupBytes", fmt.Sprintf("%dMB", *mem), - "-StaticMemory", - "-ProcessorCount", fmt.Sprintf("%d", *cpus)) - if err != nil { - log.Fatalf("Failed to configure new VM: %v\n%s", err, out) - } - - for i, d := range disks { - id := "" - if i != 0 { - id = strconv.Itoa(i) - } - if d.Size != 0 && d.Path == "" { - d.Path = *vmName + "-disk" + id + ".vhdx" - } - if d.Path == "" { - log.Fatalf("disk specified with no size or name") - } - - if _, err := os.Stat(d.Path); err != nil { - if os.IsNotExist(err) { - log.Infof("Creating new disk %s %dMB", d.Path, d.Size) - _, out, err = poshCmd("New-VHD", - "-Path", fmt.Sprintf("'%s'", d.Path), - "-SizeBytes", fmt.Sprintf("%dMB", d.Size), - "-Dynamic") - if err != nil { - log.Fatalf("Failed to create VHD %s: %v\n%s", d.Path, err, out) - } - } else { - log.Fatalf("Problem accessing disk %s. %v", d.Path, err) - } - } else { - log.Infof("Using existing disk %s", d.Path) - } - - _, out, err = poshCmd("Add-VMHardDiskDrive", - "-VMName", fmt.Sprintf("'%s'", *vmName), - "-Path", fmt.Sprintf("'%s'", d.Path)) - if err != nil { - log.Fatalf("Failed to add VHD %s: %v\n%s", d.Path, err, out) - } - } - - log.Info("Setting up boot from ISO") - _, out, err = poshCmd("Add-VMDvdDrive", - "-VMName", fmt.Sprintf("'%s'", *vmName), - "-Path", fmt.Sprintf("'%s'", isoPath)) - if err != nil { - log.Fatalf("Failed add DVD: %v\n%s", err, out) - } - _, out, err = poshCmd( - fmt.Sprintf("$cdrom = Get-VMDvdDrive -vmname '%s';", *vmName), - "Set-VMFirmware", "-VMName", fmt.Sprintf("'%s'", *vmName), - "-EnableSecureBoot", "Off", - "-FirstBootDevice", "$cdrom") - if err != nil { - log.Fatalf("Failed set DVD as boot device: %v\n%s", err, out) - } - - log.Info("Set up COM port") - _, out, err = poshCmd("Set-VMComPort", - "-VMName", fmt.Sprintf("'%s'", *vmName), - "-number", "1", - "-Path", fmt.Sprintf(`\\.\pipe\%s-com1`, *vmName)) - if err != nil { - log.Fatalf("Failed set up COM port: %v\n%s", err, out) - } - - log.Info("Start the VM") - _, out, err = poshCmd("Start-VM", "-Name", fmt.Sprintf("'%s'", *vmName)) - if err != nil { - log.Fatalf("Failed start the VM: %v\n%s", err, out) - } - - err = hypervStartConsole(*vmName) - if err != nil { - log.Infof("Console returned: %v\n", err) - } - hypervRestoreConsole() - - if *keep { - return - } - - log.Info("Stop the VM") - _, out, err = poshCmd("Stop-VM", - "-Name", fmt.Sprintf("'%s'", *vmName), "-Force") - if err != nil { - // Don't error out, could get an error if VM is already stopped - log.Infof("Stop-VM error: %v\n%s", err, out) - } - - log.Info("Remove the VM") - _, out, err = poshCmd("Remove-VM", - "-Name", fmt.Sprintf("'%s'", *vmName), "-Force") - if err != nil { - log.Infof("Remove-VM error: %v\n%s", err, out) - } + return cmd } var powershell string diff --git a/src/cmd/linuxkit/run_openstack.go b/src/cmd/linuxkit/run_openstack.go index 510f38da0..68c54aba5 100644 --- a/src/cmd/linuxkit/run_openstack.go +++ b/src/cmd/linuxkit/run_openstack.go @@ -1,15 +1,13 @@ package main import ( - "flag" "fmt" - "os" - "path/filepath" "strings" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" "github.com/gophercloud/utils/openstack/clientconfig" + "github.com/spf13/cobra" log "github.com/sirupsen/logrus" ) @@ -18,72 +16,73 @@ const ( defaultOSFlavor = "m1.tiny" ) -func runOpenStack(args []string) { - flags := flag.NewFlagSet("openstack", flag.ExitOnError) - invoked := filepath.Base(os.Args[0]) - flags.Usage = func() { - fmt.Printf("USAGE: %s run openstack [options] [name]\n\n", invoked) - fmt.Printf("'name' is the name of an OpenStack image that has already been\n") - fmt.Printf(" uploaded using 'linuxkit push'\n\n") - fmt.Printf("Options:\n\n") - flags.PrintDefaults() - } - flavorName := flags.String("flavor", defaultOSFlavor, "Instance size (flavor)") - instanceName := flags.String("instancename", "", "Name of instance. Defaults to the name of the image if not specified") - networkID := flags.String("network", "", "The ID of the network to attach the instance to") - secGroups := flags.String("sec-groups", "default", "Security Group names separated by comma") - keyName := flags.String("keyname", "", "The name of the SSH keypair to associate with the instance") +func runOpenStackCmd() *cobra.Command { + var ( + flavorName string + instanceName string + networkID string + secGroups string + keyName string + ) - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") + cmd := &cobra.Command{ + Use: "openstack", + Short: "launch an openstack instance using an existing image", + Long: `Launch an openstack instance using an existing image. + 'name' is the name of an OpenStack image that has already been uploaded using 'linuxkit push'. + `, + Args: cobra.ExactArgs(1), + Example: "linuxkit run openstack [options] [name]", + RunE: func(cmd *cobra.Command, args []string) error { + name := args[0] + if instanceName == "" { + instanceName = name + } + + client, err := clientconfig.NewServiceClient("compute", nil) + if err != nil { + return fmt.Errorf("Unable to create Compute client, %s", err) + } + + network := servers.Network{ + UUID: networkID, + } + + var serverOpts servers.CreateOptsBuilder + + serverOpts = &servers.CreateOpts{ + FlavorName: flavorName, + ImageName: name, + Name: instanceName, + Networks: []servers.Network{network}, + ServiceClient: client, + SecurityGroups: strings.Split(secGroups, ","), + } + + if keyName != "" { + serverOpts = &keypairs.CreateOptsExt{ + CreateOptsBuilder: serverOpts, + KeyName: keyName, + } + } + + server, err := servers.Create(client, serverOpts).Extract() + if err != nil { + return fmt.Errorf("Unable to create server: %w", err) + } + + _ = servers.WaitForStatus(client, server.ID, "ACTIVE", 600) + log.Infof("Server created, UUID is %s", server.ID) + fmt.Println(server.ID) + return nil + }, } - remArgs := flags.Args() - if len(remArgs) == 0 { - fmt.Printf("Please specify the name of the image to boot\n") - flags.Usage() - os.Exit(1) - } - name := remArgs[0] - - if *instanceName == "" { - *instanceName = name - } - - client, err := clientconfig.NewServiceClient("compute", nil) - if err != nil { - log.Fatalf("Unable to create Compute client, %s", err) - } - - network := servers.Network{ - UUID: *networkID, - } - - var serverOpts servers.CreateOptsBuilder - - serverOpts = &servers.CreateOpts{ - FlavorName: *flavorName, - ImageName: name, - Name: *instanceName, - Networks: []servers.Network{network}, - ServiceClient: client, - SecurityGroups: strings.Split(*secGroups, ","), - } - - if *keyName != "" { - serverOpts = &keypairs.CreateOptsExt{ - CreateOptsBuilder: serverOpts, - KeyName: *keyName, - } - } - - server, err := servers.Create(client, serverOpts).Extract() - if err != nil { - log.Fatalf("Unable to create server: %s", err) - } - - _ = servers.WaitForStatus(client, server.ID, "ACTIVE", 600) - log.Infof("Server created, UUID is %s", server.ID) - fmt.Println(server.ID) + cmd.Flags().StringVar(&flavorName, "flavor", defaultOSFlavor, "Instance size (flavor)") + cmd.Flags().StringVar(&instanceName, "instancename", "", "Name of instance. Defaults to the name of the image if not specified") + cmd.Flags().StringVar(&networkID, "network", "", "The ID of the network to attach the instance to") + cmd.Flags().StringVar(&secGroups, "sec-groups", "default", "Security Group names separated by comma") + cmd.Flags().StringVar(&keyName, "keyname", "", "The name of the SSH keypair to associate with the instance") + return cmd } diff --git a/src/cmd/linuxkit/run_packet.go b/src/cmd/linuxkit/run_packet.go index 596484dd3..51340a486 100644 --- a/src/cmd/linuxkit/run_packet.go +++ b/src/cmd/linuxkit/run_packet.go @@ -3,7 +3,7 @@ package main import ( "context" "encoding/json" - "flag" + "errors" "fmt" "io" "net" @@ -18,6 +18,7 @@ import ( "github.com/packethost/packngo" log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" "golang.org/x/term" @@ -46,202 +47,217 @@ func init() { } } -// Process the run arguments and execute run -func runPacket(args []string) { - flags := flag.NewFlagSet("packet", flag.ExitOnError) - invoked := filepath.Base(os.Args[0]) - flags.Usage = func() { - fmt.Printf("USAGE: %s run packet [options] [name]\n\n", invoked) - fmt.Printf("Options:\n\n") - flags.PrintDefaults() - } - baseURLFlag := flags.String("base-url", "", "Base URL that the kernel, initrd and iPXE script are served from (or "+packetBaseURL+")") - zoneFlag := flags.String("zone", packetDefaultZone, "Packet Zone (or "+packetZoneVar+")") - machineFlag := flags.String("machine", packetDefaultMachine, "Packet Machine Type (or "+packetMachineVar+")") - apiKeyFlag := flags.String("api-key", "", "Packet API key (or "+packetAPIKeyVar+")") - projectFlag := flags.String("project-id", "", "Packet Project ID (or "+packetProjectIDVar+")") - deviceFlag := flags.String("device", "", "The ID of an existing device") - hostNameFlag := flags.String("hostname", packetDefaultHostname, "Hostname of new instance (or "+packetHostnameVar+")") - nameFlag := flags.String("img-name", "", "Overrides the prefix used to identify the files. Defaults to [name] (or "+packetNameVar+")") - alwaysPXE := flags.Bool("always-pxe", true, "Reboot from PXE every time.") - serveFlag := flags.String("serve", "", "Serve local files via the http port specified, e.g. ':8080'.") - consoleFlag := flags.Bool("console", true, "Provide interactive access on the console.") - keepFlag := flags.Bool("keep", false, "Keep the machine after exiting/poweroff.") - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") - } +func runPacketCmd() *cobra.Command { + var ( + baseURLFlag string + zoneFlag string + machineFlag string + apiKeyFlag string + projectFlag string + deviceFlag string + hostNameFlag string + nameFlag string + alwaysPXE bool + serveFlag string + consoleFlag bool + keepFlag bool + ) - remArgs := flags.Args() - prefix := "packet" - if len(remArgs) > 0 { - prefix = remArgs[0] - } - - url := getStringValue(packetBaseURL, *baseURLFlag, "") - if url == "" { - log.Fatalf("Need to specify a value for --base-url where the images are hosted. This URL should contain /%s-kernel, /%s-initrd.img and /%s-packet.ipxe", prefix, prefix, prefix) - } - facility := getStringValue(packetZoneVar, *zoneFlag, "") - plan := getStringValue(packetMachineVar, *machineFlag, defaultMachine) - apiKey := getStringValue(packetAPIKeyVar, *apiKeyFlag, "") - if apiKey == "" { - log.Fatal("Must specify a Packet.net API key with --api-key") - } - projectID := getStringValue(packetProjectIDVar, *projectFlag, "") - if projectID == "" { - log.Fatal("Must specify a Packet.net Project ID with --project-id") - } - hostname := getStringValue(packetHostnameVar, *hostNameFlag, "") - name := getStringValue(packetNameVar, *nameFlag, prefix) - osType := "custom_ipxe" - billing := "hourly" - - if !*keepFlag && !*consoleFlag { - log.Fatalf("Combination of keep=%t and console=%t makes little sense", *keepFlag, *consoleFlag) - } - - ipxeScriptName := fmt.Sprintf("%s-packet.ipxe", name) - - // Serve files with a local http server - var httpServer *http.Server - if *serveFlag != "" { - // Read kernel command line - var cmdline string - if c, err := os.ReadFile(prefix + "-cmdline"); err != nil { - log.Fatalf("Cannot open cmdline file: %v", err) - } else { - cmdline = string(c) - } - - ipxeScript := packetIPXEScript(name, url, cmdline, packetMachineToArch(*machineFlag)) - log.Debugf("Using iPXE script:\n%s\n", ipxeScript) - - // Two handlers, one for the iPXE script and one for the kernel/initrd files - mux := http.NewServeMux() - mux.HandleFunc(fmt.Sprintf("/%s", ipxeScriptName), - func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, ipxeScript) - }) - fs := serveFiles{[]string{fmt.Sprintf("%s-kernel", name), fmt.Sprintf("%s-initrd.img", name)}} - mux.Handle("/", http.FileServer(fs)) - httpServer = &http.Server{Addr: *serveFlag, Handler: mux} - go func() { - log.Debugf("Listening on http://%s\n", *serveFlag) - if err := httpServer.ListenAndServe(); err != nil { - log.Infof("http server exited with: %v", err) + cmd := &cobra.Command{ + Use: "packet", + Short: "launch an Equinix Metal (Packet) device", + Long: `Launch an Equinix Metal (Packet) device. + `, + Args: cobra.ExactArgs(1), + Example: "linuxkit run packet [options] name", + RunE: func(cmd *cobra.Command, args []string) error { + prefix := "packet" + if len(args) > 0 { + prefix = args[0] } - }() + url := getStringValue(packetBaseURL, baseURLFlag, "") + if url == "" { + return fmt.Errorf("Need to specify a value for --base-url where the images are hosted. This URL should contain /%s-kernel, /%s-initrd.img and /%s-packet.ipxe", prefix, prefix, prefix) + } + facility := getStringValue(packetZoneVar, zoneFlag, "") + plan := getStringValue(packetMachineVar, machineFlag, defaultMachine) + apiKey := getStringValue(packetAPIKeyVar, apiKeyFlag, "") + if apiKey == "" { + return errors.New("Must specify a Packet.net API key with --api-key") + } + projectID := getStringValue(packetProjectIDVar, projectFlag, "") + if projectID == "" { + return errors.New("Must specify a Packet.net Project ID with --project-id") + } + hostname := getStringValue(packetHostnameVar, hostNameFlag, "") + name := getStringValue(packetNameVar, nameFlag, prefix) + osType := "custom_ipxe" + billing := "hourly" + + if !keepFlag && !consoleFlag { + return fmt.Errorf("Combination of keep=%t and console=%t makes little sense", keepFlag, consoleFlag) + } + + ipxeScriptName := fmt.Sprintf("%s-packet.ipxe", name) + + // Serve files with a local http server + var httpServer *http.Server + if serveFlag != "" { + // Read kernel command line + var cmdline string + if c, err := os.ReadFile(prefix + "-cmdline"); err != nil { + return fmt.Errorf("Cannot open cmdline file: %v", err) + } else { + cmdline = string(c) + } + + ipxeScript := packetIPXEScript(name, url, cmdline, packetMachineToArch(machineFlag)) + log.Debugf("Using iPXE script:\n%s\n", ipxeScript) + + // Two handlers, one for the iPXE script and one for the kernel/initrd files + mux := http.NewServeMux() + mux.HandleFunc(fmt.Sprintf("/%s", ipxeScriptName), + func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, ipxeScript) + }) + fs := serveFiles{[]string{fmt.Sprintf("%s-kernel", name), fmt.Sprintf("%s-initrd.img", name)}} + mux.Handle("/", http.FileServer(fs)) + httpServer = &http.Server{Addr: serveFlag, Handler: mux} + go func() { + log.Debugf("Listening on http://%s\n", serveFlag) + if err := httpServer.ListenAndServe(); err != nil { + log.Infof("http server exited with: %v", err) + } + }() + } + + // Make sure the URLs work + ipxeURL := fmt.Sprintf("%s/%s", url, ipxeScriptName) + initrdURL := fmt.Sprintf("%s/%s-initrd.img", url, name) + kernelURL := fmt.Sprintf("%s/%s-kernel", url, name) + log.Infof("Validating URL: %s", ipxeURL) + if err := validateHTTPURL(ipxeURL); err != nil { + return fmt.Errorf("Invalid iPXE URL %s: %v", ipxeURL, err) + } + log.Infof("Validating URL: %s", kernelURL) + if err := validateHTTPURL(kernelURL); err != nil { + return fmt.Errorf("Invalid kernel URL %s: %v", kernelURL, err) + } + log.Infof("Validating URL: %s", initrdURL) + if err := validateHTTPURL(initrdURL); err != nil { + return fmt.Errorf("Invalid initrd URL %s: %v", initrdURL, err) + } + + client := packngo.NewClient("", apiKey, nil) + var tags []string + + var dev *packngo.Device + var err error + if deviceFlag != "" { + dev, _, err = client.Devices.Get(deviceFlag) + if err != nil { + return fmt.Errorf("Getting info for device %s failed: %v", deviceFlag, err) + } + b, err := json.MarshalIndent(dev, "", " ") + if err != nil { + log.Fatal(err) + } + log.Debugf("%s\n", string(b)) + + req := packngo.DeviceUpdateRequest{ + Hostname: hostname, + Locked: dev.Locked, + Tags: dev.Tags, + IPXEScriptURL: ipxeURL, + AlwaysPXE: alwaysPXE, + } + dev, _, err = client.Devices.Update(deviceFlag, &req) + if err != nil { + return fmt.Errorf("Update device %s failed: %v", deviceFlag, err) + } + if _, err := client.Devices.Reboot(deviceFlag); err != nil { + return fmt.Errorf("Rebooting device %s failed: %v", deviceFlag, err) + } + } else { + // Create a new device + req := packngo.DeviceCreateRequest{ + Hostname: hostname, + Plan: plan, + Facility: facility, + OS: osType, + BillingCycle: billing, + ProjectID: projectID, + Tags: tags, + IPXEScriptURL: ipxeURL, + AlwaysPXE: alwaysPXE, + } + dev, _, err = client.Devices.Create(&req) + if err != nil { + return fmt.Errorf("Creating device failed: %w", err) + } + } + b, err := json.MarshalIndent(dev, "", " ") + if err != nil { + return err + } + log.Debugf("%s\n", string(b)) + + log.Printf("Booting %s...", dev.ID) + + sshHost := "sos." + dev.Facility.Code + ".packet.net" + if consoleFlag { + // Connect to the serial console + if err := packetSOS(dev.ID, sshHost); err != nil { + return err + } + } else { + log.Printf("Access the console with: ssh %s@%s", dev.ID, sshHost) + + // if the serve option is present, wait till 'ctrl-c' is hit. + // Otherwise we wouldn't serve the files + if serveFlag != "" { + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt) + log.Printf("Hit ctrl-c to stop http server") + <-stop + } + } + + // Stop the http server before exiting + if serveFlag != "" { + log.Debugf("Shutting down http server...") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _ = httpServer.Shutdown(ctx) + } + + if keepFlag { + log.Printf("The machine is kept...") + log.Printf("Device ID: %s", dev.ID) + log.Printf("Serial: ssh %s@%s", dev.ID, sshHost) + } else { + if _, err := client.Devices.Delete(dev.ID); err != nil { + return fmt.Errorf("Unable to delete device: %v", err) + } + } + return nil + }, } - // Make sure the URLs work - ipxeURL := fmt.Sprintf("%s/%s", url, ipxeScriptName) - initrdURL := fmt.Sprintf("%s/%s-initrd.img", url, name) - kernelURL := fmt.Sprintf("%s/%s-kernel", url, name) - log.Infof("Validating URL: %s", ipxeURL) - if err := validateHTTPURL(ipxeURL); err != nil { - log.Fatalf("Invalid iPXE URL %s: %v", ipxeURL, err) - } - log.Infof("Validating URL: %s", kernelURL) - if err := validateHTTPURL(kernelURL); err != nil { - log.Fatalf("Invalid kernel URL %s: %v", kernelURL, err) - } - log.Infof("Validating URL: %s", initrdURL) - if err := validateHTTPURL(initrdURL); err != nil { - log.Fatalf("Invalid initrd URL %s: %v", initrdURL, err) - } + cmd.Flags().StringVar(&baseURLFlag, "base-url", "", "Base URL that the kernel, initrd and iPXE script are served from (or "+packetBaseURL+")") + cmd.Flags().StringVar(&zoneFlag, "zone", packetDefaultZone, "Packet Zone (or "+packetZoneVar+")") + cmd.Flags().StringVar(&machineFlag, "machine", packetDefaultMachine, "Packet Machine Type (or "+packetMachineVar+")") + cmd.Flags().StringVar(&apiKeyFlag, "api-key", "", "Packet API key (or "+packetAPIKeyVar+")") + cmd.Flags().StringVar(&projectFlag, "project-id", "", "Packet Project ID (or "+packetProjectIDVar+")") + cmd.Flags().StringVar(&deviceFlag, "device", "", "The ID of an existing device") + cmd.Flags().StringVar(&hostNameFlag, "hostname", packetDefaultHostname, "Hostname of new instance (or "+packetHostnameVar+")") + cmd.Flags().StringVar(&nameFlag, "img-name", "", "Overrides the prefix used to identify the files. Defaults to [name] (or "+packetNameVar+")") + cmd.Flags().BoolVar(&alwaysPXE, "always-pxe", true, "Reboot from PXE every time.") + cmd.Flags().StringVar(&serveFlag, "serve", "", "Serve local files via the http port specified, e.g. ':8080'.") + cmd.Flags().BoolVar(&consoleFlag, "console", true, "Provide interactive access on the console.") + cmd.Flags().BoolVar(&keepFlag, "keep", false, "Keep the machine after exiting/poweroff.") - client := packngo.NewClient("", apiKey, nil) - var tags []string - - var dev *packngo.Device - var err error - if *deviceFlag != "" { - dev, _, err = client.Devices.Get(*deviceFlag) - if err != nil { - log.Fatalf("Getting info for device %s failed: %v", *deviceFlag, err) - } - b, err := json.MarshalIndent(dev, "", " ") - if err != nil { - log.Fatal(err) - } - log.Debugf("%s\n", string(b)) - - req := packngo.DeviceUpdateRequest{ - Hostname: hostname, - Locked: dev.Locked, - Tags: dev.Tags, - IPXEScriptURL: ipxeURL, - AlwaysPXE: *alwaysPXE, - } - dev, _, err = client.Devices.Update(*deviceFlag, &req) - if err != nil { - log.Fatalf("Update device %s failed: %v", *deviceFlag, err) - } - if _, err := client.Devices.Reboot(*deviceFlag); err != nil { - log.Fatalf("Rebooting device %s failed: %v", *deviceFlag, err) - } - } else { - // Create a new device - req := packngo.DeviceCreateRequest{ - Hostname: hostname, - Plan: plan, - Facility: facility, - OS: osType, - BillingCycle: billing, - ProjectID: projectID, - Tags: tags, - IPXEScriptURL: ipxeURL, - AlwaysPXE: *alwaysPXE, - } - dev, _, err = client.Devices.Create(&req) - if err != nil { - log.Fatalf("Creating device failed: %v", err) - } - } - b, err := json.MarshalIndent(dev, "", " ") - if err != nil { - log.Fatal(err) - } - log.Debugf("%s\n", string(b)) - - log.Printf("Booting %s...", dev.ID) - - sshHost := "sos." + dev.Facility.Code + ".packet.net" - if *consoleFlag { - // Connect to the serial console - if err := packetSOS(dev.ID, sshHost); err != nil { - log.Fatal(err) - } - } else { - log.Printf("Access the console with: ssh %s@%s", dev.ID, sshHost) - - // if the serve option is present, wait till 'ctrl-c' is hit. - // Otherwise we wouldn't serve the files - if *serveFlag != "" { - stop := make(chan os.Signal, 1) - signal.Notify(stop, os.Interrupt) - log.Printf("Hit ctrl-c to stop http server") - <-stop - } - } - - // Stop the http server before exiting - if *serveFlag != "" { - log.Debugf("Shutting down http server...") - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - _ = httpServer.Shutdown(ctx) - } - - if *keepFlag { - log.Printf("The machine is kept...") - log.Printf("Device ID: %s", dev.ID) - log.Printf("Serial: ssh %s@%s", dev.ID, sshHost) - } else { - if _, err := client.Devices.Delete(dev.ID); err != nil { - log.Fatalf("Unable to delete device: %v", err) - } - } + return cmd } // Convert machine type to architecture diff --git a/src/cmd/linuxkit/run_qemu.go b/src/cmd/linuxkit/run_qemu.go index 5a1cfbf9c..70ea91639 100644 --- a/src/cmd/linuxkit/run_qemu.go +++ b/src/cmd/linuxkit/run_qemu.go @@ -2,7 +2,7 @@ package main import ( "crypto/rand" - "flag" + "errors" "fmt" "net" "os" @@ -14,6 +14,7 @@ import ( "github.com/google/uuid" log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) const ( @@ -116,233 +117,241 @@ func generateMAC() net.HardwareAddr { return mac } -func runQemu(args []string) { - invoked := filepath.Base(os.Args[0]) - flags := flag.NewFlagSet("qemu", flag.ExitOnError) - flags.Usage = func() { - fmt.Printf("USAGE: %s run qemu [options] path\n\n", invoked) - fmt.Printf("'path' specifies the path to the VM image.\n") - fmt.Printf("\n") - fmt.Printf("Options:\n") - flags.PrintDefaults() - fmt.Printf("\n") - fmt.Printf("If not running as root note that '-networking bridge,br0' requires a\n") - fmt.Printf("setuid network helper and appropriate host configuration, see\n") - fmt.Printf("http://wiki.qemu.org/Features/HelperNetworking.\n") +func runQEMUCmd() *cobra.Command { + var ( + enableGUI bool + uefiBoot bool + isoBoot bool + squashFSBoot bool + kernelBoot bool + state string + data string + dataPath string + fw string + accel string + arch string + qemuCmd string + qemuDetached bool + networking string + usbEnabled bool + deviceFlags multipleFlag + publishFlags multipleFlag + ) + + cmd := &cobra.Command{ + Use: "qemu", + Short: "launch a VM using qemu", + Long: `Launch a VM using qemu. + 'path' specifies the path to the VM image. + + If not running as root note that '-networking bridge,br0' requires a + setuid network helper and appropriate host configuration, see + http://wiki.qemu.org/Features/HelperNetworking. + `, + Args: cobra.ExactArgs(1), + Example: "linuxkit run qemu [options] path", + RunE: func(cmd *cobra.Command, args []string) error { + path := args[0] + + if data != "" && dataPath != "" { + return errors.New("Cannot specify both -data and -data-file") + } + + // Generate UUID, so that /sys/class/dmi/id/product_uuid is populated + vmUUID := uuid.New() + + // These envvars override the corresponding command line + // options. So this must remain after the `flags.Parse` above. + accel = getStringValue("LINUXKIT_QEMU_ACCEL", accel, "") + + prefix := path + + _, err := os.Stat(path) + stat := err == nil + + // if the path does not exist, must be trying to do a kernel+initrd or kernel+squashfs boot + if !stat { + _, err = os.Stat(path + "-kernel") + statKernel := err == nil + if statKernel { + _, err = os.Stat(path + "-squashfs.img") + statSquashFS := err == nil + if statSquashFS { + squashFSBoot = true + } else { + kernelBoot = true + } + } + // we will error out later if neither found + } else { + // if path ends in .iso they meant an ISO + if strings.HasSuffix(path, ".iso") { + isoBoot = true + prefix = strings.TrimSuffix(path, ".iso") + } + } + + if state == "" { + state = prefix + "-state" + } + + if err := os.MkdirAll(state, 0755); err != nil { + return fmt.Errorf("Could not create state directory: %w", err) + } + + var isoPaths []string + + if isoBoot { + isoPaths = append(isoPaths, path) + } + + metadataPaths, err := CreateMetadataISO(state, data, dataPath) + if err != nil { + return err + } + isoPaths = append(isoPaths, metadataPaths...) + + for i, d := range disks { + id := "" + if i != 0 { + id = strconv.Itoa(i) + } + if d.Size != 0 && d.Format == "" { + d.Format = "qcow2" + } + if d.Size != 0 && d.Path == "" { + d.Path = filepath.Join(state, "disk"+id+".img") + } + if d.Path == "" { + return fmt.Errorf("disk specified with no size or name") + } + disks[i] = d + } + + // user not trying to boot off ISO or kernel+initrd, so assume booting from a disk image or kernel+squashfs + if !kernelBoot && !isoBoot { + var diskPath string + if squashFSBoot { + diskPath = path + "-squashfs.img" + } else { + if _, err := os.Stat(path); err != nil { + log.Fatalf("Boot disk image %s does not exist", path) + } + diskPath = path + } + // currently no way to set format, but autodetect probably works + d := Disks{DiskConfig{Path: diskPath}} + disks = append(d, disks...) + } + + if networking == "" || networking == "default" { + networking = qemuNetworkingDefault + } + netMode := strings.SplitN(networking, ",", 2) + + var netdevConfig string + switch netMode[0] { + case qemuNetworkingUser: + netdevConfig = "user,id=t0" + case qemuNetworkingTap: + if len(netMode) != 2 { + return fmt.Errorf("Not enough arguments for %q networking mode", qemuNetworkingTap) + } + if len(publishFlags) != 0 { + return fmt.Errorf("Port publishing requires %q networking mode", qemuNetworkingUser) + } + netdevConfig = fmt.Sprintf("tap,id=t0,ifname=%s,script=no,downscript=no", netMode[1]) + case qemuNetworkingBridge: + if len(netMode) != 2 { + return fmt.Errorf("Not enough arguments for %q networking mode", qemuNetworkingBridge) + } + if len(publishFlags) != 0 { + return fmt.Errorf("Port publishing requires %q networking mode", qemuNetworkingUser) + } + netdevConfig = fmt.Sprintf("bridge,id=t0,br=%s", netMode[1]) + case qemuNetworkingNone: + if len(publishFlags) != 0 { + return fmt.Errorf("Port publishing requires %q networking mode", qemuNetworkingUser) + } + netdevConfig = "" + default: + return fmt.Errorf("Invalid networking mode: %s", netMode[0]) + } + + config := QemuConfig{ + Path: path, + ISOBoot: isoBoot, + UEFI: uefiBoot, + SquashFS: squashFSBoot, + Kernel: kernelBoot, + GUI: enableGUI, + Disks: disks, + ISOImages: isoPaths, + StatePath: state, + FWPath: fw, + Arch: arch, + CPUs: fmt.Sprintf("%d", cpus), + Memory: fmt.Sprintf("%d", mem), + Accel: accel, + Detached: qemuDetached, + QemuBinPath: qemuCmd, + PublishedPorts: publishFlags, + NetdevConfig: netdevConfig, + UUID: vmUUID, + USB: usbEnabled, + Devices: deviceFlags, + } + + config, err = discoverBinaries(config) + if err != nil { + return err + } + + if err = runQemuLocal(config); err != nil { + return err + } + return nil + }, } // Display flags - enableGUI := flags.Bool("gui", false, "Set qemu to use video output instead of stdio") + cmd.Flags().BoolVar(&enableGUI, "gui", false, "Set qemu to use video output instead of stdio") // Boot type; we try to determine automatically - uefiBoot := flags.Bool("uefi", false, "Use UEFI boot") - isoBoot := flags.Bool("iso", false, "Boot image is an ISO") - squashFSBoot := flags.Bool("squashfs", false, "Boot image is a kernel+squashfs+cmdline") - kernelBoot := flags.Bool("kernel", false, "Boot image is kernel+initrd+cmdline 'path'-kernel/-initrd/-cmdline") + cmd.Flags().BoolVar(&uefiBoot, "uefi", false, "Use UEFI boot") + cmd.Flags().BoolVar(&isoBoot, "iso", false, "Boot image is an ISO") + cmd.Flags().BoolVar(&squashFSBoot, "squashfs", false, "Boot image is a kernel+squashfs+cmdline") + cmd.Flags().BoolVar(&kernelBoot, "kernel", false, "Boot image is kernel+initrd+cmdline 'path'-kernel/-initrd/-cmdline") // State flags - state := flags.String("state", "", "Path to directory to keep VM state in") + cmd.Flags().StringVar(&state, "state", "", "Path to directory to keep VM state in") // Paths and settings for disks - var disks Disks - flags.Var(&disks, "disk", "Disk config, may be repeated. [file=]path[,size=1G][,format=qcow2]") - data := flags.String("data", "", "String of metadata to pass to VM; error to specify both -data and -data-file") - dataPath := flags.String("data-file", "", "Path to file containing metadata to pass to VM; error to specify both -data and -data-file") - - if *data != "" && *dataPath != "" { - log.Fatal("Cannot specify both -data and -data-file") - } + cmd.Flags().StringVar(&data, "data", "", "String of metadata to pass to VM; error to specify both -data and -data-file") + cmd.Flags().StringVar(&dataPath, "data-file", "", "Path to file containing metadata to pass to VM; error to specify both -data and -data-file") // Paths and settings for UEFI firware // Note, we do not use defaultFWPath here as we have a special case for containerised execution - fw := flags.String("fw", "", "Path to OVMF firmware for UEFI boot") + cmd.Flags().StringVar(&fw, "fw", "", "Path to OVMF firmware for UEFI boot") // VM configuration - accel := flags.String("accel", defaultAccel, "Choose acceleration mode. Use 'tcg' to disable it.") - arch := flags.String("arch", defaultArch, "Type of architecture to use, e.g. x86_64, aarch64, s390x") - cpus := flags.String("cpus", "1", "Number of CPUs") - mem := flags.String("mem", "1024", "Amount of memory in MB") + cmd.Flags().StringVar(&accel, "accel", defaultAccel, "Choose acceleration mode. Use 'tcg' to disable it.") + cmd.Flags().StringVar(&arch, "arch", defaultArch, "Type of architecture to use, e.g. x86_64, aarch64, s390x") // Backend configuration - qemuCmd := flags.String("qemu", "", "Path to the qemu binary (otherwise look in $PATH)") - qemuDetached := flags.Bool("detached", false, "Set qemu container to run in the background") - - // Generate UUID, so that /sys/class/dmi/id/product_uuid is populated - vmUUID := uuid.New() + cmd.Flags().StringVar(&qemuCmd, "qemu", "", "Path to the qemu binary (otherwise look in $PATH)") + cmd.Flags().BoolVar(&qemuDetached, "detached", false, "Set qemu container to run in the background") // Networking - networking := flags.String("networking", qemuNetworkingDefault, "Networking mode. Valid options are 'default', 'user', 'bridge[,name]', tap[,name] and 'none'. 'user' uses QEMUs userspace networking. 'bridge' connects to a preexisting bridge. 'tap' uses a prexisting tap device. 'none' disables networking.`") + cmd.Flags().StringVar(&networking, "networking", qemuNetworkingDefault, "Networking mode. Valid options are 'default', 'user', 'bridge[,name]', tap[,name] and 'none'. 'user' uses QEMUs userspace networking. 'bridge' connects to a preexisting bridge. 'tap' uses a prexisting tap device. 'none' disables networking.`") - publishFlags := multipleFlag{} - flags.Var(&publishFlags, "publish", "Publish a vm's port(s) to the host (default [])") + cmd.Flags().Var(&publishFlags, "publish", "Publish a vm's port(s) to the host (default [])") // USB devices - usbEnabled := flags.Bool("usb", false, "Enable USB controller") - deviceFlags := multipleFlag{} - flags.Var(&deviceFlags, "device", "Add USB host device(s). Format driver[,prop=value][,...] -- add device, like -device on the qemu command line.") + cmd.Flags().BoolVar(&usbEnabled, "usb", false, "Enable USB controller") + cmd.Flags().Var(&deviceFlags, "device", "Add USB host device(s). Format driver[,prop=value][,...] -- add device, like -device on the qemu command line.") - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") - } - remArgs := flags.Args() - - // These envvars override the corresponding command line - // options. So this must remain after the `flags.Parse` above. - *accel = getStringValue("LINUXKIT_QEMU_ACCEL", *accel, "") - - if len(remArgs) == 0 { - fmt.Println("Please specify the path to the image to boot") - flags.Usage() - os.Exit(1) - } - path := remArgs[0] - prefix := path - - _, err := os.Stat(path) - stat := err == nil - - // if the path does not exist, must be trying to do a kernel+initrd or kernel+squashfs boot - if !stat { - _, err = os.Stat(path + "-kernel") - statKernel := err == nil - if statKernel { - _, err = os.Stat(path + "-squashfs.img") - statSquashFS := err == nil - if statSquashFS { - *squashFSBoot = true - } else { - *kernelBoot = true - } - } - // we will error out later if neither found - } else { - // if path ends in .iso they meant an ISO - if strings.HasSuffix(path, ".iso") { - *isoBoot = true - prefix = strings.TrimSuffix(path, ".iso") - } - } - - if *state == "" { - *state = prefix + "-state" - } - - if err := os.MkdirAll(*state, 0755); err != nil { - log.Fatalf("Could not create state directory: %v", err) - } - - var isoPaths []string - - if *isoBoot { - isoPaths = append(isoPaths, path) - } - - metadataPaths, err := CreateMetadataISO(*state, *data, *dataPath) - if err != nil { - log.Fatalf("%v", err) - } - isoPaths = append(isoPaths, metadataPaths...) - - for i, d := range disks { - id := "" - if i != 0 { - id = strconv.Itoa(i) - } - if d.Size != 0 && d.Format == "" { - d.Format = "qcow2" - } - if d.Size != 0 && d.Path == "" { - d.Path = filepath.Join(*state, "disk"+id+".img") - } - if d.Path == "" { - log.Fatalf("disk specified with no size or name") - } - disks[i] = d - } - - // user not trying to boot off ISO or kernel+initrd, so assume booting from a disk image or kernel+squashfs - if !*kernelBoot && !*isoBoot { - var diskPath string - if *squashFSBoot { - diskPath = path + "-squashfs.img" - } else { - if _, err := os.Stat(path); err != nil { - log.Fatalf("Boot disk image %s does not exist", path) - } - diskPath = path - } - // currently no way to set format, but autodetect probably works - d := Disks{DiskConfig{Path: diskPath}} - disks = append(d, disks...) - } - - if *networking == "" || *networking == "default" { - dflt := qemuNetworkingDefault - networking = &dflt - } - netMode := strings.SplitN(*networking, ",", 2) - - var netdevConfig string - switch netMode[0] { - case qemuNetworkingUser: - netdevConfig = "user,id=t0" - case qemuNetworkingTap: - if len(netMode) != 2 { - log.Fatalf("Not enough arguments for %q networking mode", qemuNetworkingTap) - } - if len(publishFlags) != 0 { - log.Fatalf("Port publishing requires %q networking mode", qemuNetworkingUser) - } - netdevConfig = fmt.Sprintf("tap,id=t0,ifname=%s,script=no,downscript=no", netMode[1]) - case qemuNetworkingBridge: - if len(netMode) != 2 { - log.Fatalf("Not enough arguments for %q networking mode", qemuNetworkingBridge) - } - if len(publishFlags) != 0 { - log.Fatalf("Port publishing requires %q networking mode", qemuNetworkingUser) - } - netdevConfig = fmt.Sprintf("bridge,id=t0,br=%s", netMode[1]) - case qemuNetworkingNone: - if len(publishFlags) != 0 { - log.Fatalf("Port publishing requires %q networking mode", qemuNetworkingUser) - } - netdevConfig = "" - default: - log.Fatalf("Invalid networking mode: %s", netMode[0]) - } - - config := QemuConfig{ - Path: path, - ISOBoot: *isoBoot, - UEFI: *uefiBoot, - SquashFS: *squashFSBoot, - Kernel: *kernelBoot, - GUI: *enableGUI, - Disks: disks, - ISOImages: isoPaths, - StatePath: *state, - FWPath: *fw, - Arch: *arch, - CPUs: *cpus, - Memory: *mem, - Accel: *accel, - Detached: *qemuDetached, - QemuBinPath: *qemuCmd, - PublishedPorts: publishFlags, - NetdevConfig: netdevConfig, - UUID: vmUUID, - USB: *usbEnabled, - Devices: deviceFlags, - } - - config, err = discoverBinaries(config) - if err != nil { - log.Fatal(err) - } - - if err = runQemuLocal(config); err != nil { - log.Fatal(err.Error()) - } + return cmd } func runQemuLocal(config QemuConfig) error { diff --git a/src/cmd/linuxkit/run_scaleway.go b/src/cmd/linuxkit/run_scaleway.go index d17f1a706..cefefff77 100644 --- a/src/cmd/linuxkit/run_scaleway.go +++ b/src/cmd/linuxkit/run_scaleway.go @@ -1,12 +1,8 @@ package main import ( - "flag" - "fmt" - "os" - "path/filepath" - log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) const ( @@ -26,76 +22,82 @@ const ( instanceTypeVar = "SCW_RUN_TYPE" // non-standard ) -func runScaleway(args []string) { - flags := flag.NewFlagSet("scaleway", flag.ExitOnError) - invoked := filepath.Base(os.Args[0]) - flags.Usage = func() { - fmt.Printf("USAGE: %s run scaleway [options] [name]\n\n", invoked) - fmt.Printf("'name' is the name of a Scaleway image that has already \n") - fmt.Printf("been uploaded using 'linuxkit push'\n\n") - fmt.Printf("Options:\n\n") - flags.PrintDefaults() - } - instanceTypeFlag := flags.String("instance-type", defaultScalewayInstanceType, "Scaleway instance type") - instanceNameFlag := flags.String("instance-name", "linuxkit", "Name of the create instance, default to the image name") - accessKeyFlag := flags.String("access-key", "", "Access Key to connect to Scaleway API") - secretKeyFlag := flags.String("secret-key", "", "Secret Key to connect to Scaleway API") - zoneFlag := flags.String("zone", defaultScalewayZone, "Select Scaleway zone") - organizationIDFlag := flags.String("organization-id", "", "Select Scaleway's organization ID") - cleanFlag := flags.Bool("clean", false, "Remove instance") - noAttachFlag := flags.Bool("no-attach", false, "Don't attach to serial port, you will have to connect to instance manually") +func runScalewayCmd() *cobra.Command { + var ( + instanceTypeFlag string + instanceNameFlag string + accessKeyFlag string + secretKeyFlag string + zoneFlag string + organizationIDFlag string + cleanFlag bool + noAttachFlag bool + ) - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") + cmd := &cobra.Command{ + Use: "scaleway", + Short: "launch a scaleway instance", + Long: `Launch an Scaleway instance using an existing image. + 'name' is the name of a Scaleway image that has already been uploaded using 'linuxkit push'. + `, + Args: cobra.ExactArgs(1), + Example: "linuxkit run scaleway [options] [name]", + RunE: func(cmd *cobra.Command, args []string) error { + name := args[0] + + instanceType := getStringValue(instanceTypeVar, instanceTypeFlag, defaultScalewayInstanceType) + instanceName := getStringValue("", instanceNameFlag, name) + accessKey := getStringValue(accessKeyVar, accessKeyFlag, "") + secretKey := getStringValue(secretKeyVar, secretKeyFlag, "") + zone := getStringValue(scwZoneVar, zoneFlag, defaultScalewayZone) + organizationID := getStringValue(organizationIDVar, organizationIDFlag, "") + + client, err := NewScalewayClient(accessKey, secretKey, zone, organizationID) + if err != nil { + log.Fatalf("Unable to connect to Scaleway: %v", err) + } + + instanceID, err := client.CreateLinuxkitInstance(instanceName, name, instanceType) + if err != nil { + log.Fatalf("Unable to create Scaleway instance: %v", err) + } + + err = client.BootInstance(instanceID) + if err != nil { + log.Fatalf("Unable to boot Scaleway instance: %v", err) + } + + if !noAttachFlag { + err = client.ConnectSerialPort(instanceID) + if err != nil { + log.Fatalf("Unable to connect to serial port: %v", err) + } + } + + if cleanFlag { + err = client.TerminateInstance(instanceID) + if err != nil { + log.Fatalf("Unable to stop instance: %v", err) + } + + err = client.DeleteInstanceAndVolumes(instanceID) + if err != nil { + log.Fatalf("Unable to delete instance: %v", err) + } + } + + return nil + }, } - remArgs := flags.Args() - if len(remArgs) == 0 { - fmt.Printf("Please specify the name of the image to boot\n") - flags.Usage() - os.Exit(1) - } - name := remArgs[0] - - instanceType := getStringValue(instanceTypeVar, *instanceTypeFlag, defaultScalewayInstanceType) - instanceName := getStringValue("", *instanceNameFlag, name) - accessKey := getStringValue(accessKeyVar, *accessKeyFlag, "") - secretKey := getStringValue(secretKeyVar, *secretKeyFlag, "") - zone := getStringValue(scwZoneVar, *zoneFlag, defaultScalewayZone) - organizationID := getStringValue(organizationIDVar, *organizationIDFlag, "") - - client, err := NewScalewayClient(accessKey, secretKey, zone, organizationID) - if err != nil { - log.Fatalf("Unable to connect to Scaleway: %v", err) - } - - instanceID, err := client.CreateLinuxkitInstance(instanceName, name, instanceType) - if err != nil { - log.Fatalf("Unable to create Scaleway instance: %v", err) - } - - err = client.BootInstance(instanceID) - if err != nil { - log.Fatalf("Unable to boot Scaleway instance: %v", err) - } - - if !*noAttachFlag { - err = client.ConnectSerialPort(instanceID) - if err != nil { - log.Fatalf("Unable to connect to serial port: %v", err) - } - } - - if *cleanFlag { - err = client.TerminateInstance(instanceID) - if err != nil { - log.Fatalf("Unable to stop instance: %v", err) - } - - err = client.DeleteInstanceAndVolumes(instanceID) - if err != nil { - log.Fatalf("Unable to delete instance: %v", err) - } - } + cmd.Flags().StringVar(&instanceTypeFlag, "instance-type", defaultScalewayInstanceType, "Scaleway instance type") + cmd.Flags().StringVar(&instanceNameFlag, "instance-name", "linuxkit", "Name of the create instance, default to the image name") + cmd.Flags().StringVar(&accessKeyFlag, "access-key", "", "Access Key to connect to Scaleway API") + cmd.Flags().StringVar(&secretKeyFlag, "secret-key", "", "Secret Key to connect to Scaleway API") + cmd.Flags().StringVar(&zoneFlag, "zone", defaultScalewayZone, "Select Scaleway zone") + cmd.Flags().StringVar(&organizationIDFlag, "organization-id", "", "Select Scaleway's organization ID") + cmd.Flags().BoolVar(&cleanFlag, "clean", false, "Remove instance") + cmd.Flags().BoolVar(&noAttachFlag, "no-attach", false, "Don't attach to serial port, you will have to connect to instance manually") + return cmd } diff --git a/src/cmd/linuxkit/run_vbox.go b/src/cmd/linuxkit/run_vbox.go index 5e04cbe47..c217d7d5a 100644 --- a/src/cmd/linuxkit/run_vbox.go +++ b/src/cmd/linuxkit/run_vbox.go @@ -2,7 +2,6 @@ package main import ( "bytes" - "flag" "fmt" "io" "net" @@ -15,6 +14,7 @@ import ( "strings" log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) // VBNetwork is the config for a Virtual Box network @@ -30,6 +30,10 @@ func (l *VBNetworks) String() string { return fmt.Sprint(*l) } +func (l *VBNetworks) Type() string { + return "[]VBNetwork" +} + // Set is used by flag to configure value from CLI func (l *VBNetworks) Set(value string) error { d := VBNetwork{} @@ -54,269 +58,266 @@ func (l *VBNetworks) Set(value string) error { return nil } -func runVbox(args []string) { - invoked := filepath.Base(os.Args[0]) - flags := flag.NewFlagSet("vbox", flag.ExitOnError) - flags.Usage = func() { - fmt.Printf("USAGE: %s run vbox [options] path\n\n", invoked) - fmt.Printf("'path' specifies the path to the VM image.\n") - fmt.Printf("\n") - fmt.Printf("Options:\n") - flags.PrintDefaults() - fmt.Printf("\n") +func runVBoxCmd() *cobra.Command { + var ( + enableGUI bool + vboxmanageFlag string + keep bool + vmName string + state string + isoBoot bool + uefiBoot bool + networks VBNetworks + ) + + cmd := &cobra.Command{ + Use: "vbox", + Short: "launch a vbox VM using an existing image", + Long: `Launch a vbox VM using an existing image. + 'path' specifies the path to the VM image. + `, + Args: cobra.ExactArgs(1), + Example: "linuxkit run vbox [options] path", + RunE: func(cmd *cobra.Command, args []string) error { + path := args[0] + if runtime.GOOS == "windows" { + return fmt.Errorf("TODO: Windows is not yet supported") + } + + if strings.HasSuffix(path, ".iso") { + isoBoot = true + } + + vboxmanage, err := exec.LookPath(vboxmanageFlag) + if err != nil { + return fmt.Errorf("Cannot find management binary %s: %v", vboxmanageFlag, err) + } + + name := vmName + if name == "" { + name = strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) + } + + if state == "" { + prefix := strings.TrimSuffix(path, filepath.Ext(path)) + state = prefix + "-state" + } + if err := os.MkdirAll(state, 0755); err != nil { + return fmt.Errorf("Could not create state directory: %v", err) + } + + // remove machine in case it already exists + cleanup(vboxmanage, name, false) + + _, out, err := manage(vboxmanage, "createvm", "--name", name, "--register") + if err != nil { + return fmt.Errorf("createvm error: %v\n%s", err, out) + } + + _, out, err = manage(vboxmanage, "modifyvm", name, "--acpi", "on") + if err != nil { + return fmt.Errorf("modifyvm --acpi error: %v\n%s", err, out) + } + + _, out, err = manage(vboxmanage, "modifyvm", name, "--memory", fmt.Sprintf("%d", mem)) + if err != nil { + return fmt.Errorf("modifyvm --memory error: %v\n%s", err, out) + } + + _, out, err = manage(vboxmanage, "modifyvm", name, "--cpus", fmt.Sprintf("%d", cpus)) + if err != nil { + return fmt.Errorf("modifyvm --cpus error: %v\n%s", err, out) + } + + firmware := "bios" + if uefiBoot { + firmware = "efi" + } + _, out, err = manage(vboxmanage, "modifyvm", name, "--firmware", firmware) + if err != nil { + return fmt.Errorf("modifyvm --firmware error: %v\n%s", err, out) + } + + // set up serial console + _, out, err = manage(vboxmanage, "modifyvm", name, "--uart1", "0x3F8", "4") + if err != nil { + return fmt.Errorf("modifyvm --uart error: %v\n%s", err, out) + } + + var consolePath string + if runtime.GOOS == "windows" { + // TODO use a named pipe on Windows + } else { + consolePath = filepath.Join(state, "console") + consolePath, err = filepath.Abs(consolePath) + if err != nil { + return fmt.Errorf("Bad path: %v", err) + } + } + + _, out, err = manage(vboxmanage, "modifyvm", name, "--uartmode1", "client", consolePath) + if err != nil { + return fmt.Errorf("modifyvm --uartmode error: %v\n%s", err, out) + } + + _, out, err = manage(vboxmanage, "storagectl", name, "--name", "IDE Controller", "--add", "ide") + if err != nil { + return fmt.Errorf("storagectl error: %v\n%s", err, out) + } + + if isoBoot { + _, out, err = manage(vboxmanage, "storageattach", name, "--storagectl", "IDE Controller", "--port", "1", "--device", "0", "--type", "dvddrive", "--medium", path) + if err != nil { + return fmt.Errorf("storageattach error: %v\n%s", err, out) + } + _, out, err = manage(vboxmanage, "modifyvm", name, "--boot1", "dvd") + if err != nil { + return fmt.Errorf("modifyvm --boot error: %v\n%s", err, out) + } + } else { + _, out, err = manage(vboxmanage, "storageattach", name, "--storagectl", "IDE Controller", "--port", "1", "--device", "0", "--type", "hdd", "--medium", path) + if err != nil { + return fmt.Errorf("storageattach error: %v\n%s", err, out) + } + _, out, err = manage(vboxmanage, "modifyvm", name, "--boot1", "disk") + if err != nil { + return fmt.Errorf("modifyvm --boot error: %v\n%s", err, out) + } + } + + if len(disks) > 0 { + _, out, err = manage(vboxmanage, "storagectl", name, "--name", "SATA", "--add", "sata") + if err != nil { + return fmt.Errorf("storagectl error: %v\n%s", err, out) + } + } + + for i, d := range disks { + id := strconv.Itoa(i) + if d.Size != 0 && d.Format == "" { + d.Format = "raw" + } + if d.Format != "raw" && d.Path == "" { + log.Fatal("vbox currently can only create raw disks") + } + if d.Path == "" && d.Size == 0 { + log.Fatal("please specify an existing disk file or a size") + } + if d.Path == "" { + d.Path = filepath.Join(state, "disk"+id+".img") + if err := os.Truncate(d.Path, int64(d.Size)*int64(1048576)); err != nil { + return fmt.Errorf("Cannot create disk: %v", err) + } + } + _, out, err = manage(vboxmanage, "storageattach", name, "--storagectl", "SATA", "--port", "0", "--device", id, "--type", "hdd", "--medium", d.Path) + if err != nil { + return fmt.Errorf("storageattach error: %v\n%s", err, out) + } + } + + for i, d := range networks { + nic := i + 1 + _, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--nictype%d", nic), "virtio") + if err != nil { + return fmt.Errorf("modifyvm --nictype error: %v\n%s", err, out) + } + + _, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--nic%d", nic), d.Type) + if err != nil { + return fmt.Errorf("modifyvm --nic error: %v\n%s", err, out) + } + if d.Type == "hostonly" { + _, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--hostonlyadapter%d", nic), d.Adapter) + if err != nil { + return fmt.Errorf("modifyvm --hostonlyadapter error: %v\n%s", err, out) + } + } else if d.Type == "bridged" { + _, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--bridgeadapter%d", nic), d.Adapter) + if err != nil { + return fmt.Errorf("modifyvm --bridgeadapter error: %v\n%s", err, out) + } + } + + _, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--cableconnected%d", nic), "on") + if err != nil { + return fmt.Errorf("modifyvm --cableconnected error: %v\n%s", err, out) + } + } + + // create socket + _ = os.Remove(consolePath) + ln, err := net.Listen("unix", consolePath) + if err != nil { + return fmt.Errorf("Cannot listen on console socket %s: %v", consolePath, err) + } + + var vmType string + if enableGUI { + vmType = "gui" + } else { + vmType = "headless" + } + + _, out, err = manage(vboxmanage, "startvm", name, "--type", vmType) + if err != nil { + return fmt.Errorf("startvm error: %v\n%s", err, out) + } + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + <-c + cleanup(vboxmanage, name, keep) + os.Exit(1) + }() + + socket, err := ln.Accept() + if err != nil { + return fmt.Errorf("Accept error: %v", err) + } + + go func() { + if _, err := io.Copy(socket, os.Stdin); err != nil { + cleanup(vboxmanage, name, keep) + log.Fatalf("Copy error: %v", err) + } + cleanup(vboxmanage, name, keep) + os.Exit(0) + }() + go func() { + if _, err := io.Copy(os.Stdout, socket); err != nil { + cleanup(vboxmanage, name, keep) + log.Fatalf("Copy error: %v", err) + } + cleanup(vboxmanage, name, keep) + os.Exit(0) + }() + // wait forever + select {} + }, } // Display flags - enableGUI := flags.Bool("gui", false, "Show the VM GUI") + cmd.Flags().BoolVar(&enableGUI, "gui", false, "Show the VM GUI") // vbox options - vboxmanageFlag := flags.String("vboxmanage", "VBoxManage", "VBoxManage binary to use") - keep := flags.Bool("keep", false, "Keep the VM after finishing") - vmName := flags.String("name", "", "Name of the Virtualbox VM") - state := flags.String("state", "", "Path to directory to keep VM state in") + cmd.Flags().StringVar(&vboxmanageFlag, "vboxmanage", "VBoxManage", "VBoxManage binary to use") + cmd.Flags().BoolVar(&keep, "keep", false, "Keep the VM after finishing") + cmd.Flags().StringVar(&vmName, "name", "", "Name of the Virtualbox VM") + cmd.Flags().StringVar(&state, "state", "", "Path to directory to keep VM state in") // Paths and settings for disks - var disks Disks - flags.Var(&disks, "disk", "Disk config, may be repeated. [file=]path[,size=1G][,format=raw]") // VM configuration - cpus := flags.String("cpus", "1", "Number of CPUs") - mem := flags.String("mem", "1024", "Amount of memory in MB") // booting config - isoBoot := flags.Bool("iso", false, "Boot image is an ISO") - uefiBoot := flags.Bool("uefi", false, "Use UEFI boot") + cmd.Flags().BoolVar(&isoBoot, "iso", false, "Boot image is an ISO") + cmd.Flags().BoolVar(&uefiBoot, "uefi", false, "Use UEFI boot") // networking - var networks VBNetworks - flags.Var(&networks, "networking", "Network config, may be repeated. [type=](null|nat|bridged|intnet|hostonly|generic|natnetwork[])[,[bridge|host]adapter=]") + cmd.Flags().Var(&networks, "networking", "Network config, may be repeated. [type=](null|nat|bridged|intnet|hostonly|generic|natnetwork[])[,[bridge|host]adapter=]") - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") - } - remArgs := flags.Args() - - if runtime.GOOS == "windows" { - log.Fatalf("TODO: Windows is not yet supported") - } - - if len(remArgs) == 0 { - fmt.Println("Please specify the path to the image to boot") - flags.Usage() - os.Exit(1) - } - path := remArgs[0] - - if strings.HasSuffix(path, ".iso") { - *isoBoot = true - } - - vboxmanage, err := exec.LookPath(*vboxmanageFlag) - if err != nil { - log.Fatalf("Cannot find management binary %s: %v", *vboxmanageFlag, err) - } - - name := *vmName - if name == "" { - name = strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) - } - - if *state == "" { - prefix := strings.TrimSuffix(path, filepath.Ext(path)) - *state = prefix + "-state" - } - if err := os.MkdirAll(*state, 0755); err != nil { - log.Fatalf("Could not create state directory: %v", err) - } - - // remove machine in case it already exists - cleanup(vboxmanage, name, false) - - _, out, err := manage(vboxmanage, "createvm", "--name", name, "--register") - if err != nil { - log.Fatalf("createvm error: %v\n%s", err, out) - } - - _, out, err = manage(vboxmanage, "modifyvm", name, "--acpi", "on") - if err != nil { - log.Fatalf("modifyvm --acpi error: %v\n%s", err, out) - } - - _, out, err = manage(vboxmanage, "modifyvm", name, "--memory", *mem) - if err != nil { - log.Fatalf("modifyvm --memory error: %v\n%s", err, out) - } - - _, out, err = manage(vboxmanage, "modifyvm", name, "--cpus", *cpus) - if err != nil { - log.Fatalf("modifyvm --cpus error: %v\n%s", err, out) - } - - firmware := "bios" - if *uefiBoot { - firmware = "efi" - } - _, out, err = manage(vboxmanage, "modifyvm", name, "--firmware", firmware) - if err != nil { - log.Fatalf("modifyvm --firmware error: %v\n%s", err, out) - } - - // set up serial console - _, out, err = manage(vboxmanage, "modifyvm", name, "--uart1", "0x3F8", "4") - if err != nil { - log.Fatalf("modifyvm --uart error: %v\n%s", err, out) - } - - var consolePath string - if runtime.GOOS == "windows" { - // TODO use a named pipe on Windows - } else { - consolePath = filepath.Join(*state, "console") - consolePath, err = filepath.Abs(consolePath) - if err != nil { - log.Fatalf("Bad path: %v", err) - } - } - - _, out, err = manage(vboxmanage, "modifyvm", name, "--uartmode1", "client", consolePath) - if err != nil { - log.Fatalf("modifyvm --uartmode error: %v\n%s", err, out) - } - - _, out, err = manage(vboxmanage, "storagectl", name, "--name", "IDE Controller", "--add", "ide") - if err != nil { - log.Fatalf("storagectl error: %v\n%s", err, out) - } - - if *isoBoot { - _, out, err = manage(vboxmanage, "storageattach", name, "--storagectl", "IDE Controller", "--port", "1", "--device", "0", "--type", "dvddrive", "--medium", path) - if err != nil { - log.Fatalf("storageattach error: %v\n%s", err, out) - } - _, out, err = manage(vboxmanage, "modifyvm", name, "--boot1", "dvd") - if err != nil { - log.Fatalf("modifyvm --boot error: %v\n%s", err, out) - } - } else { - _, out, err = manage(vboxmanage, "storageattach", name, "--storagectl", "IDE Controller", "--port", "1", "--device", "0", "--type", "hdd", "--medium", path) - if err != nil { - log.Fatalf("storageattach error: %v\n%s", err, out) - } - _, out, err = manage(vboxmanage, "modifyvm", name, "--boot1", "disk") - if err != nil { - log.Fatalf("modifyvm --boot error: %v\n%s", err, out) - } - } - - if len(disks) > 0 { - _, out, err = manage(vboxmanage, "storagectl", name, "--name", "SATA", "--add", "sata") - if err != nil { - log.Fatalf("storagectl error: %v\n%s", err, out) - } - } - - for i, d := range disks { - id := strconv.Itoa(i) - if d.Size != 0 && d.Format == "" { - d.Format = "raw" - } - if d.Format != "raw" && d.Path == "" { - log.Fatal("vbox currently can only create raw disks") - } - if d.Path == "" && d.Size == 0 { - log.Fatal("please specify an existing disk file or a size") - } - if d.Path == "" { - d.Path = filepath.Join(*state, "disk"+id+".img") - if err := os.Truncate(d.Path, int64(d.Size)*int64(1048576)); err != nil { - log.Fatalf("Cannot create disk: %v", err) - } - } - _, out, err = manage(vboxmanage, "storageattach", name, "--storagectl", "SATA", "--port", "0", "--device", id, "--type", "hdd", "--medium", d.Path) - if err != nil { - log.Fatalf("storageattach error: %v\n%s", err, out) - } - } - - for i, d := range networks { - nic := i + 1 - _, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--nictype%d", nic), "virtio") - if err != nil { - log.Fatalf("modifyvm --nictype error: %v\n%s", err, out) - } - - _, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--nic%d", nic), d.Type) - if err != nil { - log.Fatalf("modifyvm --nic error: %v\n%s", err, out) - } - if d.Type == "hostonly" { - _, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--hostonlyadapter%d", nic), d.Adapter) - if err != nil { - log.Fatalf("modifyvm --hostonlyadapter error: %v\n%s", err, out) - } - } else if d.Type == "bridged" { - _, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--bridgeadapter%d", nic), d.Adapter) - if err != nil { - log.Fatalf("modifyvm --bridgeadapter error: %v\n%s", err, out) - } - } - - _, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--cableconnected%d", nic), "on") - if err != nil { - log.Fatalf("modifyvm --cableconnected error: %v\n%s", err, out) - } - } - - // create socket - _ = os.Remove(consolePath) - ln, err := net.Listen("unix", consolePath) - if err != nil { - log.Fatalf("Cannot listen on console socket %s: %v", consolePath, err) - } - - var vmType string - if *enableGUI { - vmType = "gui" - } else { - vmType = "headless" - } - - _, out, err = manage(vboxmanage, "startvm", name, "--type", vmType) - if err != nil { - log.Fatalf("startvm error: %v\n%s", err, out) - } - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt) - go func() { - <-c - cleanup(vboxmanage, name, *keep) - os.Exit(1) - }() - - socket, err := ln.Accept() - if err != nil { - log.Fatalf("Accept error: %v", err) - } - - go func() { - if _, err := io.Copy(socket, os.Stdin); err != nil { - cleanup(vboxmanage, name, *keep) - log.Fatalf("Copy error: %v", err) - } - cleanup(vboxmanage, name, *keep) - os.Exit(0) - }() - go func() { - if _, err := io.Copy(os.Stdout, socket); err != nil { - cleanup(vboxmanage, name, *keep) - log.Fatalf("Copy error: %v", err) - } - cleanup(vboxmanage, name, *keep) - os.Exit(0) - }() - // wait forever - select {} + return cmd } func cleanup(vboxmanage string, name string, keep bool) { diff --git a/src/cmd/linuxkit/run_vcenter.go b/src/cmd/linuxkit/run_vcenter.go index 2df6bc3ae..c3f4d2e86 100644 --- a/src/cmd/linuxkit/run_vcenter.go +++ b/src/cmd/linuxkit/run_vcenter.go @@ -2,15 +2,15 @@ package main import ( "context" - "flag" + "errors" "fmt" "math/rand" "net/url" "os" "path" - "path/filepath" "strings" + "github.com/spf13/cobra" "github.com/vmware/govmomi" "github.com/vmware/govmomi/find" "github.com/vmware/govmomi/object" @@ -36,123 +36,140 @@ type vmConfig struct { guestIP *bool } -func runVcenter(args []string) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() +func runVCenterCmd() *cobra.Command { + var ( + vCenterURL string + dcName string + dsName string + networkName string + vSphereHost string + vmFolder string + vmPath string + persistent string + poweron bool + guestIP bool + ) - var newVM vmConfig + cmd := &cobra.Command{ + Use: "vcenter", + Short: "launch a VCenter instance using an existing image", + Long: `Launch a VCenter instance using an existing image. + 'path' specifies the full path of an image to run. + `, + Args: cobra.ExactArgs(1), + Example: "linuxkit run vcenter [options] path", + RunE: func(cmd *cobra.Command, args []string) error { + imagePath := args[0] + if guestIP && !poweron { + return errors.New("The waitForIP flag can not be used without the powerOn flag") + } + // Ensure an iso has been passed to the vCenter run Command + if strings.HasSuffix(vmPath, ".iso") { + // Allow alternative names for new virtual machines being created in vCenter + if vmFolder == "" { + vmFolder = strings.TrimSuffix(path.Base(vmPath), ".iso") + } + } else { + return fmt.Errorf("Please pass an \".iso\" file as the path") + } - flags := flag.NewFlagSet("vCenter", flag.ExitOnError) - invoked := filepath.Base(os.Args[0]) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + mem64 := int64(mem) + newVM := vmConfig{ + vCenterURL: &vCenterURL, + dcName: &dcName, + dsName: &dsName, + networkName: &networkName, + vSphereHost: &vSphereHost, + vmFolder: &vmFolder, + path: &vmPath, + persistent: &persistent, + mem: &mem64, + vCpus: &cpus, + poweron: &poweron, + guestIP: &guestIP, + } + newVM.path = &imagePath - newVM.vCenterURL = flags.String("url", os.Getenv("VCURL"), "URL of VMware vCenter in the format of https://username:password@VCaddress/sdk") - newVM.dcName = flags.String("datacenter", os.Getenv("VCDATACENTER"), "The name of the Datacenter to host the VM") - newVM.dsName = flags.String("datastore", os.Getenv("VCDATASTORE"), "The name of the DataStore to host the VM") - newVM.networkName = flags.String("network", os.Getenv("VCNETWORK"), "The network label the VM will use") - newVM.vSphereHost = flags.String("hostname", os.Getenv("VCHOST"), "The server that will run the VM") + // Connect to VMware vCenter and return the default and found values needed for a new VM + c, dss, folders, hs, net, rp := vCenterConnect(ctx, newVM) - newVM.vmFolder = flags.String("vmfolder", "", "Specify a name/folder for the virtual machine to reside in") - newVM.path = flags.String("path", "", "Path to a specific image") - newVM.persistent = flags.String("persistentSize", "", "Size in MB of persistent storage to allocate to the VM") - newVM.mem = flags.Int64("mem", 1024, "Size in MB of memory to allocate to the VM") - newVM.vCpus = flags.Int("cpus", 1, "Amount of vCPUs to allocate to the VM") - newVM.poweron = flags.Bool("powerOn", false, "Power On the new VM once it has been created") - newVM.guestIP = flags.Bool("waitForIP", false, "LinuxKit will wait for the VM to power on and return the guest IP, requires open-vm-tools and the -powerOn flag to be set") + log.Infof("Creating new LinuxKit Virtual Machine") + spec := types.VirtualMachineConfigSpec{ + Name: *newVM.vmFolder, + GuestId: "otherLinux64Guest", + Files: &types.VirtualMachineFileInfo{VmPathName: fmt.Sprintf("[%s]", dss.Name())}, + NumCPUs: int32(*newVM.vCpus), + MemoryMB: *newVM.mem, + } - flags.Usage = func() { - fmt.Printf("USAGE: %s run vcenter [options] path\n\n", invoked) - fmt.Printf("'path' specifies the full path of an image to run\n") - fmt.Printf("Options:\n\n") - flags.PrintDefaults() + scsi, err := object.SCSIControllerTypes().CreateSCSIController("pvscsi") + if err != nil { + return errors.New("Error creating pvscsi controller as part of new VM") + } + + spec.DeviceChange = append(spec.DeviceChange, &types.VirtualDeviceConfigSpec{ + Operation: types.VirtualDeviceConfigSpecOperationAdd, + Device: scsi, + }) + + task, err := folders.VmFolder.CreateVM(ctx, spec, rp, hs) + if err != nil { + return errors.New("Creating new VM failed, more detail can be found in vCenter tasks") + } + + info, err := task.WaitForResult(ctx, nil) + if err != nil { + return fmt.Errorf("Creating new VM failed\n%w", err) + } + + // Retrieve the new VM + vm := object.NewVirtualMachine(c.Client, info.Result.(types.ManagedObjectReference)) + + addISO(ctx, newVM, vm, dss) + + if *newVM.persistent != "" { + newVM.persistentSz, err = getDiskSizeMB(*newVM.persistent) + if err != nil { + return fmt.Errorf("Couldn't parse disk-size %s: %v", *newVM.persistent, err) + } + addVMDK(ctx, vm, dss, newVM) + } + + if *newVM.networkName != "" { + addNIC(ctx, vm, net) + } + + if *newVM.poweron { + log.Infoln("Powering on LinuxKit VM") + powerOnVM(ctx, vm) + } + + if *newVM.guestIP { + log.Infof("Waiting for OpenVM Tools to come online") + guestIP, err := getVMToolsIP(ctx, vm) + if err != nil { + log.Errorf("%v", err) + } + log.Infof("Guest IP Address: %s", guestIP) + } + return nil + }, } - if err := flags.Parse(args); err != nil { - log.Fatalln("Unable to parse args") - } + cmd.Flags().StringVar(&vCenterURL, "url", os.Getenv("VCURL"), "URL of VMware vCenter in the format of https://username:password@VCaddress/sdk") + cmd.Flags().StringVar(&dcName, "datacenter", os.Getenv("VCDATACENTER"), "The name of the Datacenter to host the VM") + cmd.Flags().StringVar(&dsName, "datastore", os.Getenv("VCDATASTORE"), "The name of the DataStore to host the VM") + cmd.Flags().StringVar(&networkName, "network", os.Getenv("VCNETWORK"), "The network label the VM will use") + cmd.Flags().StringVar(&vSphereHost, "hostname", os.Getenv("VCHOST"), "The server that will run the VM") + cmd.Flags().StringVar(&vmFolder, "vmfolder", "", "Specify a name/folder for the virtual machine to reside in") + cmd.Flags().StringVar(&vmPath, "path", "", "Path to a specific image") + cmd.Flags().StringVar(&persistent, "persistentSize", "", "Size in MB of persistent storage to allocate to the VM") + cmd.Flags().BoolVar(&poweron, "powerOn", false, "Power On the new VM once it has been created") + cmd.Flags().BoolVar(&guestIP, "waitForIP", false, "LinuxKit will wait for the VM to power on and return the guest IP, requires open-vm-tools and the -powerOn flag to be set") - remArgs := flags.Args() - if len(remArgs) == 0 { - fmt.Printf("Please specify the path to the image to run\n") - flags.Usage() - os.Exit(1) - } - *newVM.path = remArgs[0] - - if *newVM.guestIP && !*newVM.poweron { - log.Fatalln("The waitForIP flag can not be used without the powerOn flag") - } - // Ensure an iso has been passed to the vCenter run Command - if strings.HasSuffix(*newVM.path, ".iso") { - // Allow alternative names for new virtual machines being created in vCenter - if *newVM.vmFolder == "" { - *newVM.vmFolder = strings.TrimSuffix(path.Base(*newVM.path), ".iso") - } - } else { - log.Fatalln("Please pass an \".iso\" file as the path") - } - - // Connect to VMware vCenter and return the default and found values needed for a new VM - c, dss, folders, hs, net, rp := vCenterConnect(ctx, newVM) - - log.Infof("Creating new LinuxKit Virtual Machine") - spec := types.VirtualMachineConfigSpec{ - Name: *newVM.vmFolder, - GuestId: "otherLinux64Guest", - Files: &types.VirtualMachineFileInfo{VmPathName: fmt.Sprintf("[%s]", dss.Name())}, - NumCPUs: int32(*newVM.vCpus), - MemoryMB: *newVM.mem, - } - - scsi, err := object.SCSIControllerTypes().CreateSCSIController("pvscsi") - if err != nil { - log.Fatalln("Error creating pvscsi controller as part of new VM") - } - - spec.DeviceChange = append(spec.DeviceChange, &types.VirtualDeviceConfigSpec{ - Operation: types.VirtualDeviceConfigSpecOperationAdd, - Device: scsi, - }) - - task, err := folders.VmFolder.CreateVM(ctx, spec, rp, hs) - if err != nil { - log.Fatalln("Creating new VM failed, more detail can be found in vCenter tasks") - } - - info, err := task.WaitForResult(ctx, nil) - if err != nil { - log.Fatalf("Creating new VM failed\n%v", err) - } - - // Retrieve the new VM - vm := object.NewVirtualMachine(c.Client, info.Result.(types.ManagedObjectReference)) - - addISO(ctx, newVM, vm, dss) - - if *newVM.persistent != "" { - newVM.persistentSz, err = getDiskSizeMB(*newVM.persistent) - if err != nil { - log.Fatalf("Couldn't parse disk-size %s: %v", *newVM.persistent, err) - } - addVMDK(ctx, vm, dss, newVM) - } - - if *newVM.networkName != "" { - addNIC(ctx, vm, net) - } - - if *newVM.poweron { - log.Infoln("Powering on LinuxKit VM") - powerOnVM(ctx, vm) - } - - if *newVM.guestIP { - log.Infof("Waiting for OpenVM Tools to come online") - guestIP, err := getVMToolsIP(ctx, vm) - if err != nil { - log.Errorf("%v", err) - } - log.Infof("Guest IP Address: %s", guestIP) - } + return cmd } func getVMToolsIP(ctx context.Context, vm *object.VirtualMachine) (string, error) { diff --git a/src/cmd/linuxkit/run_virtualizationframework.go b/src/cmd/linuxkit/run_virtualizationframework.go index 9e754dde6..59b5ed083 100644 --- a/src/cmd/linuxkit/run_virtualizationframework.go +++ b/src/cmd/linuxkit/run_virtualizationframework.go @@ -1,13 +1,68 @@ -//go:build !darwin -// +build !darwin - package main import ( - log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) -// Process the run arguments and execute run -func runVirtualizationFramework(args []string) { - log.Fatal("virtualization framework is available only on macOS") +const ( + virtualizationNetworkingNone string = "none" + virtualizationNetworkingDockerForMac = "docker-for-mac" + virtualizationNetworkingVPNKit = "vpnkit" + virtualizationNetworkingVMNet = "vmnet" + virtualizationNetworkingDefault = virtualizationNetworkingVMNet + virtualizationFrameworkConsole = "console=hvc0" +) + +type virtualizationFramwworkConfig struct { + cpus uint + mem uint64 + disks Disks + data string + dataPath string + state string + networking string + kernelBoot bool +} + +func runVirtualizationFrameworkCmd() *cobra.Command { + var ( + data string + dataPath string + state string + networking string + kernelBoot bool + ) + + cmd := &cobra.Command{ + Use: "virtualization", + Short: "launch a VM using the macOS virtualization framework", + Long: `Launch a VM using the macOS virtualization framework. + 'prefix' specifies the path to the VM image. + `, + Args: cobra.ExactArgs(1), + Example: "linuxkit run virtualization [options] prefix", + RunE: func(cmd *cobra.Command, args []string) error { + cfg := virtualizationFramwworkConfig{ + cpus: uint(cpus), + mem: uint64(mem) * 1024 * 1024, + disks: disks, + data: data, + dataPath: dataPath, + state: state, + networking: networking, + kernelBoot: kernelBoot, + } + return runVirtualizationFramework(cfg, args[0]) + }, + } + + cmd.Flags().StringVar(&data, "data", "", "String of metadata to pass to VM; error to specify both -data and -data-file") + cmd.Flags().StringVar(&dataPath, "data-file", "", "Path to file containing metadata to pass to VM; error to specify both -data and -data-file") + + cmd.Flags().StringVar(&state, "state", "", "Path to directory to keep VM state in") + cmd.Flags().StringVar(&networking, "networking", virtualizationNetworkingDefault, "Networking mode. Valid options are 'default', 'vmnet' and 'none'. 'vmnet' uses the Apple vmnet framework. 'none' disables networking.`") + + cmd.Flags().BoolVar(&kernelBoot, "kernel", false, "Boot image is kernel+initrd+cmdline 'path'-kernel/-initrd/-cmdline") + + return cmd } diff --git a/src/cmd/linuxkit/run_virtualizationframework_darwin_cgo_disabled.go b/src/cmd/linuxkit/run_virtualizationframework_darwin_cgo_disabled.go index 114438f91..03da75e68 100644 --- a/src/cmd/linuxkit/run_virtualizationframework_darwin_cgo_disabled.go +++ b/src/cmd/linuxkit/run_virtualizationframework_darwin_cgo_disabled.go @@ -4,11 +4,11 @@ package main import ( - log "github.com/sirupsen/logrus" + "errors" ) // Process the run arguments and execute run -func runVirtualizationFramework(args []string) { - log.Fatal("This build of linuxkit was compiled without virtualization framework capabilities. " + +func runVirtualizationFramework(cfg virtualizationFramwworkConfig, image string) error { + return errors.New("This build of linuxkit was compiled without virtualization framework capabilities. " + "To perform 'linuxkit run' on macOS, please use a version of linuxkit compiled with virtualization framework.") } diff --git a/src/cmd/linuxkit/run_virtualizationframework_darwin_cgo_enabled.go b/src/cmd/linuxkit/run_virtualizationframework_darwin_cgo_enabled.go index 0949e3e57..fe132ea26 100644 --- a/src/cmd/linuxkit/run_virtualizationframework_darwin_cgo_enabled.go +++ b/src/cmd/linuxkit/run_virtualizationframework_darwin_cgo_enabled.go @@ -5,7 +5,7 @@ package main import ( "compress/gzip" - "flag" + "errors" "fmt" "io" "net/http" @@ -22,53 +22,12 @@ import ( "golang.org/x/sys/unix" ) -const ( - virtualizationNetworkingNone string = "none" - virtualizationNetworkingDockerForMac = "docker-for-mac" - virtualizationNetworkingVPNKit = "vpnkit" - virtualizationNetworkingVMNet = "vmnet" - virtualizationNetworkingDefault = virtualizationNetworkingVMNet - virtualizationFrameworkConsole = "console=hvc0" -) - // Process the run arguments and execute run -func runVirtualizationFramework(args []string) { - flags := flag.NewFlagSet("virtualization", flag.ExitOnError) - invoked := filepath.Base(os.Args[0]) - flags.Usage = func() { - fmt.Printf("USAGE: %s run virtualization [options] prefix\n\n", invoked) - fmt.Printf("'prefix' specifies the path to the VM image.\n") - fmt.Printf("\n") - fmt.Printf("Options:\n") - flags.PrintDefaults() - } - cpus := flags.Uint("cpus", 1, "Number of CPUs") - mem := flags.Uint64("mem", 1024, "Amount of memory in MB") - memBytes := *mem * 1024 * 1024 - var disks Disks - flags.Var(&disks, "disk", "Disk config. [file=]path[,size=1G]") - data := flags.String("data", "", "String of metadata to pass to VM; error to specify both -data and -data-file") - dataPath := flags.String("data-file", "", "Path to file containing metadata to pass to VM; error to specify both -data and -data-file") - - if *data != "" && *dataPath != "" { - log.Fatal("Cannot specify both -data and -data-file") +func runVirtualizationFramework(cfg virtualizationFramwworkConfig, path string) error { + if cfg.data != "" && cfg.dataPath != "" { + return errors.New("Cannot specify both -data and -data-file") } - state := flags.String("state", "", "Path to directory to keep VM state in") - networking := flags.String("networking", virtualizationNetworkingDefault, "Networking mode. Valid options are 'default', 'vmnet' and 'none'. 'vmnet' uses the Apple vmnet framework. 'none' disables networking.`") - - kernelBoot := flags.Bool("kernel", false, "Boot image is kernel+initrd+cmdline 'path'-kernel/-initrd/-cmdline") - - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") - } - remArgs := flags.Args() - if len(remArgs) == 0 { - fmt.Println("Please specify the prefix to the image to boot") - flags.Usage() - os.Exit(1) - } - path := remArgs[0] prefix := path _, err := os.Stat(path + "-kernel") @@ -78,18 +37,18 @@ func runVirtualizationFramework(args []string) { // Default to kernel+initrd if !statKernel { - log.Fatalf("Cannot find kernel file: %s", path+"-kernel") + return fmt.Errorf("Cannot find kernel file: %s", path+"-kernel") } _, err = os.Stat(path + "-initrd.img") statInitrd := err == nil if !statInitrd { - log.Fatalf("Cannot find initrd file (%s): %v", path+"-initrd.img", err) + return fmt.Errorf("Cannot find initrd file (%s): %w", path+"-initrd.img", err) } - *kernelBoot = true + cfg.kernelBoot = true - metadataPaths, err := CreateMetadataISO(*state, *data, *dataPath) + metadataPaths, err := CreateMetadataISO(cfg.state, cfg.data, cfg.dataPath) if err != nil { - log.Fatalf("%v", err) + return fmt.Errorf("%w", err) } isoPaths = append(isoPaths, metadataPaths...) @@ -100,7 +59,7 @@ func runVirtualizationFramework(args []string) { cmdlineBytes, err := os.ReadFile(prefix + "-cmdline") if err != nil { - log.Fatalf("Cannot open cmdline file: %v", err) + return fmt.Errorf("Cannot open cmdline file: %v", err) } // must have hvc0 as console for vf kernelCommandLineArguments := strings.Split(string(cmdlineBytes), " ") @@ -119,7 +78,7 @@ func runVirtualizationFramework(args []string) { // need to check if it is gzipped, and, if so, gunzip it filetype, err := checkFileType(vmlinuz) if err != nil { - log.Fatalf("unable to check kernel file type at %s: %v", vmlinuz, err) + return fmt.Errorf("unable to check kernel file type at %s: %v", vmlinuz, err) } if filetype == "application/x-gzip" { @@ -127,23 +86,23 @@ func runVirtualizationFramework(args []string) { // gzipped kernel, we load it into memory, unzip it, and pass it f, err := os.Open(vmlinuz) if err != nil { - log.Fatalf("unable to read kernel file %s: %v", vmlinuz, err) + return fmt.Errorf("unable to read kernel file %s: %v", vmlinuz, err) } defer f.Close() r, err := gzip.NewReader(f) if err != nil { - log.Fatalf("unable to read from file %s: %v", vmlinuz, err) + return fmt.Errorf("unable to read from file %s: %v", vmlinuz, err) } defer r.Close() writer, err := os.Create(vmlinuzUncompressed) if err != nil { - log.Fatalf("unable to create decompressed kernel file %s: %v", vmlinuzUncompressed, err) + return fmt.Errorf("unable to create decompressed kernel file %s: %v", vmlinuzUncompressed, err) } defer writer.Close() if _, err = io.Copy(writer, r); err != nil { - log.Fatalf("unable to decompress kernel file to %s: %v", vmlinuzUncompressed, err) + return fmt.Errorf("unable to decompress kernel file to %s: %v", vmlinuzUncompressed, err) } vmlinuzFile = vmlinuzUncompressed } @@ -153,27 +112,27 @@ func runVirtualizationFramework(args []string) { vz.WithInitrd(initrd), ) if err != nil { - log.Fatalf("unable to create bootloader: %v", err) + return fmt.Errorf("unable to create bootloader: %v", err) } config, err := vz.NewVirtualMachineConfiguration( bootLoader, - *cpus, - memBytes, + cfg.cpus, + cfg.mem, ) if err != nil { - log.Fatalf("unable to create VM config: %v", err) + return fmt.Errorf("unable to create VM config: %v", err) } // console stdin, stdout := os.Stdin, os.Stdout serialPortAttachment, err := vz.NewFileHandleSerialPortAttachment(stdin, stdout) if err != nil { - log.Fatalf("unable to create serial port attachment: %v", err) + return fmt.Errorf("unable to create serial port attachment: %v", err) } consoleConfig, err := vz.NewVirtioConsoleDeviceSerialPortConfiguration(serialPortAttachment) if err != nil { - log.Fatalf("unable to create console config: %v", err) + return fmt.Errorf("unable to create console config: %v", err) } config.SetSerialPortsVirtualMachineConfiguration([]*vz.VirtioConsoleDeviceSerialPortConfiguration{ consoleConfig, @@ -183,39 +142,38 @@ func runVirtualizationFramework(args []string) { // network // Select network mode // for now, we only support vmnet and none, but hoping to have more in the future - if *networking == "" || *networking == "default" { - dflt := virtualizationNetworkingDefault - networking = &dflt + if cfg.networking == "" || cfg.networking == "default" { + cfg.networking = virtualizationNetworkingDefault } - netMode := strings.SplitN(*networking, ",", 3) + netMode := strings.SplitN(cfg.networking, ",", 3) switch netMode[0] { case virtualizationNetworkingVMNet: natAttachment, err := vz.NewNATNetworkDeviceAttachment() if err != nil { - log.Fatalf("Could not create NAT network device attachment: %v", err) + return fmt.Errorf("Could not create NAT network device attachment: %v", err) } networkConfig, err := vz.NewVirtioNetworkDeviceConfiguration(natAttachment) if err != nil { - log.Fatalf("Could not create virtio network device configuration: %v", err) + return fmt.Errorf("Could not create virtio network device configuration: %v", err) } config.SetNetworkDevicesVirtualMachineConfiguration([]*vz.VirtioNetworkDeviceConfiguration{ networkConfig, }) macAddress, err := vz.NewRandomLocallyAdministeredMACAddress() if err != nil { - log.Fatalf("Could not create random MAC address: %v", err) + return fmt.Errorf("Could not create random MAC address: %v", err) } networkConfig.SetMACAddress(macAddress) case virtualizationNetworkingNone: default: - log.Fatalf("Invalid networking mode: %s", netMode[0]) + return fmt.Errorf("Invalid networking mode: %s", netMode[0]) } // entropy entropyConfig, err := vz.NewVirtioEntropyDeviceConfiguration() if err != nil { - log.Fatalf("Could not create virtio entropy device configuration: %v", err) + return fmt.Errorf("Could not create virtio entropy device configuration: %v", err) } config.SetEntropyDevicesVirtualMachineConfiguration([]*vz.VirtioEntropyDeviceConfiguration{ @@ -223,16 +181,16 @@ func runVirtualizationFramework(args []string) { }) var storageDevices []vz.StorageDeviceConfiguration - for i, d := range disks { + for i, d := range cfg.disks { var id, diskPath string if i != 0 { id = strconv.Itoa(i) } if d.Size != 0 && d.Path == "" { - diskPath = filepath.Join(*state, "disk"+id+".raw") + diskPath = filepath.Join(cfg.state, "disk"+id+".raw") } if d.Path == "" { - log.Fatalf("disk specified with no size or name") + return fmt.Errorf("disk specified with no size or name") } diskImageAttachment, err := vz.NewDiskImageStorageDeviceAttachment( diskPath, @@ -243,7 +201,7 @@ func runVirtualizationFramework(args []string) { } storageDeviceConfig, err := vz.NewVirtioBlockDeviceConfiguration(diskImageAttachment) if err != nil { - log.Fatalf("Could not create virtio block device configuration: %v", err) + return fmt.Errorf("Could not create virtio block device configuration: %v", err) } storageDevices = append(storageDevices, storageDeviceConfig) } @@ -257,7 +215,7 @@ func runVirtualizationFramework(args []string) { } storageDeviceConfig, err := vz.NewVirtioBlockDeviceConfiguration(diskImageAttachment) if err != nil { - log.Fatalf("Could not create virtio block device configuration: %v", err) + return fmt.Errorf("Could not create virtio block device configuration: %v", err) } storageDevices = append(storageDevices, storageDeviceConfig) } @@ -267,7 +225,7 @@ func runVirtualizationFramework(args []string) { // traditional memory balloon device which allows for managing guest memory. (optional) memoryBalloonDeviceConfiguration, err := vz.NewVirtioTraditionalMemoryBalloonDeviceConfiguration() if err != nil { - log.Fatalf("Could not create virtio traditional memory balloon device configuration: %v", err) + return fmt.Errorf("Could not create virtio traditional memory balloon device configuration: %v", err) } config.SetMemoryBalloonDevicesVirtualMachineConfiguration([]vz.MemoryBalloonDeviceConfiguration{ memoryBalloonDeviceConfiguration, @@ -276,7 +234,7 @@ func runVirtualizationFramework(args []string) { // socket device (optional) socketDeviceConfiguration, err := vz.NewVirtioSocketDeviceConfiguration() if err != nil { - log.Fatalf("Could not create virtio socket device configuration: %v", err) + return fmt.Errorf("Could not create virtio socket device configuration: %v", err) } config.SetSocketDevicesVirtualMachineConfiguration([]vz.SocketDeviceConfiguration{ socketDeviceConfiguration, @@ -288,7 +246,7 @@ func runVirtualizationFramework(args []string) { vm, err := vz.NewVirtualMachine(config) if err != nil { - log.Fatalf("Could not create virtual machine: %v", err) + return fmt.Errorf("Could not create virtual machine: %v", err) } signalCh := make(chan os.Signal, 1) @@ -306,7 +264,7 @@ func runVirtualizationFramework(args []string) { result, err := vm.RequestStop() if err != nil { log.Println("request stop error:", err) - return + return nil } log.Println("recieved signal", result) case newState := <-vm.StateChangedNotify(): @@ -315,7 +273,7 @@ func runVirtualizationFramework(args []string) { } if newState == vz.VirtualMachineStateStopped { log.Println("stopped successfully") - return + return nil } case err := <-errCh: log.Println("in start:", err) diff --git a/src/cmd/linuxkit/run_virtualizationframework_others.go b/src/cmd/linuxkit/run_virtualizationframework_others.go new file mode 100644 index 000000000..c9ef0536c --- /dev/null +++ b/src/cmd/linuxkit/run_virtualizationframework_others.go @@ -0,0 +1,13 @@ +//go:build !darwin +// +build !darwin + +package main + +import ( + "errors" +) + +// Process the run arguments and execute run +func runVirtualizationFramework(cfg virtualizationFramwworkConfig, image string) error { + return errors.New("virtualization framework is available only on macOS") +} diff --git a/src/cmd/linuxkit/run_vmware.go b/src/cmd/linuxkit/run_vmware.go index a88357c1f..ac2bf721b 100644 --- a/src/cmd/linuxkit/run_vmware.go +++ b/src/cmd/linuxkit/run_vmware.go @@ -1,7 +1,6 @@ package main import ( - "flag" "fmt" "os" "os/exec" @@ -10,6 +9,7 @@ import ( "strconv" log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) //Version 12 relates to Fusion 8 and WS 12 @@ -64,144 +64,138 @@ ethernet0.generatedAddressOffset = "0" guestOS = "other3xlinux-64" ` -func runVMware(args []string) { - invoked := filepath.Base(os.Args[0]) - flags := flag.NewFlagSet("vmware", flag.ExitOnError) - flags.Usage = func() { - fmt.Printf("USAGE: %s run vmware [options] prefix\n\n", invoked) - fmt.Printf("'prefix' specifies the path to the VM image.\n") - fmt.Printf("\n") - fmt.Printf("Options:\n") - flags.PrintDefaults() - } - cpus := flags.Int("cpus", 1, "Number of CPUs") - mem := flags.Int("mem", 1024, "Amount of memory in MB") - var disks Disks - flags.Var(&disks, "disk", "Disk config. [file=]path[,size=1G]") - state := flags.String("state", "", "Path to directory to keep VM state in") +func runVMWareCmd() *cobra.Command { + var ( + state string + ) - if err := flags.Parse(args); err != nil { - log.Fatal("Unable to parse args") - } - remArgs := flags.Args() - - if len(remArgs) == 0 { - fmt.Println("Please specify the prefix to the image to boot") - flags.Usage() - os.Exit(1) - } - prefix := remArgs[0] - - if *state == "" { - *state = prefix + "-state" - } - if err := os.MkdirAll(*state, 0755); err != nil { - log.Fatalf("Could not create state directory: %v", err) - } - - var vmrunPath, vmDiskManagerPath string - var vmrunArgs []string - - switch runtime.GOOS { - case "windows": - vmrunPath = "C:\\Program\\ files\\VMware Workstation\\vmrun.exe" - vmDiskManagerPath = "C:\\Program\\ files\\VMware Workstation\\vmware-vdiskmanager.exe" - vmrunArgs = []string{"-T", "ws", "start"} - case "darwin": - vmrunPath = "/Applications/VMware Fusion.app/Contents/Library/vmrun" - vmDiskManagerPath = "/Applications/VMware Fusion.app/Contents/Library/vmware-vdiskmanager" - vmrunArgs = []string{"-T", "fusion", "start"} - default: - vmrunPath = "vmrun" - vmDiskManagerPath = "vmware-vdiskmanager" - fullVMrunPath, err := exec.LookPath(vmrunPath) - if err != nil { - // Kept as separate error as people may manually change their environment vars - log.Fatalf("Unable to find %s within the $PATH", vmrunPath) - } - vmrunPath = fullVMrunPath - vmrunArgs = []string{"-T", "ws", "start"} - } - - // Check vmrunPath exists before attempting to execute - if _, err := os.Stat(vmrunPath); os.IsNotExist(err) { - log.Fatalf("ERROR VMware executables can not be found, ensure software is installed") - } - - for i, d := range disks { - id := "" - if i != 0 { - id = strconv.Itoa(i) - } - if d.Size != 0 && d.Path == "" { - d.Path = filepath.Join(*state, "disk"+id+".vmdk") - } - if d.Format != "" && d.Format != "vmdk" { - log.Fatalf("only vmdk supported for VMware driver") - } - if d.Path == "" { - log.Fatalf("disk specified with no size or name") - } - disks[i] = d - } - - for _, d := range disks { - // Check vmDiskManagerPath exist before attempting to execute - if _, err := os.Stat(vmDiskManagerPath); os.IsNotExist(err) { - log.Fatalf("ERROR VMware Disk Manager executables can not be found, ensure software is installed") - } - - // If disk doesn't exist then create one, error if disk is unreadable - if _, err := os.Stat(d.Path); err != nil { - if os.IsPermission(err) { - log.Fatalf("Unable to read file [%s], please check permissions", d.Path) - } else if os.IsNotExist(err) { - log.Infof("Creating new VMware disk [%s]", d.Path) - vmDiskCmd := exec.Command(vmDiskManagerPath, "-c", "-s", fmt.Sprintf("%dMB", d.Size), "-a", "lsilogic", "-t", "0", d.Path) - if err = vmDiskCmd.Run(); err != nil { - log.Fatalf("Error creating disk [%s]: %v", d.Path, err) - } - } else { - log.Fatalf("Unable to read file [%s]: %v", d.Path, err) + cmd := &cobra.Command{ + Use: "vmware", + Short: "launch a VMWare instance using the provided image", + Long: `Launch a VMWare instance using the provided image. + 'prefix' specifies the path to the VM image. + `, + Args: cobra.ExactArgs(1), + Example: "linuxkit run vmware [options] prefix", + RunE: func(cmd *cobra.Command, args []string) error { + prefix := args[0] + if state == "" { + state = prefix + "-state" } - } else { - log.Infof("Using existing disk [%s]", d.Path) - } + if err := os.MkdirAll(state, 0755); err != nil { + return fmt.Errorf("Could not create state directory: %v", err) + } + + var vmrunPath, vmDiskManagerPath string + var vmrunArgs []string + + switch runtime.GOOS { + case "windows": + vmrunPath = "C:\\Program\\ files\\VMware Workstation\\vmrun.exe" + vmDiskManagerPath = "C:\\Program\\ files\\VMware Workstation\\vmware-vdiskmanager.exe" + vmrunArgs = []string{"-T", "ws", "start"} + case "darwin": + vmrunPath = "/Applications/VMware Fusion.app/Contents/Library/vmrun" + vmDiskManagerPath = "/Applications/VMware Fusion.app/Contents/Library/vmware-vdiskmanager" + vmrunArgs = []string{"-T", "fusion", "start"} + default: + vmrunPath = "vmrun" + vmDiskManagerPath = "vmware-vdiskmanager" + fullVMrunPath, err := exec.LookPath(vmrunPath) + if err != nil { + // Kept as separate error as people may manually change their environment vars + return fmt.Errorf("Unable to find %s within the $PATH", vmrunPath) + } + vmrunPath = fullVMrunPath + vmrunArgs = []string{"-T", "ws", "start"} + } + + // Check vmrunPath exists before attempting to execute + if _, err := os.Stat(vmrunPath); os.IsNotExist(err) { + return fmt.Errorf("ERROR VMware executables can not be found, ensure software is installed") + } + + for i, d := range disks { + id := "" + if i != 0 { + id = strconv.Itoa(i) + } + if d.Size != 0 && d.Path == "" { + d.Path = filepath.Join(state, "disk"+id+".vmdk") + } + if d.Format != "" && d.Format != "vmdk" { + return fmt.Errorf("only vmdk supported for VMware driver") + } + if d.Path == "" { + return fmt.Errorf("disk specified with no size or name") + } + disks[i] = d + } + + for _, d := range disks { + // Check vmDiskManagerPath exist before attempting to execute + if _, err := os.Stat(vmDiskManagerPath); os.IsNotExist(err) { + return fmt.Errorf("ERROR VMware Disk Manager executables can not be found, ensure software is installed") + } + + // If disk doesn't exist then create one, error if disk is unreadable + if _, err := os.Stat(d.Path); err != nil { + if os.IsPermission(err) { + return fmt.Errorf("Unable to read file [%s], please check permissions", d.Path) + } else if os.IsNotExist(err) { + log.Infof("Creating new VMware disk [%s]", d.Path) + vmDiskCmd := exec.Command(vmDiskManagerPath, "-c", "-s", fmt.Sprintf("%dMB", d.Size), "-a", "lsilogic", "-t", "0", d.Path) + if err = vmDiskCmd.Run(); err != nil { + return fmt.Errorf("Error creating disk [%s]: %w", d.Path, err) + } + } else { + return fmt.Errorf("Unable to read file [%s]: %w", d.Path, err) + } + } else { + log.Infof("Using existing disk [%s]", d.Path) + } + } + + if len(disks) > 1 { + return fmt.Errorf("VMware driver currently only supports a single disk") + } + + disk := "" + if len(disks) == 1 { + disk = disks[0].Path + } + + // Build the contents of the VMWare .vmx file + vmx := buildVMX(cpus, mem, disk, prefix) + if vmx == "" { + return fmt.Errorf("VMware .vmx file could not be generated, please confirm inputs") + } + + // Create the .vmx file + vmxPath := filepath.Join(state, "linuxkit.vmx") + err := os.WriteFile(vmxPath, []byte(vmx), 0644) + if err != nil { + return fmt.Errorf("Error writing .vmx file: %v", err) + } + vmrunArgs = append(vmrunArgs, vmxPath) + + execCmd := exec.Command(vmrunPath, vmrunArgs...) + out, err := execCmd.Output() + if err != nil { + return fmt.Errorf("Error starting vmrun: %v", err) + } + + // check there is output to push to logging + if len(out) > 0 { + log.Info(out) + } + return nil + }, } - if len(disks) > 1 { - log.Fatalf("VMware driver currently only supports a single disk") - } + cmd.Flags().StringVar(&state, "state", "", "Path to directory to keep VM state in") - disk := "" - if len(disks) == 1 { - disk = disks[0].Path - } - - // Build the contents of the VMWare .vmx file - vmx := buildVMX(*cpus, *mem, disk, prefix) - if vmx == "" { - log.Fatalf("VMware .vmx file could not be generated, please confirm inputs") - } - - // Create the .vmx file - vmxPath := filepath.Join(*state, "linuxkit.vmx") - err := os.WriteFile(vmxPath, []byte(vmx), 0644) - if err != nil { - log.Fatalf("Error writing .vmx file: %v", err) - } - vmrunArgs = append(vmrunArgs, vmxPath) - - cmd := exec.Command(vmrunPath, vmrunArgs...) - out, err := cmd.Output() - if err != nil { - log.Fatalf("Error starting vmrun: %v", err) - } - - // check there is output to push to logging - if len(out) > 0 { - log.Info(out) - } + return cmd } func buildVMX(cpus int, mem int, persistentDisk string, prefix string) string { diff --git a/src/cmd/linuxkit/serve.go b/src/cmd/linuxkit/serve.go index a925a1609..e065d8eb1 100644 --- a/src/cmd/linuxkit/serve.go +++ b/src/cmd/linuxkit/serve.go @@ -1,13 +1,10 @@ package main import ( - "flag" - "fmt" "net/http" - "os" - "path/filepath" log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) func logRequest(handler http.Handler) http.Handler { @@ -17,19 +14,25 @@ func logRequest(handler http.Handler) http.Handler { }) } -// serve starts a local web server -func serve(args []string) { - flags := flag.NewFlagSet("serve", flag.ExitOnError) - invoked := filepath.Base(os.Args[0]) - flags.Usage = func() { - fmt.Printf("USAGE: %s serve [options]\n\n", invoked) - fmt.Printf("Options:\n\n") - flags.PrintDefaults() - } - portFlag := flags.String("port", ":8080", "Local port to serve on") - dirFlag := flags.String("directory", ".", "Directory to serve") - _ = flags.Parse(args) +func serveCmd() *cobra.Command { + var ( + port string + dir string + ) + cmd := &cobra.Command{ + Use: "serve", + Short: "serve a directory over http", + Long: `Serve a directory over http.`, + RunE: func(cmd *cobra.Command, args []string) error { + http.Handle("/", http.FileServer(http.Dir(dir))) + log.Fatal(http.ListenAndServe(port, logRequest(http.DefaultServeMux))) - http.Handle("/", http.FileServer(http.Dir(*dirFlag))) - log.Fatal(http.ListenAndServe(*portFlag, logRequest(http.DefaultServeMux))) + return nil + }, + } + + cmd.Flags().StringVar(&port, "port", ":8080", "Local port to serve on") + cmd.Flags().StringVar(&dir, "directory", ".", "Directory to serve") + + return cmd } diff --git a/src/cmd/linuxkit/util.go b/src/cmd/linuxkit/util.go index 92601d639..0dc5efd3c 100644 --- a/src/cmd/linuxkit/util.go +++ b/src/cmd/linuxkit/util.go @@ -16,6 +16,10 @@ func (f *multipleFlag) String() string { return "A multiple flag is a type of flag that can be repeated any number of times" } +func (f *multipleFlag) Type() string { + return "[]string" +} + func (f *multipleFlag) Set(value string) error { *f = append(*f, value) return nil @@ -138,6 +142,10 @@ func (f *flagOverEnvVarOverDefaultString) Set(value string) error { return nil } +func (f *flagOverEnvVarOverDefaultString) Type() string { + return "string" +} + // Convert a multi-line string into an array of strings func splitLines(in string) []string { var res []string @@ -199,6 +207,10 @@ func (l *Disks) String() string { return fmt.Sprint(*l) } +func (l *Disks) Type() string { + return "[]DiskConfig" +} + // Set is used by flag to configure value from CLI func (l *Disks) Set(value string) error { d := DiskConfig{} diff --git a/src/cmd/linuxkit/util/flags.go b/src/cmd/linuxkit/util/flags.go index 7ae71b8eb..33f47bfeb 100644 --- a/src/cmd/linuxkit/util/flags.go +++ b/src/cmd/linuxkit/util/flags.go @@ -1,10 +1,8 @@ package util import ( - "flag" - "fmt" + "errors" stdlog "log" - "os" ggcrlog "github.com/google/go-containerregistry/pkg/logs" log "github.com/sirupsen/logrus" @@ -26,37 +24,18 @@ func (f *infoFormatter) Format(entry *log.Entry) ([]byte, error) { return defaultLogFormatter.Format(entry) } -var ( - flagQuiet, flagVerbose *bool -) - -// AddLoggingFlags add the logging flags to a flagset, or, if none given, -// the default flag package -func AddLoggingFlags(fs *flag.FlagSet) { - // if we have no flagset, add it to the default flag package - if fs == nil { - flagQuiet = flag.Bool("q", false, "Quiet execution") - flagVerbose = flag.Bool("v", false, "Verbose execution") - } else { - flagQuiet = fs.Bool("q", false, "Quiet execution") - flagVerbose = fs.Bool("v", false, "Verbose execution") - } -} - // SetupLogging once the flags have been parsed, setup the logging -func SetupLogging() { +func SetupLogging(quiet, verbose bool) error { // Set up logging log.SetFormatter(new(infoFormatter)) log.SetLevel(log.InfoLevel) - flag.Parse() - if *flagQuiet && *flagVerbose { - fmt.Printf("Can't set quiet and verbose flag at the same time\n") - os.Exit(1) + if quiet && verbose { + return errors.New("can't set quiet and verbose flag at the same time") } - if *flagQuiet { + if quiet { log.SetLevel(log.ErrorLevel) } - if *flagVerbose { + if verbose { // Switch back to the standard formatter log.SetFormatter(defaultLogFormatter) log.SetLevel(log.DebugLevel) @@ -65,4 +44,5 @@ func SetupLogging() { ggcrlog.Debug = stdlog.New(log.StandardLogger().WriterLevel(log.DebugLevel), "", 0) } ggcrlog.Progress = stdlog.New(log.StandardLogger().WriterLevel(log.InfoLevel), "", 0) + return nil } diff --git a/src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/LICENSE b/src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/LICENSE new file mode 100644 index 000000000..5f920e973 --- /dev/null +++ b/src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 Alan Shreve (@inconshreveable) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/README.md b/src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/README.md new file mode 100644 index 000000000..7a950d177 --- /dev/null +++ b/src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/README.md @@ -0,0 +1,23 @@ +# mousetrap + +mousetrap is a tiny library that answers a single question. + +On a Windows machine, was the process invoked by someone double clicking on +the executable file while browsing in explorer? + +### Motivation + +Windows developers unfamiliar with command line tools will often "double-click" +the executable for a tool. Because most CLI tools print the help and then exit +when invoked without arguments, this is often very frustrating for those users. + +mousetrap provides a way to detect these invocations so that you can provide +more helpful behavior and instructions on how to run the CLI tool. To see what +this looks like, both from an organizational and a technical perspective, see +https://inconshreveable.com/09-09-2014/sweat-the-small-stuff/ + +### The interface + +The library exposes a single interface: + + func StartedByExplorer() (bool) diff --git a/src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/trap_others.go b/src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/trap_others.go new file mode 100644 index 000000000..9d2d8a4ba --- /dev/null +++ b/src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/trap_others.go @@ -0,0 +1,15 @@ +// +build !windows + +package mousetrap + +// StartedByExplorer returns true if the program was invoked by the user +// double-clicking on the executable from explorer.exe +// +// It is conservative and returns false if any of the internal calls fail. +// It does not guarantee that the program was run from a terminal. It only can tell you +// whether it was launched from explorer.exe +// +// On non-Windows platforms, it always returns false. +func StartedByExplorer() bool { + return false +} diff --git a/src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/trap_windows.go b/src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/trap_windows.go new file mode 100644 index 000000000..336142a5e --- /dev/null +++ b/src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/trap_windows.go @@ -0,0 +1,98 @@ +// +build windows +// +build !go1.4 + +package mousetrap + +import ( + "fmt" + "os" + "syscall" + "unsafe" +) + +const ( + // defined by the Win32 API + th32cs_snapprocess uintptr = 0x2 +) + +var ( + kernel = syscall.MustLoadDLL("kernel32.dll") + CreateToolhelp32Snapshot = kernel.MustFindProc("CreateToolhelp32Snapshot") + Process32First = kernel.MustFindProc("Process32FirstW") + Process32Next = kernel.MustFindProc("Process32NextW") +) + +// ProcessEntry32 structure defined by the Win32 API +type processEntry32 struct { + dwSize uint32 + cntUsage uint32 + th32ProcessID uint32 + th32DefaultHeapID int + th32ModuleID uint32 + cntThreads uint32 + th32ParentProcessID uint32 + pcPriClassBase int32 + dwFlags uint32 + szExeFile [syscall.MAX_PATH]uint16 +} + +func getProcessEntry(pid int) (pe *processEntry32, err error) { + snapshot, _, e1 := CreateToolhelp32Snapshot.Call(th32cs_snapprocess, uintptr(0)) + if snapshot == uintptr(syscall.InvalidHandle) { + err = fmt.Errorf("CreateToolhelp32Snapshot: %v", e1) + return + } + defer syscall.CloseHandle(syscall.Handle(snapshot)) + + var processEntry processEntry32 + processEntry.dwSize = uint32(unsafe.Sizeof(processEntry)) + ok, _, e1 := Process32First.Call(snapshot, uintptr(unsafe.Pointer(&processEntry))) + if ok == 0 { + err = fmt.Errorf("Process32First: %v", e1) + return + } + + for { + if processEntry.th32ProcessID == uint32(pid) { + pe = &processEntry + return + } + + ok, _, e1 = Process32Next.Call(snapshot, uintptr(unsafe.Pointer(&processEntry))) + if ok == 0 { + err = fmt.Errorf("Process32Next: %v", e1) + return + } + } +} + +func getppid() (pid int, err error) { + pe, err := getProcessEntry(os.Getpid()) + if err != nil { + return + } + + pid = int(pe.th32ParentProcessID) + return +} + +// StartedByExplorer returns true if the program was invoked by the user double-clicking +// on the executable from explorer.exe +// +// It is conservative and returns false if any of the internal calls fail. +// It does not guarantee that the program was run from a terminal. It only can tell you +// whether it was launched from explorer.exe +func StartedByExplorer() bool { + ppid, err := getppid() + if err != nil { + return false + } + + pe, err := getProcessEntry(ppid) + if err != nil { + return false + } + + name := syscall.UTF16ToString(pe.szExeFile[:]) + return name == "explorer.exe" +} diff --git a/src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/trap_windows_1.4.go b/src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/trap_windows_1.4.go new file mode 100644 index 000000000..9a28e57c3 --- /dev/null +++ b/src/cmd/linuxkit/vendor/github.com/inconshreveable/mousetrap/trap_windows_1.4.go @@ -0,0 +1,46 @@ +// +build windows +// +build go1.4 + +package mousetrap + +import ( + "os" + "syscall" + "unsafe" +) + +func getProcessEntry(pid int) (*syscall.ProcessEntry32, error) { + snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0) + if err != nil { + return nil, err + } + defer syscall.CloseHandle(snapshot) + var procEntry syscall.ProcessEntry32 + procEntry.Size = uint32(unsafe.Sizeof(procEntry)) + if err = syscall.Process32First(snapshot, &procEntry); err != nil { + return nil, err + } + for { + if procEntry.ProcessID == uint32(pid) { + return &procEntry, nil + } + err = syscall.Process32Next(snapshot, &procEntry) + if err != nil { + return nil, err + } + } +} + +// StartedByExplorer returns true if the program was invoked by the user double-clicking +// on the executable from explorer.exe +// +// It is conservative and returns false if any of the internal calls fail. +// It does not guarantee that the program was run from a terminal. It only can tell you +// whether it was launched from explorer.exe +func StartedByExplorer() bool { + pe, err := getProcessEntry(os.Getppid()) + if err != nil { + return false + } + return "explorer.exe" == syscall.UTF16ToString(pe.ExeFile[:]) +} diff --git a/src/cmd/linuxkit/vendor/github.com/mitchellh/go-ps/Vagrantfile b/src/cmd/linuxkit/vendor/github.com/mitchellh/go-ps/Vagrantfile index 9c93a8e80..61662ab1e 100644 --- a/src/cmd/linuxkit/vendor/github.com/mitchellh/go-ps/Vagrantfile +++ b/src/cmd/linuxkit/vendor/github.com/mitchellh/go-ps/Vagrantfile @@ -1,43 +1,43 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! -VAGRANTFILE_API_VERSION = "2" - -Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| - config.vm.box = "chef/ubuntu-12.04" - - config.vm.provision "shell", inline: $script - - ["vmware_fusion", "vmware_workstation"].each do |p| - config.vm.provider "p" do |v| - v.vmx["memsize"] = "1024" - v.vmx["numvcpus"] = "2" - v.vmx["cpuid.coresPerSocket"] = "1" - end - end -end - -$script = <