Files
nomad/client/allocrunner/taskrunner/template/template_default.go
Piotr Kazmierczak 356ea87e00 template: disable sandboxed rendering on Windows (#23432)
Following #23443, we no longer need to sandbox template rendering on Windows.
2024-06-28 17:16:27 +02:00

191 lines
5.0 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !windows
package template
import (
"bytes"
"context"
"fmt"
"io"
"log"
"os"
"os/exec"
"strconv"
"time"
"github.com/hashicorp/consul-template/renderer"
trenderer "github.com/hashicorp/nomad/client/allocrunner/taskrunner/template/renderer"
"github.com/hashicorp/nomad/helper/subproc"
)
// renderTemplateInSandbox runs the template-render command in a subprocess that
// will chroot itself to prevent a task from swapping a directory between the
// sandbox path and the destination with a symlink pointing to somewhere outside
// the sandbox.
//
// See renderer/ subdirectory for implementation.
func renderTemplateInSandbox(cfg *sandboxConfig) (string, int, error) {
// Safe to inject user input as command arguments since Go's exec.Command
// does not invoke a shell.
args := []string{
"template-render",
"write",
"-sandbox-path", cfg.sandboxPath,
"-dest-path", cfg.destPath,
"-perms", cfg.perms,
}
if cfg.user != "" {
args = append(args, "-user")
args = append(args, cfg.user)
}
if cfg.group != "" {
args = append(args, "-group")
args = append(args, cfg.group)
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
// note: we can't simply set cmd.SysProcAttr.Chroot here because the Nomad
// binary isn't in the chroot
cmd := exec.CommandContext(ctx, cfg.thisBin, args...)
stdin, err := cmd.StdinPipe()
if err != nil {
return "", 1, err
}
go func() {
defer stdin.Close()
io.Copy(stdin, bytes.NewReader(cfg.contents))
}()
out, err := cmd.CombinedOutput()
code := cmd.ProcessState.ExitCode()
if code == trenderer.ExitWouldRenderButDidnt {
err = nil // erase the "exit code 117" error
}
return string(out), code, err
}
// readTemplateFromSandbox runs the template-render command in a subprocess that
// will chroot itself to prevent a task from swapping a directory between the
// sandbox path and the source with a symlink pointing to somewhere outside
// the sandbox.
func readTemplateFromSandbox(cfg *sandboxConfig) ([]byte, []byte, int, error) {
// Safe to inject user input as command arguments since Go's exec.Command
// does not invoke a shell. Also, the only user-controlled argument here is
// the source path which we've already verified is at least a valid path in
// the caller.
args := []string{
"template-render",
"read",
"-sandbox-path", cfg.sandboxPath,
"-source-path", cfg.sourcePath,
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
// note: we can't simply set cmd.SysProcAttr.Chroot here because the Nomad
// binary isn't in the chroot
cmd := exec.CommandContext(ctx, cfg.thisBin, args...)
var outb, errb bytes.Buffer
cmd.Stdout = &outb
cmd.Stderr = &errb
err := cmd.Run()
stdout := outb.Bytes()
stderr := errb.Bytes()
return stdout, stderr, cmd.ProcessState.ExitCode(), err
}
func RenderFn(taskID, taskDir string, sandboxEnabled bool) func(*renderer.RenderInput) (*renderer.RenderResult, error) {
if !sandboxEnabled {
return nil
}
thisBin := subproc.Self()
return func(i *renderer.RenderInput) (*renderer.RenderResult, error) {
wouldRender := false
didRender := false
sandboxCfg := &sandboxConfig{
thisBin: thisBin,
sandboxPath: taskDir,
destPath: i.Path,
perms: strconv.FormatUint(uint64(i.Perms), 8),
user: i.User,
group: i.Group,
taskID: taskID,
contents: i.Contents,
}
logs, code, err := renderTemplateInSandbox(sandboxCfg)
if err != nil {
if len(logs) > 0 {
log.Printf("[ERROR] %v: %s", err, logs)
} else {
log.Printf("[ERROR] %v", err)
}
return &renderer.RenderResult{
DidRender: false,
WouldRender: false,
Contents: []byte{},
}, fmt.Errorf("template render subprocess failed: %w", err)
}
if code == trenderer.ExitWouldRenderButDidnt {
didRender = false
wouldRender = true
} else {
didRender = true
wouldRender = true
}
// the subprocess emits logs matching the consul-template runner, but we
// CT doesn't support hclog, so we just print the whole output here to
// stderr the same way CT does so the results look seamless
if len(logs) > 0 {
log.Printf("[DEBUG] %s", logs)
}
result := &renderer.RenderResult{
DidRender: didRender,
WouldRender: wouldRender,
Contents: i.Contents,
}
return result, nil
}
}
func ReaderFn(taskID, taskDir string, sandboxEnabled bool) func(string) ([]byte, error) {
if !sandboxEnabled {
return nil
}
thisBin := subproc.Self()
return func(src string) ([]byte, error) {
sandboxCfg := &sandboxConfig{
thisBin: thisBin,
sandboxPath: taskDir,
sourcePath: src,
taskID: taskID,
}
stdout, stderr, code, err := readTemplateFromSandbox(sandboxCfg)
if err != nil && code != 0 {
return nil, fmt.Errorf("%v: %s", err, string(stderr))
}
// this will get wrapped in CT log formatter
fmt.Fprintf(os.Stderr, "[DEBUG] %s", string(stderr))
return stdout, nil
}
}