From 13aa14917552a42b2307f2d4b52b0fe134cdc17c Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Tue, 14 Jun 2022 10:09:38 -0400 Subject: [PATCH] Make `/usr/bin/cosa` a Go program This creates an initial skeleton for Go code at the very toplevel of the project. What is currently the `coreos-assembler` shell script entrypoint is changed to be embedded via Go file embedding into `/usr/bin/cosa`. This is a pattern I think we'll use to aid the transition; rather than trying to rewrite things wholesale in Go, we'll continue to exec some shell scripts. There's an embryonic `bashexec` internal Go module that is designed to help with this. Closes: https://github.com/coreos/coreos-assembler/issues/2821 --- .gitignore | 1 + Makefile | 10 ++- cmd/cosa.go | 141 ++++++++++++++++++++++++++++++ go.mod | 3 + internal/pkg/bashexec/bashexec.go | 60 +++++++++++++ src/coreos-assembler | 77 ---------------- 6 files changed, 212 insertions(+), 80 deletions(-) create mode 100644 cmd/cosa.go create mode 100644 go.mod create mode 100644 internal/pkg/bashexec/bashexec.go delete mode 100755 src/coreos-assembler diff --git a/.gitignore b/.gitignore index ab18382565..494336dfe3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ maipo/ .coverage tools/bin .idea +bin/ diff --git a/Makefile b/Makefile index 5a16e7d5e3..85f129a453 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ PYIGNORE ?= E128,E241,E402,E501,E722,W503,W504 MANTLE_BINARIES := ore kola plume -all: tools mantle gangplank +all: bin/cosa tools mantle gangplank src:=$(shell find src -maxdepth 1 -type f -executable -print) pysources=$(shell find src -type f -name '*.py') $(shell for x in $(src); do if head -1 $$x | grep -q python; then echo $$x; fi; done) @@ -30,6 +30,10 @@ else ifeq ($(GOARCH),aarch64) GOARCH="arm64" endif +bin/cosa: + go build -mod vendor -o $@ cmd/cosa.go +.PHONY: bin/cosa + .%.shellchecked: % ./tests/check_one.sh $< $@ @@ -114,9 +118,9 @@ install: install -d $(DESTDIR)$(PREFIX)/lib/coreos-assembler/cosalib install -D -t $(DESTDIR)$(PREFIX)/lib/coreos-assembler/cosalib $$(find src/cosalib/ -maxdepth 1 -type f) install -d $(DESTDIR)$(PREFIX)/bin - ln -sf ../lib/coreos-assembler/coreos-assembler $(DESTDIR)$(PREFIX)/bin/ + install bin/cosa $(DESTDIR)$(PREFIX)/bin/ ln -sf ../lib/coreos-assembler/cp-reflink $(DESTDIR)$(PREFIX)/bin/ - ln -sf coreos-assembler $(DESTDIR)$(PREFIX)/bin/cosa + ln -sf cosa $(DESTDIR)$(PREFIX)/bin/coreos-assembler install -d $(DESTDIR)$(PREFIX)/lib/coreos-assembler/tests/kola cd tools && $(MAKE) install DESTDIR=$(DESTDIR) cd mantle && $(MAKE) install DESTDIR=$(DESTDIR) diff --git a/cmd/cosa.go b/cmd/cosa.go new file mode 100644 index 0000000000..d1982c6272 --- /dev/null +++ b/cmd/cosa.go @@ -0,0 +1,141 @@ +package main + +import ( + _ "embed" + "fmt" + "io/ioutil" + "os" + "os/exec" + "sort" + "strings" +) + +// commands we'd expect to use in the local dev path +var buildCommands = []string{"init", "fetch", "build", "run", "prune", "clean", "list"} +var advancedBuildCommands = []string{"buildfetch", "buildupload", "oc-adm-release", "push-container", "upload-oscontainer"} +var buildextendCommands = []string{"aliyun", "aws", "azure", "digitalocean", "exoscale", "gcp", "ibmcloud", "kubevirt", "live", "metal", "metal4k", "nutanix", "openstack", "qemu", "secex", "virtualbox", "vmware", "vultr"} +var utilityCommands = []string{"aws-replicate", "compress", "generate-hashlist", "koji-upload", "kola", "remote-build-container", "remote-prune", "sign", "tag"} +var otherCommands = []string{"shell", "meta"} + +func init() { + // Note buildCommands is intentionally listed in frequency order + sort.Strings(advancedBuildCommands) + sort.Strings(buildextendCommands) + sort.Strings(utilityCommands) + sort.Strings(otherCommands) +} + +func printCommands(title string, cmds []string) { + fmt.Printf("%s:\n", title) + for _, cmd := range cmds { + fmt.Printf(" %s\n", cmd) + } +} + +func printUsage() { + fmt.Println("Usage: coreos-assembler CMD ...") + printCommands("Build commands", buildCommands) + printCommands("Advanced build commands", advancedBuildCommands) + printCommands("Platform builds", buildextendCommands) + printCommands("Utility commands", utilityCommands) + printCommands("Other commands", otherCommands) +} + +func run(argv []string) error { + if err := sanityCheckEnvironment(); err != nil { + return err + } + if err := initializeGlobalState(); err != nil { + return err + } + + var cmd string + if len(argv) > 0 { + cmd = argv[0] + argv = argv[1:] + } + + if cmd == "" { + printUsage() + os.Exit(1) + } + + target := fmt.Sprintf("/usr/lib/coreos-assembler/cmd-%s", cmd) + _, err := os.Stat(target) + if err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("unknown command: %s", cmd) + } + return fmt.Errorf("failed to stat %s: %w", target, err) + } + + c := exec.Command(target, argv...) + c.Stdin = os.Stdin + c.Stdout = os.Stdout + c.Stderr = os.Stderr + return c.Run() +} + +func initializeGlobalState() error { + // Set PYTHONUNBUFFERED=1 so that we get unbuffered output. We should + // be able to do this on the shebang lines but env doesn't support args + // right now. In Fedora we should be able to use the `env -S` option. + os.Setenv("export PYTHONUNBUFFERED", "1") + + // docker/podman don't run through PAM, but we want this set for the privileged + // (non-virtualized) path + + user, ok := os.LookupEnv("USER") + if !ok { + b, err := exec.Command("id", "-nu").Output() + if err != nil { + return err + } + user = strings.TrimSpace(string(b)) + os.Setenv("USER", user) + } + + // When trying to connect to libvirt we get "Failed to find user record + // for uid" errors if there is no entry for our UID in /etc/passwd. + // This was taken from 'Support Arbitrary User IDs' section of: + // https://docs.openshift.com/container-platform/3.10/creating_images/guidelines.html + c := exec.Command("whoami") + c.Stdout = ioutil.Discard + c.Stderr = ioutil.Discard + if err := c.Run(); err != nil { + home := fmt.Sprintf("/var/tmp/%s", user) + err := os.MkdirAll(home, 0755) + if err != nil { + return err + } + f, err := os.OpenFile("/etc/passwd", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + id := os.Getuid() + buf := fmt.Sprintf("%s:x:%d:0:%s user:%s:/sbin/nologin\n", user, id, user, home) + if _, err = f.WriteString(buf); err != nil { + return err + } + } + + return nil +} + +func sanityCheckEnvironment() error { + // https://github.com/containers/libpod/issues/1448 + if _, err := os.Stat("/sys/fs/selinux/status"); err == nil { + return fmt.Errorf("/sys/fs/selinux appears to be mounted but should not be") + } + + return nil +} + +func main() { + err := run(os.Args[1:]) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000..2e394d206e --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/coreos/coreos-assembler + +go 1.15 diff --git a/internal/pkg/bashexec/bashexec.go b/internal/pkg/bashexec/bashexec.go new file mode 100644 index 0000000000..092db6d5e0 --- /dev/null +++ b/internal/pkg/bashexec/bashexec.go @@ -0,0 +1,60 @@ +package bashexec + +import ( + "fmt" + "io" + "os" + "os/exec" + "strings" +) + +// BashRunner is a wrapper for executing in-memory bash scripts +type BashRunner struct { + name string + cmd *exec.Cmd +} + +// NewBashRunner creates a bash executor from in-memory shell script. +func NewBashRunner(name, src string, args ...string) (*BashRunner, error) { + f, err := os.CreateTemp("", name) + if err != nil { + return nil, err + } + if _, err := io.Copy(f, strings.NewReader(src)); err != nil { + return nil, err + } + if err := os.Remove(f.Name()); err != nil { + return nil, err + } + + fullargs := append([]string{"-c", ". /proc/self/fd/3", name}, args...) + cmd := exec.Command("/bin/bash", fullargs...) + cmd.Stdin = os.Stdin + cmd.ExtraFiles = append(cmd.ExtraFiles, f) + + return &BashRunner{ + name: name, + cmd: cmd, + }, nil +} + +// Exec synchronously spawns the child process, passing stdin/stdout/stderr directly. +func (r *BashRunner) Exec() error { + r.cmd.Stdin = os.Stdin + r.cmd.Stdout = os.Stdout + r.cmd.Stderr = os.Stderr + err := r.cmd.Run() + if err != nil { + return fmt.Errorf("failed to execute internal script %s: %w", r.name, err) + } + return nil +} + +// Run spawns the script, gathering stdout/stderr into a buffer that is displayed only on error. +func (r *BashRunner) Run() error { + buf, err := r.cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to execute internal script %s: %w\n%s", r.name, err, buf) + } + return nil +} diff --git a/src/coreos-assembler b/src/coreos-assembler deleted file mode 100755 index 740bab2584..0000000000 --- a/src/coreos-assembler +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -# Usage: coreos-assembler ... -# Currently this just wraps the two binaries we have today -# under a global entrypoint with subcommands. - -# Set PYTHONUNBUFFERED=1 so that we get unbuffered output. We should -# be able to do this on the shebang lines but env doesn't support args -# right now. In Fedora we should be able to use the `env -S` option. -export PYTHONUNBUFFERED=1 - -# docker/podman don't run through PAM, but we want this set for the privileged -# (non-virtualized) path -export USER="${USER:-$(id -nu)}" - -# When trying to connect to libvirt we get "Failed to find user record -# for uid" errors if there is no entry for our UID in /etc/passwd. -# This was taken from 'Support Arbitrary User IDs' section of: -# https://docs.openshift.com/container-platform/3.10/creating_images/guidelines.html -if ! whoami &> /dev/null; then - # We need to make sure we set $HOME in the /etc/passwd file because - # if we don't libvirt will try to use `/` and we will get permission - # issues - export HOME="/var/tmp/${USER_NAME:-default}" && mkdir -p "$HOME" - if [ -w /etc/passwd ]; then - echo "${USER_NAME:-default}:x:$(id -u):0:${USER_NAME:-default} user:${HOME}:/sbin/nologin" >> /etc/passwd - fi -fi - -# https://github.com/containers/libpod/issues/1448 -if [ -e /sys/fs/selinux/status ]; then - echo "error: /sys/fs/selinux appears to be mounted but should not be" 1>&2 - exit 1 -fi - -cmd=${1:-} -# commands we'd expect to use in the local dev path -build_commands="init fetch build run prune clean list" -# commands more likely to be used in a prod pipeline only -advanced_build_commands="buildfetch buildupload oc-adm-release push-container upload-oscontainer" -buildextend_commands="aliyun aws azure digitalocean exoscale gcp ibmcloud kubevirt live metal metal4k nutanix openstack qemu secex virtualbox vmware vultr" -utility_commands="aws-replicate compress generate-hashlist koji-upload kola remote-build-container remote-prune sign tag" -other_commands="shell meta" -if [ -z "${cmd}" ]; then - echo Usage: "coreos-assembler CMD ..." - echo "Build commands:" - for bin in ${build_commands}; do - echo " ${bin}" - done # don't sort these ones, they're roughly in the order they're used - - echo "Advanced build commands:" - for bin in ${advanced_build_commands}; do - echo " ${bin}" - done && for bin in ${buildextend_commands}; do - echo " buildextend-${bin}" - done | sort - - echo "Utility commands:" - for bin in ${utility_commands}; do - echo " ${bin}" - done | sort - - echo "Other commands:" - for bin in ${other_commands}; do - echo " ${bin}" - done | sort - exit 1 -fi -shift - -target=/usr/lib/coreos-assembler/cmd-${cmd} -if test -x "${target}"; then - exec "${target}" "$@" -fi - -echo "Unknown command: ${cmd}" 1>&2 -exit 1