feat: backend native executor implement
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
// Package hostnginx manages the exact backend upstream block used by the current host Nginx deployment.
|
||||
package hostnginx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
managedBegin = "# yms-update managed upstream begin"
|
||||
managedEnd = "# yms-update managed upstream end"
|
||||
port8080 = 8080
|
||||
port8081 = 8081
|
||||
)
|
||||
|
||||
// ActiveBackendPort reads the one uncommented backend server in the managed upstream block.
|
||||
func ActiveBackendPort(content []byte) (int, error) {
|
||||
block, err := parseManagedBlock(content)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
active := 0
|
||||
for _, server := range block.servers {
|
||||
if !server.commented {
|
||||
if active != 0 {
|
||||
return 0, errors.New("managed Nginx upstream contains more than one active backend server")
|
||||
}
|
||||
active = server.port
|
||||
}
|
||||
}
|
||||
if active == 0 {
|
||||
return 0, errors.New("managed Nginx upstream does not contain an active backend server")
|
||||
}
|
||||
return active, nil
|
||||
}
|
||||
|
||||
// RenderBackendPort returns a complete configuration with only activePort uncommented.
|
||||
func RenderBackendPort(content []byte, activePort int) ([]byte, error) {
|
||||
if activePort != port8080 && activePort != port8081 {
|
||||
return nil, fmt.Errorf("host Nginx backend port must be 8080 or 8081: %d", activePort)
|
||||
}
|
||||
block, err := parseManagedBlock(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lines := block.lines
|
||||
for _, server := range block.servers {
|
||||
indentLength := len(lines[server.line]) - len(strings.TrimLeft(lines[server.line], " \t"))
|
||||
indent := lines[server.line][:indentLength]
|
||||
serverText := strings.TrimSpace(lines[server.line])
|
||||
serverText = strings.TrimPrefix(serverText, "# ")
|
||||
if server.port == activePort {
|
||||
lines[server.line] = indent + serverText
|
||||
} else {
|
||||
lines[server.line] = indent + "# " + serverText
|
||||
}
|
||||
}
|
||||
rendered := []byte(strings.Join(lines, "\n"))
|
||||
if _, err := ActiveBackendPort(rendered); err != nil {
|
||||
return nil, fmt.Errorf("validate rendered host Nginx backend upstream: %w", err)
|
||||
}
|
||||
return rendered, nil
|
||||
}
|
||||
|
||||
type managedBlock struct {
|
||||
lines []string
|
||||
servers []managedServer
|
||||
}
|
||||
|
||||
type managedServer struct {
|
||||
line int
|
||||
port int
|
||||
commented bool
|
||||
}
|
||||
|
||||
func parseManagedBlock(content []byte) (managedBlock, error) {
|
||||
if len(content) == 0 {
|
||||
return managedBlock{}, errors.New("host Nginx configuration is empty")
|
||||
}
|
||||
lines := strings.Split(string(content), "\n")
|
||||
beginLine := -1
|
||||
endLine := -1
|
||||
for index, line := range lines {
|
||||
switch strings.TrimSpace(line) {
|
||||
case managedBegin:
|
||||
if beginLine != -1 {
|
||||
return managedBlock{}, errors.New("host Nginx configuration contains duplicate managed upstream begin markers")
|
||||
}
|
||||
beginLine = index
|
||||
case managedEnd:
|
||||
if endLine != -1 {
|
||||
return managedBlock{}, errors.New("host Nginx configuration contains duplicate managed upstream end markers")
|
||||
}
|
||||
endLine = index
|
||||
}
|
||||
}
|
||||
if beginLine == -1 || endLine == -1 || endLine <= beginLine {
|
||||
return managedBlock{}, errors.New("host Nginx configuration requires one ordered managed upstream marker pair")
|
||||
}
|
||||
|
||||
servers := make([]managedServer, 0, 2)
|
||||
seenPorts := make(map[int]struct{}, 2)
|
||||
for index := beginLine + 1; index < endLine; index++ {
|
||||
server, found, err := parseManagedServer(lines[index], index)
|
||||
if err != nil {
|
||||
return managedBlock{}, err
|
||||
}
|
||||
if !found {
|
||||
if strings.TrimSpace(lines[index]) != "" {
|
||||
return managedBlock{}, fmt.Errorf("managed Nginx upstream contains an unexpected line: %q", strings.TrimSpace(lines[index]))
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, duplicate := seenPorts[server.port]; duplicate {
|
||||
return managedBlock{}, fmt.Errorf("managed Nginx upstream contains duplicate port %d", server.port)
|
||||
}
|
||||
seenPorts[server.port] = struct{}{}
|
||||
servers = append(servers, server)
|
||||
}
|
||||
for _, port := range []int{port8080, port8081} {
|
||||
if _, found := seenPorts[port]; !found {
|
||||
return managedBlock{}, fmt.Errorf("managed Nginx upstream is missing port %d", port)
|
||||
}
|
||||
}
|
||||
if len(servers) != 2 {
|
||||
return managedBlock{}, fmt.Errorf("managed Nginx upstream must contain exactly two backend servers, got %d", len(servers))
|
||||
}
|
||||
return managedBlock{lines: lines, servers: servers}, nil
|
||||
}
|
||||
|
||||
func parseManagedServer(line string, lineIndex int) (managedServer, bool, error) {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
commented := strings.HasPrefix(trimmed, "# server ")
|
||||
active := strings.HasPrefix(trimmed, "server ")
|
||||
if !commented && !active {
|
||||
return managedServer{}, false, nil
|
||||
}
|
||||
serverText := strings.TrimPrefix(trimmed, "# ")
|
||||
fields := strings.Fields(serverText)
|
||||
if len(fields) != 4 || fields[0] != "server" || fields[2] != "max_fails=1" || fields[3] != "fail_timeout=2s;" {
|
||||
return managedServer{}, false, fmt.Errorf("managed Nginx upstream server line has an unsupported format: %q", trimmed)
|
||||
}
|
||||
_, portText, err := net.SplitHostPort(fields[1])
|
||||
if err != nil {
|
||||
return managedServer{}, false, fmt.Errorf("parse managed Nginx upstream address %q: %w", fields[1], err)
|
||||
}
|
||||
port, err := strconv.Atoi(portText)
|
||||
if err != nil || (port != port8080 && port != port8081) {
|
||||
return managedServer{}, false, fmt.Errorf("managed Nginx upstream contains unsupported backend port %q", portText)
|
||||
}
|
||||
return managedServer{line: lineIndex, port: port, commented: commented}, true, nil
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package hostnginx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const serverConfiguration = `http {
|
||||
upstream yms-server {
|
||||
# yms-update managed upstream begin
|
||||
# server 10.11.1.117:8080 max_fails=1 fail_timeout=2s;
|
||||
server 10.11.1.117:8081 max_fails=1 fail_timeout=2s;
|
||||
# yms-update managed upstream end
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func TestActiveBackendPortReadsExactManagedBlock(t *testing.T) {
|
||||
port, err := ActiveBackendPort([]byte(serverConfiguration))
|
||||
if err != nil || port != 8081 {
|
||||
t.Fatalf("unexpected active backend port: port=%d err=%v", port, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderBackendPortPreservesConfigurationAndSwitchesOneServer(t *testing.T) {
|
||||
rendered, err := RenderBackendPort([]byte(serverConfiguration), 8080)
|
||||
if err != nil {
|
||||
t.Fatalf("render backend port: %v", err)
|
||||
}
|
||||
port, err := ActiveBackendPort(rendered)
|
||||
if err != nil || port != 8080 {
|
||||
t.Fatalf("unexpected rendered backend port: port=%d err=%v", port, err)
|
||||
}
|
||||
if !bytes.Contains(rendered, []byte("server 10.11.1.117:8080 max_fails=1 fail_timeout=2s;")) ||
|
||||
!bytes.Contains(rendered, []byte("# server 10.11.1.117:8081 max_fails=1 fail_timeout=2s;")) {
|
||||
t.Fatalf("rendered configuration does not contain exact server lines:\n%s", rendered)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagedBlockRejectsAmbiguousOrAlteredInput(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"both active": strings.Replace(serverConfiguration, "# server 10.11.1.117:8080", "server 10.11.1.117:8080", 1),
|
||||
"missing port": strings.Replace(serverConfiguration, " # server 10.11.1.117:8080 max_fails=1 fail_timeout=2s;\n", "", 1),
|
||||
"changed option": strings.Replace(serverConfiguration, "max_fails=1", "max_fails=2", 1),
|
||||
"extra line": strings.Replace(serverConfiguration, managedEnd, "# unexpected\n "+managedEnd, 1),
|
||||
}
|
||||
for name, content := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if _, err := ActiveBackendPort([]byte(content)); err == nil {
|
||||
t.Fatal("expected altered managed block rejection")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package hostnginx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Snapshot is the complete host Nginx configuration before or after a switch.
|
||||
type Snapshot struct {
|
||||
Content []byte
|
||||
ActivePort int
|
||||
}
|
||||
|
||||
// Controller validates, atomically replaces, and reloads the current host Nginx configuration.
|
||||
type Controller struct {
|
||||
configPath string
|
||||
nginxExecutable string
|
||||
systemctlPath string
|
||||
nginxServiceName string
|
||||
runner commandRunner
|
||||
}
|
||||
|
||||
// NewController requires every external identifier to be supplied explicitly.
|
||||
func NewController(configPath string, nginxExecutable string, systemctlPath string, nginxServiceName string) (*Controller, error) {
|
||||
return newController(configPath, nginxExecutable, systemctlPath, nginxServiceName, execRunner{})
|
||||
}
|
||||
|
||||
func newController(configPath string, nginxExecutable string, systemctlPath string, nginxServiceName string, runner commandRunner) (*Controller, error) {
|
||||
for _, entry := range []struct {
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{"host Nginx configuration", configPath},
|
||||
{"Nginx executable", nginxExecutable},
|
||||
{"systemctl executable", systemctlPath},
|
||||
} {
|
||||
if !filepath.IsAbs(entry.value) || filepath.Clean(entry.value) != entry.value {
|
||||
return nil, fmt.Errorf("%s must be a clean absolute path", entry.name)
|
||||
}
|
||||
}
|
||||
if nginxServiceName == "" || strings.TrimSpace(nginxServiceName) != nginxServiceName {
|
||||
return nil, errors.New("exact Nginx systemd service name is required")
|
||||
}
|
||||
if runner == nil {
|
||||
return nil, errors.New("host Nginx command runner is required")
|
||||
}
|
||||
return &Controller{
|
||||
configPath: configPath,
|
||||
nginxExecutable: nginxExecutable,
|
||||
systemctlPath: systemctlPath,
|
||||
nginxServiceName: nginxServiceName,
|
||||
runner: runner,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Read returns the complete current configuration and its active backend port.
|
||||
func (c *Controller) Read() (Snapshot, error) {
|
||||
info, err := os.Lstat(c.configPath)
|
||||
if err != nil {
|
||||
return Snapshot{}, fmt.Errorf("inspect host Nginx configuration %s: %w", c.configPath, err)
|
||||
}
|
||||
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
||||
return Snapshot{}, fmt.Errorf("host Nginx configuration is not a direct regular file: %s", c.configPath)
|
||||
}
|
||||
content, err := os.ReadFile(c.configPath)
|
||||
if err != nil {
|
||||
return Snapshot{}, fmt.Errorf("read host Nginx configuration %s: %w", c.configPath, err)
|
||||
}
|
||||
port, err := ActiveBackendPort(content)
|
||||
if err != nil {
|
||||
return Snapshot{}, err
|
||||
}
|
||||
return Snapshot{Content: content, ActivePort: port}, nil
|
||||
}
|
||||
|
||||
// Switch renders activePort from the current configuration, then validates and reloads Nginx.
|
||||
// It returns the durable pre-switch snapshot required for compensation.
|
||||
func (c *Controller) Switch(ctx context.Context, activePort int) (Snapshot, error) {
|
||||
previous, err := c.Read()
|
||||
if err != nil {
|
||||
return Snapshot{}, err
|
||||
}
|
||||
if previous.ActivePort == activePort {
|
||||
return previous, nil
|
||||
}
|
||||
next, err := RenderBackendPort(previous.Content, activePort)
|
||||
if err != nil {
|
||||
return Snapshot{}, err
|
||||
}
|
||||
if err := c.replaceValidateReload(ctx, next, previous.Content); err != nil {
|
||||
return Snapshot{}, err
|
||||
}
|
||||
return previous, nil
|
||||
}
|
||||
|
||||
// Restore atomically restores a previously persisted complete configuration and reloads Nginx.
|
||||
func (c *Controller) Restore(ctx context.Context, snapshot Snapshot) error {
|
||||
if len(snapshot.Content) == 0 {
|
||||
return errors.New("host Nginx restore snapshot is empty")
|
||||
}
|
||||
parsedPort, err := ActiveBackendPort(snapshot.Content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate host Nginx restore snapshot: %w", err)
|
||||
}
|
||||
if parsedPort != snapshot.ActivePort {
|
||||
return fmt.Errorf("host Nginx restore snapshot port mismatch: content=%d metadata=%d", parsedPort, snapshot.ActivePort)
|
||||
}
|
||||
current, err := c.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if string(current.Content) == string(snapshot.Content) {
|
||||
return nil
|
||||
}
|
||||
return c.replaceValidateReload(ctx, snapshot.Content, current.Content)
|
||||
}
|
||||
|
||||
// Apply installs one previously persisted complete snapshot and reloads Nginx.
|
||||
func (c *Controller) Apply(ctx context.Context, snapshot Snapshot) error {
|
||||
if len(snapshot.Content) == 0 {
|
||||
return errors.New("host Nginx apply snapshot is empty")
|
||||
}
|
||||
parsedPort, err := ActiveBackendPort(snapshot.Content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate host Nginx apply snapshot: %w", err)
|
||||
}
|
||||
if parsedPort != snapshot.ActivePort {
|
||||
return fmt.Errorf("host Nginx apply snapshot port mismatch: content=%d metadata=%d", parsedPort, snapshot.ActivePort)
|
||||
}
|
||||
current, err := c.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.replaceValidateReload(ctx, snapshot.Content, current.Content)
|
||||
}
|
||||
|
||||
func (c *Controller) replaceValidateReload(ctx context.Context, desired []byte, rollback []byte) error {
|
||||
if err := c.atomicWrite(desired); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.runner.Run(ctx, c.nginxExecutable, "-t"); err != nil {
|
||||
return errors.Join(
|
||||
fmt.Errorf("validate host Nginx configuration: %w", err),
|
||||
c.restoreAfterFailure(ctx, rollback),
|
||||
)
|
||||
}
|
||||
if err := c.runner.Run(ctx, c.systemctlPath, "reload", "--", c.nginxServiceName); err != nil {
|
||||
return errors.Join(
|
||||
fmt.Errorf("reload host Nginx service %s: %w", c.nginxServiceName, err),
|
||||
c.restoreAfterFailure(ctx, rollback),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) restoreAfterFailure(ctx context.Context, content []byte) error {
|
||||
if err := c.atomicWrite(content); err != nil {
|
||||
return fmt.Errorf("restore host Nginx configuration after failure: %w", err)
|
||||
}
|
||||
validateErr := c.runner.Run(ctx, c.nginxExecutable, "-t")
|
||||
reloadErr := c.runner.Run(ctx, c.systemctlPath, "reload", "--", c.nginxServiceName)
|
||||
return errors.Join(
|
||||
wrapError("validate restored host Nginx configuration", validateErr),
|
||||
wrapError("reload restored host Nginx configuration", reloadErr),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Controller) atomicWrite(content []byte) error {
|
||||
info, err := os.Lstat(c.configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect host Nginx configuration before replacement: %w", err)
|
||||
}
|
||||
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
||||
return fmt.Errorf("host Nginx configuration is not a direct regular file: %s", c.configPath)
|
||||
}
|
||||
parent := filepath.Dir(c.configPath)
|
||||
temporary, err := os.CreateTemp(parent, ".yms-daemon-nginx-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create temporary host Nginx configuration: %w", err)
|
||||
}
|
||||
temporaryPath := temporary.Name()
|
||||
defer os.Remove(temporaryPath)
|
||||
if err := temporary.Chmod(info.Mode().Perm()); err != nil {
|
||||
_ = temporary.Close()
|
||||
return fmt.Errorf("set temporary host Nginx configuration permissions: %w", err)
|
||||
}
|
||||
if _, err := temporary.Write(content); err != nil {
|
||||
_ = temporary.Close()
|
||||
return fmt.Errorf("write temporary host Nginx configuration: %w", err)
|
||||
}
|
||||
if err := temporary.Sync(); err != nil {
|
||||
_ = temporary.Close()
|
||||
return fmt.Errorf("flush temporary host Nginx configuration: %w", err)
|
||||
}
|
||||
if err := temporary.Close(); err != nil {
|
||||
return fmt.Errorf("close temporary host Nginx configuration: %w", err)
|
||||
}
|
||||
if err := os.Rename(temporaryPath, c.configPath); err != nil {
|
||||
return fmt.Errorf("replace host Nginx configuration: %w", err)
|
||||
}
|
||||
return syncDirectory(parent)
|
||||
}
|
||||
|
||||
type commandRunner interface {
|
||||
Run(context.Context, string, ...string) error
|
||||
}
|
||||
|
||||
type execRunner struct{}
|
||||
|
||||
func (execRunner) Run(ctx context.Context, executable string, arguments ...string) error {
|
||||
output, err := exec.CommandContext(ctx, executable, arguments...).CombinedOutput()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
detail := strings.TrimSpace(string(output))
|
||||
if detail == "" {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("%w: %s", err, detail)
|
||||
}
|
||||
|
||||
func wrapError(message string, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s: %w", message, err)
|
||||
}
|
||||
|
||||
func syncDirectory(directory string) error {
|
||||
file, err := os.Open(directory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open host Nginx configuration directory: %w", err)
|
||||
}
|
||||
syncErr := file.Sync()
|
||||
closeErr := file.Close()
|
||||
if err := errors.Join(syncErr, closeErr); err != nil {
|
||||
return fmt.Errorf("flush host Nginx configuration directory: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package hostnginx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestControllerSwitchesAndReloadsExactService(t *testing.T) {
|
||||
configPath := writeNginxConfig(t, serverConfiguration)
|
||||
runner := &recordingRunner{}
|
||||
controller, err := newController(configPath, "/usr/sbin/nginx", "/bin/systemctl", "nginx.service", runner)
|
||||
if err != nil {
|
||||
t.Fatalf("create host Nginx controller: %v", err)
|
||||
}
|
||||
|
||||
previous, err := controller.Switch(context.Background(), 8080)
|
||||
if err != nil {
|
||||
t.Fatalf("switch host Nginx backend: %v", err)
|
||||
}
|
||||
if previous.ActivePort != 8081 {
|
||||
t.Fatalf("unexpected previous port: %d", previous.ActivePort)
|
||||
}
|
||||
current, err := controller.Read()
|
||||
if err != nil || current.ActivePort != 8080 {
|
||||
t.Fatalf("unexpected current host Nginx configuration: snapshot=%+v err=%v", current, err)
|
||||
}
|
||||
wantCalls := [][]string{
|
||||
{"/usr/sbin/nginx", "-t"},
|
||||
{"/bin/systemctl", "reload", "--", "nginx.service"},
|
||||
}
|
||||
if !slices.EqualFunc(runner.calls, wantCalls, slices.Equal) {
|
||||
t.Fatalf("unexpected host Nginx commands: %+v", runner.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestControllerRestoresConfigurationWhenValidationFails(t *testing.T) {
|
||||
configPath := writeNginxConfig(t, serverConfiguration)
|
||||
runner := &recordingRunner{errors: []error{errors.New("nginx test failed"), nil, nil}}
|
||||
controller, err := newController(configPath, "/usr/sbin/nginx", "/bin/systemctl", "nginx.service", runner)
|
||||
if err != nil {
|
||||
t.Fatalf("create host Nginx controller: %v", err)
|
||||
}
|
||||
|
||||
if _, err := controller.Switch(context.Background(), 8080); err == nil {
|
||||
t.Fatal("expected host Nginx validation failure")
|
||||
}
|
||||
current, err := controller.Read()
|
||||
if err != nil || current.ActivePort != 8081 || string(current.Content) != serverConfiguration {
|
||||
t.Fatalf("host Nginx configuration was not restored: snapshot=%+v err=%v", current, err)
|
||||
}
|
||||
if len(runner.calls) != 3 {
|
||||
t.Fatalf("unexpected validation compensation calls: %+v", runner.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestControllerRestoreUsesCompleteSnapshot(t *testing.T) {
|
||||
configPath := writeNginxConfig(t, serverConfiguration)
|
||||
runner := &recordingRunner{}
|
||||
controller, err := newController(configPath, "/usr/sbin/nginx", "/bin/systemctl", "nginx.service", runner)
|
||||
if err != nil {
|
||||
t.Fatalf("create host Nginx controller: %v", err)
|
||||
}
|
||||
previous, err := controller.Switch(context.Background(), 8080)
|
||||
if err != nil {
|
||||
t.Fatalf("switch host Nginx backend: %v", err)
|
||||
}
|
||||
if err := controller.Restore(context.Background(), previous); err != nil {
|
||||
t.Fatalf("restore host Nginx backend: %v", err)
|
||||
}
|
||||
current, err := controller.Read()
|
||||
if err != nil || current.ActivePort != 8081 || string(current.Content) != serverConfiguration {
|
||||
t.Fatalf("unexpected restored configuration: snapshot=%+v err=%v", current, err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeNginxConfig(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "nginx.conf")
|
||||
if err := os.WriteFile(path, []byte(content), 0o640); err != nil {
|
||||
t.Fatalf("write host Nginx configuration: %v", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
type recordingRunner struct {
|
||||
calls [][]string
|
||||
errors []error
|
||||
}
|
||||
|
||||
func (r *recordingRunner) Run(_ context.Context, executable string, arguments ...string) error {
|
||||
call := append([]string{executable}, arguments...)
|
||||
r.calls = append(r.calls, call)
|
||||
if len(r.errors) == 0 {
|
||||
return nil
|
||||
}
|
||||
err := r.errors[0]
|
||||
r.errors = r.errors[1:]
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user