Compare commits
10 Commits
63b0bbd165
...
78c0d0184e
| Author | SHA1 | Date | |
|---|---|---|---|
|
78c0d0184e
|
|||
|
03ecba3892
|
|||
|
6d4a3f6cce
|
|||
|
e0bf545881
|
|||
|
67fce39d1f
|
|||
|
f86cfec0a7
|
|||
|
d211493e3c
|
|||
|
095a92dfc2
|
|||
|
4e2b2046ae
|
|||
|
b942757ade
|
36
.github/workflows/release.yml
vendored
Normal file
36
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Run tests
|
||||
run: go test ./...
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: "~> v2"
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
30
.goreleaser.yaml
Normal file
30
.goreleaser.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
version: 2
|
||||
|
||||
project_name: pb
|
||||
|
||||
builds:
|
||||
- id: pb
|
||||
main: .
|
||||
binary: pb
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
|
||||
archives:
|
||||
- id: pb
|
||||
builds:
|
||||
- pb
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
|
||||
checksum:
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt"
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Nick Goodall
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
10
README.md
10
README.md
@@ -2,6 +2,12 @@
|
||||
|
||||
A simple and _fast_ PocketBase deployment tool.
|
||||
|
||||
## ⚠️ Fair Warning
|
||||
|
||||
**This is a personal project.** I built it to manage and deploy some side-quests using PocketBase: [Milkstonks](https://milkstonks.com), [ringing.guide](https://ringing.guide), [belfry.world](https://belfry.world) and [waterways.watch](https://waterways.watch).
|
||||
|
||||
Use at your own peril.
|
||||
|
||||
## Commands
|
||||
|
||||
### `init`
|
||||
@@ -10,9 +16,9 @@ Start a new PocketBase project (optionally provide a service name via `pb init <
|
||||
|
||||
```toml
|
||||
[server]
|
||||
ip = '127.0.0.1'
|
||||
port = 8090
|
||||
domain = 'example.com'
|
||||
# ip = '127.0.0.1'
|
||||
# domain = 'example.com'
|
||||
|
||||
[pocketbase]
|
||||
version = '0.35.1'
|
||||
|
||||
4
go.mod
4
go.mod
@@ -4,7 +4,9 @@ go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbletea v0.26.1
|
||||
github.com/mattn/go-runewidth v0.0.15
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
golang.org/x/term v0.19.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -13,7 +15,6 @@ require (
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
@@ -21,6 +22,5 @@ require (
|
||||
github.com/rivo/uniseg v0.4.6 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
golang.org/x/term v0.19.0 // indirect
|
||||
golang.org/x/text v0.3.8 // indirect
|
||||
)
|
||||
|
||||
86
main.go
86
main.go
@@ -250,10 +250,26 @@ func runInit() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ensureGitignoreEntries(filepath.Join(cwd, ".gitignore"), []string{"pocketbase", "pb_data", ".env", ".DS_store"}); err != nil {
|
||||
if err := ensureGitignoreEntries(filepath.Join(cwd, ".gitignore"), []string{"pocketbase", "pb_data", ".env", ".DS_Store"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, dir := range []string{"pb_public", "pb_migrations", "pb_hooks"} {
|
||||
if err := os.MkdirAll(filepath.Join(cwd, dir), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
envPath := filepath.Join(cwd, ".env")
|
||||
if _, err := os.Stat(envPath); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(envPath, []byte("# PocketBase env overrides\n"), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Initialized PocketBase project %q\n", serviceName)
|
||||
return nil
|
||||
}
|
||||
@@ -326,9 +342,9 @@ func init() {
|
||||
|
||||
func writePBConfig(path, serviceName string) error {
|
||||
const tmpl = `[server]
|
||||
ip = "127.0.0.1"
|
||||
port = 8090
|
||||
domain = "example.com"
|
||||
# ip = "127.0.0.1"
|
||||
# domain = "example.com"
|
||||
|
||||
[pocketbase]
|
||||
version = "%s"
|
||||
@@ -369,12 +385,16 @@ func resolveServiceName(defaultName string) (string, error) {
|
||||
}
|
||||
|
||||
func shouldConfirmServerConfig(ctx *deploymentContext) (bool, error) {
|
||||
exists, err := remoteDirExists(ctx.serverIP, ctx.serviceDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: unable to verify remote service at %s: %v\n", ctx.serviceDir, err)
|
||||
if strings.TrimSpace(ctx.serverIP) == "" {
|
||||
return true, nil
|
||||
}
|
||||
return !exists, nil
|
||||
if strings.TrimSpace(ctx.domain) == "" {
|
||||
return true, nil
|
||||
}
|
||||
if ctx.port <= 0 {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func ensureServerConfigConfirmed(ctx *deploymentContext) (*deploymentContext, bool, error) {
|
||||
@@ -389,7 +409,7 @@ func ensureServerConfigConfirmed(ctx *deploymentContext) (*deploymentContext, bo
|
||||
if err := confirmServerConfig(ctx.configPath); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
newCtx, err := buildDeploymentContext()
|
||||
newCtx, err := buildDeploymentContext(true)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -969,7 +989,7 @@ type deploymentContext struct {
|
||||
configPath string
|
||||
}
|
||||
|
||||
func buildDeploymentContext() (*deploymentContext, error) {
|
||||
func buildDeploymentContext(requireServer bool) (*deploymentContext, error) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -987,17 +1007,17 @@ func buildDeploymentContext() (*deploymentContext, error) {
|
||||
}
|
||||
|
||||
serverIP := cfg.Server.IP
|
||||
if serverIP == "" {
|
||||
if requireServer && serverIP == "" {
|
||||
return nil, fmt.Errorf("pb.toml missing [server].ip")
|
||||
}
|
||||
|
||||
domain := cfg.Server.Domain
|
||||
if domain == "" {
|
||||
if requireServer && domain == "" {
|
||||
return nil, fmt.Errorf("pb.toml missing [server].domain")
|
||||
}
|
||||
|
||||
port := cfg.Server.Port
|
||||
if port <= 0 {
|
||||
if requireServer && port <= 0 {
|
||||
return nil, fmt.Errorf("pb.toml server.port must be greater than zero")
|
||||
}
|
||||
|
||||
@@ -1034,7 +1054,7 @@ func buildDeploymentContext() (*deploymentContext, error) {
|
||||
}
|
||||
|
||||
func runSetup() error {
|
||||
ctx, err := buildDeploymentContext()
|
||||
ctx, err := buildDeploymentContext(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1113,14 +1133,22 @@ func performSetup(ctx *deploymentContext, restart bool) error {
|
||||
}
|
||||
|
||||
func runDeploy() error {
|
||||
ctx, err := buildDeploymentContext()
|
||||
ctx, err := buildDeploymentContext(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
start := time.Now()
|
||||
|
||||
ctx, prompted, err := ensureServerConfigConfirmed(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if prompted {
|
||||
start = time.Now()
|
||||
}
|
||||
defer func() {
|
||||
closeSSHControlMaster(ctx.serverIP)
|
||||
}()
|
||||
start := time.Now()
|
||||
|
||||
binaryPath := filepath.Join(ctx.serviceDir, "pocketbase")
|
||||
exists, err := remoteBinaryExists(ctx.serverIP, binaryPath)
|
||||
@@ -1130,13 +1158,6 @@ func runDeploy() error {
|
||||
|
||||
if err != nil || !exists {
|
||||
fmt.Println("PocketBase binary missing on remote; running setup")
|
||||
ctx, prompted, err := ensureServerConfigConfirmed(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if prompted {
|
||||
start = time.Now()
|
||||
}
|
||||
if err := performSetup(ctx, false); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1161,16 +1182,16 @@ func runDeploy() error {
|
||||
}
|
||||
|
||||
func runStatus() error {
|
||||
ctx, err := buildDeploymentContext()
|
||||
ctx, err := buildDeploymentContext(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer closeSSHControlMaster(ctx.serverIP)
|
||||
|
||||
printInfo("Service", fmt.Sprintf("pb@%s", ctx.serviceName), remoteLineColor)
|
||||
printInfo("Server", ctx.serverIP, remoteLineColor)
|
||||
printInfo("Domain", ctx.domain, remoteLineColor)
|
||||
printInfo("Port", fmt.Sprintf("%d", ctx.port), remoteLineColor)
|
||||
printInfo("Service", fmt.Sprintf("pb@%s", ctx.serviceName), "")
|
||||
printInfo("Server", ctx.serverIP, "")
|
||||
printInfo("Domain", ctx.domain, "")
|
||||
printInfo("Port", fmt.Sprintf("%d", ctx.port), "")
|
||||
|
||||
props, err := querySystemdProperties(ctx)
|
||||
if err != nil {
|
||||
@@ -1188,7 +1209,7 @@ func runStatus() error {
|
||||
printInfo("Status", statusLine, statusColorFor(state))
|
||||
|
||||
if pid := strings.TrimSpace(props["ExecMainPID"]); pid != "" && pid != "0" {
|
||||
printInfo("PID", pid, remoteLineColor)
|
||||
printInfo("PID", pid, "")
|
||||
}
|
||||
|
||||
if memCurrent, ok := parseSystemdBytes(props["MemoryCurrent"]); ok {
|
||||
@@ -1196,7 +1217,7 @@ func runStatus() error {
|
||||
if memPeak, ok := parseSystemdBytes(props["MemoryPeak"]); ok {
|
||||
memLine = fmt.Sprintf("%s (peak: %s)", memLine, formatBytes(memPeak))
|
||||
}
|
||||
printInfo("Memory", memLine, remoteLineColor)
|
||||
printInfo("Memory", memLine, "")
|
||||
}
|
||||
|
||||
if started := strings.TrimSpace(props["ActiveEnterTimestamp"]); started != "" {
|
||||
@@ -1211,9 +1232,6 @@ func runStatus() error {
|
||||
}
|
||||
|
||||
func printInfo(label, value, valueColor string) {
|
||||
if valueColor == "" {
|
||||
valueColor = remoteLineColor
|
||||
}
|
||||
fmt.Printf("%s%s:%s %s%s%s\n", headerColor, label, remoteColorReset, valueColor, value, remoteColorReset)
|
||||
}
|
||||
|
||||
@@ -1326,7 +1344,7 @@ func formatDuration(d time.Duration) string {
|
||||
}
|
||||
|
||||
func runLogs() error {
|
||||
ctx, err := buildDeploymentContext()
|
||||
ctx, err := buildDeploymentContext(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1349,7 +1367,7 @@ func runSecrets() error {
|
||||
return fmt.Errorf("usage: pb secrets <list|set|delete> [arguments]")
|
||||
}
|
||||
|
||||
ctx, err := buildDeploymentContext()
|
||||
ctx, err := buildDeploymentContext(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
296
main_test.go
Normal file
296
main_test.go
Normal file
@@ -0,0 +1,296 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseEnvLine(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
line string
|
||||
wantKey string
|
||||
wantValue string
|
||||
wantOK bool
|
||||
}{
|
||||
{name: "simple", line: "FOO=bar", wantKey: "FOO", wantValue: "bar", wantOK: true},
|
||||
{name: "trims", line: " FOO = bar ", wantKey: "FOO", wantValue: "bar", wantOK: true},
|
||||
{name: "comment", line: "# comment", wantOK: false},
|
||||
{name: "empty", line: "", wantOK: false},
|
||||
{name: "no equals", line: "FOO", wantOK: false},
|
||||
{name: "invalid key digit", line: "1FOO=bar", wantOK: false},
|
||||
{name: "invalid key dash", line: "FOO-BAR=1", wantOK: false},
|
||||
{name: "underscore ok", line: "_FOO=bar", wantKey: "_FOO", wantValue: "bar", wantOK: true},
|
||||
{name: "digit ok after", line: "FOO1=2", wantKey: "FOO1", wantValue: "2", wantOK: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
key, value, ok := parseEnvLine(tt.line)
|
||||
if ok != tt.wantOK {
|
||||
t.Fatalf("ok=%v want %v", ok, tt.wantOK)
|
||||
}
|
||||
if key != tt.wantKey || value != tt.wantValue {
|
||||
t.Fatalf("got key=%q value=%q want key=%q value=%q", key, value, tt.wantKey, tt.wantValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsEnvKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
key string
|
||||
want bool
|
||||
}{
|
||||
{key: "A", want: true},
|
||||
{key: "_A", want: true},
|
||||
{key: "A1", want: true},
|
||||
{key: "A_B2", want: true},
|
||||
{key: "", want: false},
|
||||
{key: "1A", want: false},
|
||||
{key: "A-B", want: false},
|
||||
{key: "A B", want: false},
|
||||
{key: "A.B", want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.key, func(t *testing.T) {
|
||||
if got := isEnvKey(tt.key); got != tt.want {
|
||||
t.Fatalf("got %v want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsEnvLine(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
line string
|
||||
want bool
|
||||
}{
|
||||
{line: "", want: true},
|
||||
{line: " ", want: true},
|
||||
{line: "FOO=bar", want: true},
|
||||
{line: "FOO = bar", want: false},
|
||||
{line: "1FOO=bar", want: false},
|
||||
{line: "NO_EQUALS", want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.line, func(t *testing.T) {
|
||||
if got := isEnvLine(tt.line); got != tt.want {
|
||||
t.Fatalf("got %v want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvAssignments(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
lines := []string{
|
||||
"# comment",
|
||||
"FOO=old",
|
||||
"BAR=keep",
|
||||
"INVALID LINE",
|
||||
"BAZ=gone",
|
||||
"FOO=duplicate",
|
||||
}
|
||||
assignments := []envAssignment{
|
||||
{key: "FOO", value: "new"},
|
||||
{key: "BAZ", value: "zzz"},
|
||||
{key: "NEWKEY", value: "123"},
|
||||
}
|
||||
want := []string{
|
||||
"# comment",
|
||||
"FOO=new",
|
||||
"BAR=keep",
|
||||
"INVALID LINE",
|
||||
"BAZ=zzz",
|
||||
"NEWKEY=123",
|
||||
}
|
||||
|
||||
got := applyEnvAssignments(lines, assignments)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %#v want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveEnvKeys(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
lines := []string{
|
||||
"KEEP=1",
|
||||
"DROP=2",
|
||||
"# comment",
|
||||
"INVALID LINE",
|
||||
"DROP=3",
|
||||
"ALSO=4",
|
||||
}
|
||||
keys := []string{"DROP", "MISSING"}
|
||||
want := []string{
|
||||
"KEEP=1",
|
||||
"# comment",
|
||||
"INVALID LINE",
|
||||
"ALSO=4",
|
||||
}
|
||||
|
||||
got := removeEnvKeys(lines, keys)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %#v want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellQuote(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{name: "empty", input: "", want: "''"},
|
||||
{name: "simple", input: "abc", want: "'abc'"},
|
||||
{name: "spaces", input: "a b", want: "'a b'"},
|
||||
{name: "single quote", input: "a'b", want: "'a'\"'\"'b'"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := shellQuote(tt.input); got != tt.want {
|
||||
t.Fatalf("got %q want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveVolumePath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
template string
|
||||
baseDir string
|
||||
service string
|
||||
wantResult string
|
||||
}{
|
||||
{name: "empty uses base", template: "", baseDir: "/srv/pb", service: "svc", wantResult: "/srv/pb"},
|
||||
{name: "absolute path", template: "/data/{service}", baseDir: "/srv/pb", service: "svc", wantResult: "/data/svc"},
|
||||
{name: "relative path", template: "data/{service}", baseDir: "/srv/pb", service: "svc", wantResult: "/srv/pb/data/svc"},
|
||||
{name: "simple relative", template: "custom", baseDir: "/srv/pb", service: "svc", wantResult: "/srv/pb/custom"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := resolveVolumePath(tt.template, tt.baseDir, tt.service); got != tt.wantResult {
|
||||
t.Fatalf("got %q want %q", got, tt.wantResult)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslateMachineArch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "x86_64", input: " x86_64 ", want: "amd64"},
|
||||
{name: "amd64", input: "amd64", want: "amd64"},
|
||||
{name: "i686", input: "i686", want: "386"},
|
||||
{name: "armv7l", input: "armv7l", want: "armv7"},
|
||||
{name: "armv6l", input: "armv6l", want: "arm"},
|
||||
{name: "aarch64", input: "aarch64", want: "arm64"},
|
||||
{name: "arm64", input: "arm64", want: "arm64"},
|
||||
{name: "unknown", input: "mips", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := translateMachineArch(tt.input)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("got %q want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemdScript(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
serviceDir := "/srv/pb"
|
||||
envFile := "/etc/pb/env"
|
||||
serviceName := "demo"
|
||||
script := systemdScript(serviceDir, envFile, serviceName)
|
||||
|
||||
required := []string{
|
||||
"/etc/systemd/system/pb@.service",
|
||||
"StandardOutput = append:" + serviceDir + "/%i.log",
|
||||
"StandardError = append:" + serviceDir + "/%i.log",
|
||||
"WorkingDirectory = " + serviceDir,
|
||||
"EnvironmentFile = " + envFile,
|
||||
"ExecStart = " + serviceDir + "/pocketbase serve --http=127.0.0.1:${PORT} --hooksWatch=false --dir=${DATA_DIR} --hooksDir=" + serviceDir + "/pb_hooks --migrationsDir=" + serviceDir + "/pb_migrations --publicDir=" + serviceDir + "/pb_public",
|
||||
"systemctl --no-block enable pb@" + serviceName,
|
||||
}
|
||||
|
||||
for _, snippet := range required {
|
||||
if !strings.Contains(script, snippet) {
|
||||
t.Fatalf("script missing %q", snippet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemdOverrideScript(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
serviceName := "demo"
|
||||
port := 8090
|
||||
volume := "/srv/pb/data"
|
||||
script := systemdOverrideScript(serviceName, port, volume)
|
||||
|
||||
required := []string{
|
||||
"/etc/systemd/system/pb@" + serviceName + ".service.d",
|
||||
"Environment=PORT=8090",
|
||||
"Environment=DATA_DIR=" + volume,
|
||||
}
|
||||
for _, snippet := range required {
|
||||
if !strings.Contains(script, snippet) {
|
||||
t.Fatalf("script missing %q", snippet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemdRestartScript(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
serviceName := "demo"
|
||||
script := systemdRestartScript(serviceName)
|
||||
expected := "systemctl --no-block restart pb@" + serviceName
|
||||
if !strings.Contains(script, expected) {
|
||||
t.Fatalf("script missing %q", expected)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user