Skip to content

Commit

Permalink
Move install-go from metal-images to metal-hammer
Browse files Browse the repository at this point in the history
  • Loading branch information
majst01 committed Sep 9, 2024
1 parent 70ee180 commit 457d5b2
Show file tree
Hide file tree
Showing 10 changed files with 2,138 additions and 56 deletions.
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

0 comments on commit 457d5b2

Please sign in to comment.