Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move install-go from metal-images to metal-hammer #137

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 10 additions & 30 deletions cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ import (
"encoding/base64"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"syscall"
"time"

"github.com/metal-stack/metal-hammer/cmd/install"
"github.com/metal-stack/metal-hammer/cmd/utils"
"github.com/metal-stack/metal-hammer/pkg/api"

Expand Down Expand Up @@ -41,7 +40,7 @@ func (h *hammer) Install(machine *models.V1MachineResponse) (*api.Bootinfo, erro
return nil, err
}

info, err := h.install(h.chrootPrefix, machine, s.RootUUID)
info, err := h.installOS(h.chrootPrefix, machine, s.RootUUID)
if err != nil {
return nil, err
}
Expand All @@ -58,9 +57,9 @@ func (h *hammer) Install(machine *models.V1MachineResponse) (*api.Bootinfo, erro
return info, nil
}

// install will execute /install.sh in the pulled docker image which was extracted onto disk
// install will install the OS of the pulled docker image which was extracted onto disk
// to finish installation e.g. install mbr, grub, write network and filesystem config
func (h *hammer) install(prefix string, machine *models.V1MachineResponse, rootUUID string) (*api.Bootinfo, error) {
func (h *hammer) installOS(prefix string, machine *models.V1MachineResponse, rootUUID string) (*api.Bootinfo, error) {
h.log.Info("install", "image", machine.Allocation.Image.URL)

err := h.writeInstallerConfig(machine, rootUUID)
Expand All @@ -78,41 +77,22 @@ func (h *hammer) install(prefix string, machine *models.V1MachineResponse, rootU
return nil, err
}

installBinary := "/install.sh"
if fileExists(path.Join(prefix, "install-go")) {
installBinary = "/install-go"
}

h.log.Info("running install", "binary", installBinary, "prefix", prefix)
h.log.Info("running install", "prefix", prefix)
err = os.Chdir(prefix)
if err != nil {
return nil, fmt.Errorf("unable to chdir to: %s error %w", prefix, err)
}
cmd := exec.Command(installBinary)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
// these syscalls are required to execute the command in a chroot env.
cmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: uint32(0),
Gid: uint32(0),
Groups: []uint32{0},
},
Chroot: prefix,
}
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("running %q in chroot failed %w", installBinary, err)
}

err = os.Chdir("/")
err = install.Run()
if err != nil {
return nil, fmt.Errorf("unable to chdir to: / error %w", err)
return nil, fmt.Errorf("unable to install, error %w", err)
}
h.log.Info("finish running", "binary", installBinary)

err = os.Remove(path.Join(prefix, installBinary))
err = os.Chdir("/")
if err != nil {
h.log.Warn("unable to remove, ignoring", "binary", installBinary, "error", err)
return nil, fmt.Errorf("unable to chdir to: / error %w", err)
}
h.log.Info("finish installing OS")

info, err := kernel.ReadBootinfo(path.Join(prefix, "etc", "metal", "boot-info.yaml"))
if err != nil {
Expand Down
84 changes: 84 additions & 0 deletions cmd/install/cmdexec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package install

import (
"context"
"io"
"log/slog"
"os"
"os/exec"
"strings"
"time"
)

type cmdexec struct {
log *slog.Logger
c func(ctx context.Context, name string, arg ...string) *exec.Cmd
}

type cmdParams struct {
name string
args []string
dir string
timeout time.Duration
combined bool
stdin string
env []string
}

func (i *cmdexec) command(p *cmdParams) (out string, err error) {
var (
start = time.Now()
output []byte
)
i.log.Info("running command", "command", strings.Join(append([]string{p.name}, p.args...), " "), "start", start.String())

ctx := context.Background()
if p.timeout != 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, p.timeout)
defer cancel()
}

cmd := i.c(ctx, p.name, p.args...)
if p.dir != "" {
cmd.Dir = "/etc/metal"
}

cmd.Env = append(cmd.Env, p.env...)

// show stderr
cmd.Stderr = os.Stderr

if p.stdin != "" {
stdin, err := cmd.StdinPipe()
if err != nil {
return "", err
}

go func() {
defer stdin.Close()
_, err = io.WriteString(stdin, p.stdin)
if err != nil {
i.log.Error("error when writing to command's stdin", "error", err)
}
}()
}

if p.combined {
output, err = cmd.CombinedOutput()
} else {
output, err = cmd.Output()
}

out = string(output)
took := time.Since(start)

if err != nil {
i.log.Error("executed command with error", "output", out, "duration", took.String(), "error", err)
return "", err
}

i.log.Info("executed command", "output", out, "duration", took.String())

return
}
69 changes: 69 additions & 0 deletions cmd/install/cmdexec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package install

import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// tests were inspired by this blog article: https://npf.io/2015/06/testing-exec-command/

type fakeexec struct {
t *testing.T
mockCount int
mocks []fakeexecparams
}

// nolint:musttag
type fakeexecparams struct {
WantCmd []string `json:"want_cmd"`
Output string `json:"output"`
ExitCode int `json:"exit_code"`
}

func fakeCmd(t *testing.T, params ...fakeexecparams) func(ctx context.Context, command string, args ...string) *exec.Cmd {
f := fakeexec{
t: t,
mocks: params,
}
return f.command
}

func (f *fakeexec) command(ctx context.Context, command string, args ...string) *exec.Cmd {
if f.mockCount >= len(f.mocks) {
require.Fail(f.t, "more commands called than mocks are available")
}

params := f.mocks[f.mockCount]
f.mockCount++

assert.Equal(f.t, params.WantCmd, append([]string{command}, args...))

j, err := json.Marshal(params)
require.NoError(f.t, err)

cs := []string{"-test.run=TestHelperProcess", "--", string(j)}
cmd := exec.CommandContext(ctx, os.Args[0], cs...) //nolint
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
return cmd
}

func TestHelperProcess(t *testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}

var f fakeexecparams
err := json.Unmarshal([]byte(os.Args[3]), &f)
require.NoError(t, err)

fmt.Fprint(os.Stdout, f.Output)

os.Exit(f.ExitCode)
}
Loading
Loading