This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
name: build-and-release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # svu needs full history + tags
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.26'
|
||||
|
||||
- name: Install svu
|
||||
run: go install github.com/caarlos0/svu@latest
|
||||
|
||||
- name: Compute next version
|
||||
id: ver
|
||||
run: |
|
||||
CURRENT=$(svu current 2>/dev/null || echo v0.0.0)
|
||||
NEXT=$(svu next)
|
||||
echo "current=$CURRENT" >> $GITHUB_OUTPUT
|
||||
echo "next=$NEXT" >> $GITHUB_OUTPUT
|
||||
if [ "$CURRENT" = "$NEXT" ]; then
|
||||
echo "release=false" >> $GITHUB_OUTPUT
|
||||
echo "No version-worthy commits since $CURRENT; skipping release."
|
||||
else
|
||||
echo "release=true" >> $GITHUB_OUTPUT
|
||||
echo "Releasing $NEXT (was $CURRENT)."
|
||||
fi
|
||||
|
||||
- name: Install UPX
|
||||
if: steps.ver.outputs.release == 'true'
|
||||
run: sudo apt-get update && sudo apt-get install -y upx-ucl
|
||||
|
||||
- name: Build binaries
|
||||
if: steps.ver.outputs.release == 'true'
|
||||
env:
|
||||
VERSION: ${{ steps.ver.outputs.next }}
|
||||
LDFLAGS: -s -w -X nadir.Version=${{ steps.ver.outputs.next }}
|
||||
run: |
|
||||
mkdir -p dist
|
||||
GOOS=linux GOARCH=amd64 go build -ldflags="$LDFLAGS" -o dist/nadir-$VERSION-linux-amd64 ./cmd/server
|
||||
GOOS=linux GOARCH=arm64 go build -ldflags="$LDFLAGS" -o dist/nadir-$VERSION-linux-arm64 ./cmd/server
|
||||
upx --best --lzma dist/nadir-$VERSION-linux-amd64 dist/nadir-$VERSION-linux-arm64
|
||||
|
||||
- name: Tag and release
|
||||
if: steps.ver.outputs.release == 'true'
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEATOKEN }}
|
||||
VERSION: ${{ steps.ver.outputs.next }}
|
||||
run: |
|
||||
git config user.email "ci@nadir"
|
||||
git config user.name "nadir-ci"
|
||||
git tag -a "$VERSION" -m "$VERSION"
|
||||
git push "https://x:${GITEA_TOKEN}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git" "$VERSION"
|
||||
# Create the release via Gitea API (works on any Gitea ≥ 1.20)
|
||||
curl -sSf -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"$VERSION\",\"name\":\"$VERSION\"}" \
|
||||
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases" > release.json
|
||||
RELEASE_ID=$(grep -o '"id":[0-9]*' release.json | head -1 | cut -d: -f2)
|
||||
for f in dist/*; do
|
||||
curl -sSf -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-F "attachment=@$f" \
|
||||
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/$RELEASE_ID/assets?name=$(basename $f)"
|
||||
done
|
||||
@@ -24,5 +24,7 @@ var Favicon string
|
||||
//go:embed install.sh.tmpl
|
||||
var InstallScriptTemplate string
|
||||
|
||||
// Version is the current version of the Nadir application.
|
||||
const Version = "0.0.1"
|
||||
// Version is set by the linker at build time:
|
||||
// go build -ldflags "-X nadir.Version=v1.2.3"
|
||||
// Local dev builds leave it as "dev".
|
||||
var Version = "dev"
|
||||
|
||||
+38
-16
@@ -2,30 +2,29 @@
|
||||
set -e
|
||||
# Nadir bootstrap installer.
|
||||
#
|
||||
# Downloads the nadir binary from the SAME instance that served this script
|
||||
# and installs it as a systemd service. Intended for spinning up nadir on a
|
||||
# sibling host quickly (same architecture as the source instance):
|
||||
# Detects the host architecture, fetches the latest release asset from the
|
||||
# Gitea releases API, and installs it as a systemd service:
|
||||
#
|
||||
# curl -fsSL https://<existing-nadir-host>:<port>/install.sh | sudo sh
|
||||
#
|
||||
# This is not a general-purpose distribution channel: there is no separate
|
||||
# release server, no signature, and no version pinning beyond "whatever the
|
||||
# source instance is currently running." Trust it exactly as much as you
|
||||
# trust the source host.
|
||||
# The binary always comes from the Gitea release (not the source instance),
|
||||
# so the installed copy is guaranteed to be the latest tagged build,
|
||||
# independent of whatever version the serving host happens to be running.
|
||||
#
|
||||
# Everything is wrapped in do_install and invoked on the last line, the same
|
||||
# pattern get.docker.com uses: when piped via `curl | sh`, sh executes while
|
||||
# still receiving bytes from the network. Defining the whole body as a
|
||||
# function before running anything means sh has fully parsed the script
|
||||
# before do_install ever runs, which avoids exactly the failure mode where a
|
||||
# command further down (here, the binary download) races the still-open
|
||||
# script-fetch connection and fails with a write error.
|
||||
# before do_install ever runs.
|
||||
|
||||
BASE_URL="__NADIR_BASE_URL__"
|
||||
# Where to look for releases. Substituted at request time by the server
|
||||
# (cmd/server/server.go) so the script knows which Gitea instance owns
|
||||
# this deployment. Example: https://gitea.example.com/urania/nadir
|
||||
RELEASE_REPO="__NADIR_RELEASE_REPO__"
|
||||
|
||||
do_install() {
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "this script must be run as root (try: curl -fsSL $BASE_URL/install.sh | sudo sh)" >&2
|
||||
echo "this script must be run as root (try piping through 'sudo sh')" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -34,10 +33,33 @@ do_install() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "downloading nadir from $BASE_URL ..."
|
||||
# No -s here: the progress meter is the whole point of leaving it visible,
|
||||
# so you can see how much is left mid-download.
|
||||
curl -f --progress-bar -L "$BASE_URL/nadir-binary" -o /usr/local/bin/nadir.tmp
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) arch=amd64 ;;
|
||||
aarch64|arm64) arch=arm64 ;;
|
||||
*) echo "unsupported architecture: $(uname -m) (only amd64 and arm64 are built)" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
api="$RELEASE_REPO/releases/latest"
|
||||
api="${api%/}"
|
||||
# Convert https://host/owner/repo/releases/latest into Gitea API form.
|
||||
# Splits the URL once on '/' between host and path, inserts /api/v1/repos.
|
||||
host=$(echo "$RELEASE_REPO" | awk -F/ '{print $1"//"$3}')
|
||||
path=$(echo "$RELEASE_REPO" | awk -F/ '{print $4"/"$5}')
|
||||
api="$host/api/v1/repos/$path/releases/latest"
|
||||
|
||||
echo "querying $api ..."
|
||||
asset_url=$(curl -fsSL "$api" \
|
||||
| grep -o '"browser_download_url":"[^"]*linux-'"$arch"'"' \
|
||||
| head -n1 \
|
||||
| cut -d'"' -f4)
|
||||
|
||||
if [ -z "$asset_url" ]; then
|
||||
echo "no linux-$arch asset found in the latest release at $api" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "downloading $asset_url ..."
|
||||
curl -f --progress-bar -L "$asset_url" -o /usr/local/bin/nadir.tmp
|
||||
mv /usr/local/bin/nadir.tmp /usr/local/bin/nadir
|
||||
chmod +x /usr/local/bin/nadir
|
||||
|
||||
|
||||
@@ -40,9 +40,8 @@ func (a *TokenAuth) Verify(ip, raw string) (name string, ok, throttled bool) {
|
||||
// reporting whether it was a Bearer scheme. Returns ("", false) for cookie-only
|
||||
// or unauthenticated requests.
|
||||
func BearerToken(authHeader string) (string, bool) {
|
||||
const scheme = "Bearer "
|
||||
if len(authHeader) <= len(scheme) || !strings.EqualFold(authHeader[:len(scheme)], scheme) {
|
||||
if len(authHeader) <= 7 || !strings.EqualFold(authHeader[:7], "Bearer ") {
|
||||
return "", false
|
||||
}
|
||||
return strings.TrimSpace(authHeader[len(scheme):]), true
|
||||
return strings.TrimSpace(authHeader[7:]), true
|
||||
}
|
||||
|
||||
@@ -10,20 +10,12 @@ import (
|
||||
"time"
|
||||
|
||||
"nadir/internal/auditlog"
|
||||
"nadir/internal/oscmd"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/danielgtaylor/huma/v2/adapters/humago"
|
||||
"github.com/danielgtaylor/huma/v2/humatest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if oscmd.RunHelperProcess() {
|
||||
return
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestEnsurePAMService(t *testing.T) {
|
||||
tempFile := filepath.Join(t.TempDir(), "nadir-pam-test")
|
||||
oldPath := pamServicePath
|
||||
|
||||
@@ -89,11 +89,6 @@ func (s *SessionStore) GetByToken(token string) (Session, bool) {
|
||||
|
||||
func randomToken() string {
|
||||
b := make([]byte, 32)
|
||||
// crypto/rand.Read never returns an error on supported platforms; if it
|
||||
// somehow does, an all-zero (guessable) token would be a security hole, so
|
||||
// fail hard rather than hand one out.
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic("crypto/rand failed: " + err.Error())
|
||||
}
|
||||
rand.Read(b) // never fails; rand.Read panics internally on misconfigured platforms.
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
@@ -60,6 +60,11 @@ type Server struct {
|
||||
TrustProxy bool `yaml:"trust_proxy"`
|
||||
TLSCert string `yaml:"tls_cert"`
|
||||
TLSKey string `yaml:"tls_key"`
|
||||
|
||||
// ReleaseRepo is the Gitea repo URL used by /install.sh to fetch the
|
||||
// latest binary. Example: https://gitea.example.com/urania/nadir.
|
||||
// Empty disables the install.sh endpoint.
|
||||
ReleaseRepo string `yaml:"release_repo"`
|
||||
}
|
||||
|
||||
// SecureCookie reports whether the session cookie should carry the Secure
|
||||
|
||||
@@ -20,7 +20,6 @@ type fakeModule struct {
|
||||
}
|
||||
|
||||
func (f fakeModule) ID() string { return f.id }
|
||||
func (f fakeModule) Name() string { return f.id }
|
||||
func (f fakeModule) Permissions() []rbac.Permission { return f.perms }
|
||||
func (f fakeModule) Register(huma.API) {}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ func Register(api huma.API, mods []module.Module) {
|
||||
for i, p := range perms {
|
||||
ps[i] = string(p)
|
||||
}
|
||||
infos = append(infos, ModuleInfo{ID: m.ID(), Name: m.Name(), Permissions: ps})
|
||||
infos = append(infos, ModuleInfo{ID: m.ID(), Name: module.Title(m.ID()), Permissions: ps})
|
||||
}
|
||||
slices.SortFunc(infos, func(a, b ModuleInfo) int { return cmp.Compare(a.ID, b.ID) })
|
||||
|
||||
|
||||
@@ -8,7 +8,19 @@ import (
|
||||
|
||||
type Module interface {
|
||||
ID() string
|
||||
Name() string
|
||||
Permissions() []rbac.Permission // permissions this module exposes (no "*")
|
||||
Register(api huma.API)
|
||||
}
|
||||
|
||||
// Title returns the display name for a module ID, capitalizing the first
|
||||
// letter (modules are single lowercase words: "system" -> "System").
|
||||
func Title(id string) string {
|
||||
if id == "" {
|
||||
return ""
|
||||
}
|
||||
b := id[0]
|
||||
if b >= 'a' && b <= 'z' {
|
||||
b -= 'a' - 'A'
|
||||
}
|
||||
return string(b) + id[1:]
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ type Module struct {
|
||||
func New(store *auditlog.Store) *Module { return &Module{store: store} }
|
||||
|
||||
func (m *Module) ID() string { return ModuleID }
|
||||
func (m *Module) Name() string { return "Audit" }
|
||||
|
||||
// Permissions: read to view the audit trail. There is no write - entries are
|
||||
// produced by the middleware, never by an API call.
|
||||
|
||||
@@ -15,13 +15,6 @@ import (
|
||||
"github.com/danielgtaylor/huma/v2/humatest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if oscmd.RunHelperProcess() {
|
||||
return
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestGroupsHandlers(t *testing.T) {
|
||||
tempGroup := filepath.Join(t.TempDir(), "group")
|
||||
initialContent := "root:x:0:\nwheel:x:10:alice,bob\n"
|
||||
|
||||
@@ -13,7 +13,6 @@ type Module struct{}
|
||||
func New() *Module { return &Module{} }
|
||||
|
||||
func (m *Module) ID() string { return ModuleID }
|
||||
func (m *Module) Name() string { return "Groups" }
|
||||
|
||||
// Permissions: read to list/inspect groups; write to create; root to delete
|
||||
// (irreversible). Group membership lives in the users module.
|
||||
|
||||
@@ -24,7 +24,6 @@ type Module struct {
|
||||
func New() *Module { return &Module{be: detect()} }
|
||||
|
||||
func (m *Module) ID() string { return ModuleID }
|
||||
func (m *Module) Name() string { return "Networking" }
|
||||
|
||||
// Permissions: read to inspect interfaces/routes/DNS; write to reconfigure them
|
||||
// (apply config, bring links up/down, confirm a pending change).
|
||||
|
||||
@@ -19,13 +19,6 @@ import (
|
||||
"github.com/danielgtaylor/huma/v2/humatest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if oscmd.RunHelperProcess() {
|
||||
return
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
type mockBackend struct {
|
||||
name string
|
||||
snapshotResult IfaceConfig
|
||||
|
||||
@@ -16,7 +16,6 @@ type Module struct {
|
||||
func New() *Module { return &Module{pm: detect()} }
|
||||
|
||||
func (m *Module) ID() string { return ModuleID }
|
||||
func (m *Module) Name() string { return "Packages" }
|
||||
|
||||
// Permissions: read to list installed/available; write to install, remove, and
|
||||
// upgrade.
|
||||
|
||||
@@ -3,7 +3,6 @@ package packages
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -15,13 +14,6 @@ import (
|
||||
"github.com/danielgtaylor/huma/v2/humatest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if oscmd.RunHelperProcess() {
|
||||
return
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestPackagesHandlers(t *testing.T) {
|
||||
managers := []string{"dnf", "apt", "pacman"}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ type Module struct {
|
||||
func New(logFiles map[string][]string) *Module { return &Module{logFiles: logFiles} }
|
||||
|
||||
func (m *Module) ID() string { return ModuleID }
|
||||
func (m *Module) Name() string { return "Services" }
|
||||
|
||||
// Permissions: read to list and inspect units; write to control them
|
||||
// (start/stop/restart/enable/disable).
|
||||
|
||||
@@ -16,13 +16,6 @@ import (
|
||||
"github.com/danielgtaylor/huma/v2/humatest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if oscmd.RunHelperProcess() {
|
||||
return
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestServicesHandlers(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
api := humatest.Wrap(t, humago.New(mux, huma.DefaultConfig("Test", "1.0.0")))
|
||||
|
||||
@@ -13,7 +13,6 @@ type Module struct{}
|
||||
func New() *Module { return &Module{} }
|
||||
|
||||
func (m *Module) ID() string { return ModuleID }
|
||||
func (m *Module) Name() string { return "Storage" }
|
||||
|
||||
// Permissions: read to list mounts and fstab; write to mount/unmount and edit
|
||||
// fstab entries.
|
||||
|
||||
@@ -6,17 +6,8 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"nadir/internal/oscmd"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if oscmd.RunHelperProcess() {
|
||||
return
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestParseFstab(t *testing.T) {
|
||||
data := `# /etc/fstab
|
||||
UUID=1111-2222 / ext4 defaults 0 1
|
||||
|
||||
@@ -13,7 +13,6 @@ type Module struct{}
|
||||
func New() *Module { return &Module{} }
|
||||
|
||||
func (m *Module) ID() string { return ModuleID }
|
||||
func (m *Module) Name() string { return "System" }
|
||||
|
||||
func (m *Module) Permissions() []rbac.Permission {
|
||||
return []rbac.Permission{rbac.Read, rbac.Write, rbac.Root}
|
||||
|
||||
@@ -3,7 +3,6 @@ package system
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
@@ -14,13 +13,6 @@ import (
|
||||
"github.com/danielgtaylor/huma/v2/humatest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if oscmd.RunHelperProcess() {
|
||||
return
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestSystemHandlers(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
api := humatest.Wrap(t, humago.New(mux, huma.DefaultConfig("Test", "1.0.0")))
|
||||
|
||||
@@ -46,7 +46,6 @@ func New(sessions *auth.SessionStore) module.Module {
|
||||
}
|
||||
|
||||
func (m *terminalModule) ID() string { return "terminal" }
|
||||
func (m *terminalModule) Name() string { return "Terminal" }
|
||||
|
||||
func (m *terminalModule) Permissions() []rbac.Permission {
|
||||
return []rbac.Permission{rbac.Root}
|
||||
|
||||
@@ -13,7 +13,6 @@ type Module struct{}
|
||||
func New() *Module { return &Module{} }
|
||||
|
||||
func (m *Module) ID() string { return ModuleID }
|
||||
func (m *Module) Name() string { return "Users" }
|
||||
|
||||
// Permissions: read to list/inspect accounts; write to create and change
|
||||
// passwords; root to delete (irreversible).
|
||||
|
||||
@@ -15,13 +15,6 @@ import (
|
||||
"github.com/danielgtaylor/huma/v2/humatest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if oscmd.RunHelperProcess() {
|
||||
return
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestUsersHandlers(t *testing.T) {
|
||||
tempPasswd := filepath.Join(t.TempDir(), "passwd")
|
||||
initialContent := "root:x:0:0:root:/root:/bin/bash\nalice:x:1000:1000:Alice Smith:/home/alice:/bin/bash\n"
|
||||
|
||||
+44
-74
@@ -7,7 +7,6 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -28,37 +27,51 @@ import (
|
||||
const cmdTimeout = 60 * time.Second
|
||||
|
||||
// CommandRunner allows overriding the command execution function for testing.
|
||||
// In tests, register a handler with SetMock; the runner translates the
|
||||
// MockCommand into a /bin/sh script that reproduces the stdout/stderr/exit
|
||||
// behavior — no helper process, no temp file.
|
||||
var CommandRunner = func(ctx context.Context, name string, args ...string) *exec.Cmd {
|
||||
mockMu.Lock()
|
||||
handler, ok := mockCmds[name]
|
||||
mockMu.Unlock()
|
||||
|
||||
if ok {
|
||||
mockRes := handler(args)
|
||||
tmpFile, err := os.CreateTemp("", "nadir-mock-*.json")
|
||||
if err != nil {
|
||||
log.Printf("oscmd mock: failed to create temp file: %v", err)
|
||||
return exec.CommandContext(ctx, name, args...)
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(tmpFile)
|
||||
if err := encoder.Encode(mockRes); err != nil {
|
||||
log.Printf("oscmd mock: failed to write json: %v", err)
|
||||
tmpFile.Close()
|
||||
return exec.CommandContext(ctx, name, args...)
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
|
||||
cmd := exec.CommandContext(ctx, os.Args[0])
|
||||
cmd.Env = append(os.Environ(),
|
||||
"GO_WANT_HELPER_PROCESS=1",
|
||||
"NADIR_MOCK_FILE="+tmpPath,
|
||||
)
|
||||
return cmd
|
||||
if !ok {
|
||||
return exec.CommandContext(ctx, name, args...)
|
||||
}
|
||||
return exec.CommandContext(ctx, "/bin/sh", "-c", mockScript(handler(args)))
|
||||
}
|
||||
|
||||
return exec.CommandContext(ctx, name, args...)
|
||||
// mockScript builds the shell script that emits a MockCommand's behavior.
|
||||
// Uses printf so backslashes and percent signs in output pass through verbatim.
|
||||
func mockScript(m MockCommand) string {
|
||||
var b strings.Builder
|
||||
if len(m.Lines) > 0 {
|
||||
for _, line := range m.Lines {
|
||||
b.WriteString("printf '%s\\n' ")
|
||||
b.WriteString(shellQuote(line))
|
||||
b.WriteByte('\n')
|
||||
if m.DelayMs > 0 {
|
||||
fmt.Fprintf(&b, "sleep %g\n", float64(m.DelayMs)/1000)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if m.Stdout != "" {
|
||||
b.WriteString("printf '%s' ")
|
||||
b.WriteString(shellQuote(m.Stdout))
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
if m.Stderr != "" {
|
||||
b.WriteString("printf '%s' ")
|
||||
b.WriteString(shellQuote(m.Stderr))
|
||||
b.WriteString(" 1>&2\n")
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&b, "exit %d\n", m.ExitCode)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// shellQuote returns s wrapped in single quotes, with embedded single quotes escaped.
|
||||
func shellQuote(s string) string {
|
||||
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
|
||||
}
|
||||
|
||||
// Run executes name with args and returns trimmed stdout. On failure it wraps
|
||||
@@ -283,11 +296,11 @@ func OK() *StatusOutput {
|
||||
|
||||
// MockCommand holds the behavior for a mocked command.
|
||||
type MockCommand struct {
|
||||
Stdout string `json:"stdout"`
|
||||
Stderr string `json:"stderr"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
Lines []string `json:"lines,omitempty"`
|
||||
DelayMs int `json:"delay_ms,omitempty"`
|
||||
Stdout string
|
||||
Stderr string
|
||||
ExitCode int
|
||||
Lines []string
|
||||
DelayMs int
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -309,46 +322,3 @@ func ClearMocks() {
|
||||
clear(mockCmds)
|
||||
}
|
||||
|
||||
// RunHelperProcess executes the mock helper process logic if GO_WANT_HELPER_PROCESS is set.
|
||||
// It returns true if it ran (and exits the process), false otherwise.
|
||||
func RunHelperProcess() bool {
|
||||
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
|
||||
return false
|
||||
}
|
||||
mockFile := os.Getenv("NADIR_MOCK_FILE")
|
||||
if mockFile == "" {
|
||||
os.Exit(1)
|
||||
}
|
||||
defer os.Remove(mockFile)
|
||||
|
||||
data, err := os.ReadFile(mockFile)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "mock helper: read failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var mock MockCommand
|
||||
if err := json.Unmarshal(data, &mock); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "mock helper: unmarshal failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(mock.Lines) > 0 {
|
||||
for _, line := range mock.Lines {
|
||||
fmt.Fprintln(os.Stdout, line)
|
||||
if mock.DelayMs > 0 {
|
||||
time.Sleep(time.Duration(mock.DelayMs) * time.Millisecond)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if mock.Stdout != "" {
|
||||
fmt.Fprint(os.Stdout, mock.Stdout)
|
||||
}
|
||||
if mock.Stderr != "" {
|
||||
fmt.Fprint(os.Stderr, mock.Stderr)
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(mock.ExitCode)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
package oscmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if RunHelperProcess() {
|
||||
return
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestRunTrimsStdout(t *testing.T) {
|
||||
out, err := Run("echo", "hello")
|
||||
if err != nil {
|
||||
|
||||
@@ -3,26 +3,17 @@ package rbac
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"nadir/internal/auditlog"
|
||||
"nadir/internal/auth"
|
||||
"nadir/internal/oscmd"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/danielgtaylor/huma/v2/adapters/humago"
|
||||
"github.com/danielgtaylor/huma/v2/humatest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if oscmd.RunHelperProcess() {
|
||||
return
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestRbacMiddleware(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
auditStore, err := auditlog.New(filepath.Join(tempDir, "audit.db"))
|
||||
|
||||
Reference in New Issue
Block a user