Move avoidable daemon shell-outs into Go
Reduce the control plane's dependency on helper scripts while keeping the hard Linux integration points in the approved shell-out layer. Replace the bash-driven image build path with a native Go builder that clones and optionally resizes the rootfs, boots a temporary Firecracker VM, provisions the guest over SSH, installs packages and modules, and preserves the package-manifest sidecar. Also replace a few small convenience shell-outs with Go helpers: read process stats from /proc, use os.Truncate for ext4 image growth, add file-clone and normalized-line helpers, drop the sh -c work-disk flattening path, and launch Firecracker via a direct sudo command. Add tests for the new SSH/archive and system helpers, plus a policy test that keeps os/exec imports confined to cli/firecracker/system. Update the docs to describe customize.sh as a manual helper rather than the daemon's image-build backend. Validated with go mod tidy, go test ./..., and make build.
This commit is contained in:
parent
0a0b0b617b
commit
942d242c03
17 changed files with 936 additions and 145 deletions
170
internal/guest/ssh.go
Normal file
170
internal/guest/ssh.go
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
package guest
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
client *ssh.Client
|
||||
}
|
||||
|
||||
func WaitForSSH(ctx context.Context, address, privateKeyPath string, interval time.Duration) error {
|
||||
if interval <= 0 {
|
||||
interval = time.Second
|
||||
}
|
||||
for {
|
||||
client, err := Dial(ctx, address, privateKeyPath)
|
||||
if err == nil {
|
||||
_ = client.Close()
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(interval):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Dial(ctx context.Context, address, privateKeyPath string) (*Client, error) {
|
||||
signer, err := privateKeySigner(privateKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config := &ssh.ClientConfig{
|
||||
User: "root",
|
||||
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
dialer := &net.Dialer{Timeout: 10 * time.Second}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sshConn, chans, reqs, err := ssh.NewClientConn(conn, address, config)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
client := ssh.NewClient(sshConn, chans, reqs)
|
||||
return &Client{client: client}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
if c == nil || c.client == nil {
|
||||
return nil
|
||||
}
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
func (c *Client) RunScript(ctx context.Context, script string, logWriter io.Writer) error {
|
||||
return c.runSession(ctx, "bash -se", strings.NewReader(script), logWriter)
|
||||
}
|
||||
|
||||
func (c *Client) StreamTar(ctx context.Context, sourceDir, remoteCommand string, logWriter io.Writer) error {
|
||||
reader, writer := io.Pipe()
|
||||
writeErr := make(chan error, 1)
|
||||
go func() {
|
||||
writeErr <- writeTarArchive(writer, sourceDir)
|
||||
_ = writer.Close()
|
||||
}()
|
||||
|
||||
runErr := c.runSession(ctx, remoteCommand, reader, logWriter)
|
||||
tarErr := <-writeErr
|
||||
return errors.Join(runErr, tarErr)
|
||||
}
|
||||
|
||||
func (c *Client) runSession(ctx context.Context, command string, stdin io.Reader, logWriter io.Writer) error {
|
||||
if c == nil || c.client == nil {
|
||||
return fmt.Errorf("ssh client is not connected")
|
||||
}
|
||||
session, err := c.client.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer session.Close()
|
||||
session.Stdin = stdin
|
||||
if logWriter != nil {
|
||||
session.Stdout = logWriter
|
||||
session.Stderr = logWriter
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = c.client.Close()
|
||||
case <-done:
|
||||
}
|
||||
}()
|
||||
|
||||
err = session.Run(command)
|
||||
done <- nil
|
||||
return err
|
||||
}
|
||||
|
||||
func privateKeySigner(path string) (ssh.Signer, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ssh.ParsePrivateKey(data)
|
||||
}
|
||||
|
||||
func writeTarArchive(dst io.Writer, sourceDir string) error {
|
||||
tw := tar.NewWriter(dst)
|
||||
defer tw.Close()
|
||||
|
||||
sourceDir = filepath.Clean(sourceDir)
|
||||
rootName := filepath.Base(sourceDir)
|
||||
return filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := rootName
|
||||
if path != sourceDir {
|
||||
relPath, err := filepath.Rel(sourceDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name = filepath.Join(rootName, relPath)
|
||||
}
|
||||
linkTarget := ""
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
linkTarget, err = os.Readlink(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
header, err := tar.FileInfoHeader(info, linkTarget)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = name
|
||||
if err := tw.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return nil
|
||||
}
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = io.Copy(tw, file)
|
||||
return err
|
||||
})
|
||||
}
|
||||
58
internal/guest/ssh_test.go
Normal file
58
internal/guest/ssh_test.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package guest
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWriteTarArchiveKeepsTopLevelDirectory(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sourceDir := filepath.Join(t.TempDir(), "6.8.0-test")
|
||||
if err := os.MkdirAll(filepath.Join(sourceDir, "kernel"), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(sourceDir, "modules.dep"), []byte("deps"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile modules.dep: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(sourceDir, "kernel", "module.ko"), []byte("ko"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile module.ko: %v", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := writeTarArchive(&buf, sourceDir); err != nil {
|
||||
t.Fatalf("writeTarArchive: %v", err)
|
||||
}
|
||||
|
||||
tr := tar.NewReader(bytes.NewReader(buf.Bytes()))
|
||||
var names []string
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("tar.Next: %v", err)
|
||||
}
|
||||
names = append(names, header.Name)
|
||||
}
|
||||
|
||||
want := map[string]struct{}{
|
||||
"6.8.0-test": {},
|
||||
"6.8.0-test/modules.dep": {},
|
||||
"6.8.0-test/kernel": {},
|
||||
"6.8.0-test/kernel/module.ko": {},
|
||||
}
|
||||
if len(names) != len(want) {
|
||||
t.Fatalf("archive names = %v, want %d entries", names, len(want))
|
||||
}
|
||||
for _, name := range names {
|
||||
if _, ok := want[name]; !ok {
|
||||
t.Fatalf("unexpected archive entry %q in %v", name, names)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue