Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.25.0

require (
github.com/BurntSushi/toml v1.6.0
github.com/caarlos0/env/v11 v11.4.0
github.com/deevus/truenas-go v0.4.0
github.com/spf13/cobra v1.10.2
)
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeX
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/caarlos0/env/v11 v11.4.0 h1:Kcb6t5kIIr4XkoQC9AF2j+8E1Jsrl3Wz/hhm1LtoGAc=
github.com/caarlos0/env/v11 v11.4.0/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/deevus/truenas-go v0.3.0 h1:IuJ7wm+aW0aSmm4AjW8GK6f+8bjKolj9BVd9bTfKeSU=
github.com/deevus/truenas-go v0.3.0/go.mod h1:a5MwZEqT4NE8jwSA9BHONOAO8yH4kCaS5a+d4ad6sLA=
github.com/deevus/truenas-go v0.4.0 h1:gESJ0naqtwzgdN1/gG5wBrp/Lm/5HF8xCBsRcwNgg78=
github.com/deevus/truenas-go v0.4.0/go.mod h1:a5MwZEqT4NE8jwSA9BHONOAO8yH4kCaS5a+d4ad6sLA=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
Expand Down
85 changes: 21 additions & 64 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/BurntSushi/toml"
"github.com/caarlos0/env/v11"
)

type Config struct {
Expand All @@ -21,36 +21,36 @@ type Config struct {
}

type TrueNAS struct {
Host string `toml:"host"`
Port int `toml:"port"`
Username string `toml:"username"`
APIKey string `toml:"api_key"`
InsecureSkipVerify *bool `toml:"insecure_skip_verify"`
Host string `toml:"host" env:"PIXELS_TRUENAS_HOST"`
Port int `toml:"port" env:"PIXELS_TRUENAS_PORT"`
Username string `toml:"username" env:"PIXELS_TRUENAS_USERNAME"`
APIKey string `toml:"api_key" env:"PIXELS_TRUENAS_API_KEY"`
InsecureSkipVerify *bool `toml:"insecure_skip_verify" env:"PIXELS_TRUENAS_INSECURE"`
}

type Defaults struct {
Image string `toml:"image"`
CPU string `toml:"cpu"`
Memory int64 `toml:"memory"` // MiB
Pool string `toml:"pool"`
NICType string `toml:"nic_type"` // "macvlan" or "bridged"
Parent string `toml:"parent"` // parent interface (e.g. "eno1", "br0")
Image string `toml:"image" env:"PIXELS_DEFAULT_IMAGE"`
CPU string `toml:"cpu" env:"PIXELS_DEFAULT_CPU"`
Memory int64 `toml:"memory" env:"PIXELS_DEFAULT_MEMORY"` // MiB
Pool string `toml:"pool" env:"PIXELS_DEFAULT_POOL"`
NICType string `toml:"nic_type"` // "macvlan" or "bridged"
Parent string `toml:"parent"` // parent interface (e.g. "eno1", "br0")
Network string `toml:"network"` // Incus network name (e.g. "incusbr0")
DNS []string `toml:"dns"` // nameservers to write into containers
}

type SSH struct {
User string `toml:"user"`
Key string `toml:"key"`
User string `toml:"user" env:"PIXELS_SSH_USER"`
Key string `toml:"key" env:"PIXELS_SSH_KEY"`
}

type Checkpoint struct {
DatasetPrefix string `toml:"dataset_prefix"`
DatasetPrefix string `toml:"dataset_prefix" env:"PIXELS_CHECKPOINT_DATASET_PREFIX"`
}

type Provision struct {
Enabled *bool `toml:"enabled"`
DevTools *bool `toml:"devtools"`
Enabled *bool `toml:"enabled" env:"PIXELS_PROVISION_ENABLED"`
DevTools *bool `toml:"devtools" env:"PIXELS_PROVISION_DEVTOOLS"`
}

func (p *Provision) IsEnabled() bool {
Expand All @@ -68,7 +68,7 @@ func (p *Provision) DevToolsEnabled() bool {
}

type Network struct {
Egress string `toml:"egress"`
Egress string `toml:"egress" env:"PIXELS_NETWORK_EGRESS"`
Allow []string `toml:"allow"`
}

Expand Down Expand Up @@ -103,21 +103,9 @@ func Load() (*Config, error) {
}
}

applyEnv(&cfg.TrueNAS.Host, "PIXELS_TRUENAS_HOST")
applyEnv(&cfg.TrueNAS.Username, "PIXELS_TRUENAS_USERNAME")
applyEnv(&cfg.TrueNAS.APIKey, "PIXELS_TRUENAS_API_KEY")
applyEnvInt(&cfg.TrueNAS.Port, "PIXELS_TRUENAS_PORT")
applyEnvBool(&cfg.TrueNAS.InsecureSkipVerify, "PIXELS_TRUENAS_INSECURE")
applyEnv(&cfg.Defaults.Image, "PIXELS_DEFAULT_IMAGE")
applyEnv(&cfg.Defaults.CPU, "PIXELS_DEFAULT_CPU")
applyEnvInt64(&cfg.Defaults.Memory, "PIXELS_DEFAULT_MEMORY")
applyEnv(&cfg.Defaults.Pool, "PIXELS_DEFAULT_POOL")
applyEnv(&cfg.SSH.User, "PIXELS_SSH_USER")
applyEnv(&cfg.SSH.Key, "PIXELS_SSH_KEY")
applyEnv(&cfg.Checkpoint.DatasetPrefix, "PIXELS_CHECKPOINT_DATASET_PREFIX")
applyEnvBool(&cfg.Provision.Enabled, "PIXELS_PROVISION_ENABLED")
applyEnvBool(&cfg.Provision.DevTools, "PIXELS_PROVISION_DEVTOOLS")
applyEnv(&cfg.Network.Egress, "PIXELS_NETWORK_EGRESS")
if err := env.Parse(cfg); err != nil {
return nil, fmt.Errorf("parsing environment: %w", err)
}

cfg.SSH.Key = expandHome(cfg.SSH.Key)

Expand All @@ -136,37 +124,6 @@ func configPath() string {
return filepath.Join(home, ".config", "pixels", "config.toml")
}

func applyEnv(dst *string, key string) {
if v := os.Getenv(key); v != "" {
*dst = v
}
}

func applyEnvInt(dst *int, key string) {
if v := os.Getenv(key); v != "" {
if n, err := strconv.Atoi(v); err == nil {
*dst = n
}
}
}

func applyEnvInt64(dst *int64, key string) {
if v := os.Getenv(key); v != "" {
if n, err := strconv.ParseInt(v, 10, 64); err == nil {
*dst = n
}
}
}

func applyEnvBool(dst **bool, key string) {
if v := os.Getenv(key); v != "" {
b, err := strconv.ParseBool(v)
if err == nil {
*dst = &b
}
}
}

// InsecureSkipVerify returns whether TLS verification should be skipped.
// Defaults to true (skip) when not explicitly set, since most TrueNAS boxes use self-signed certs.
func (t *TrueNAS) InsecureSkipVerifyValue() bool {
Expand Down
28 changes: 28 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,34 @@ api_key = "file-key"
}
}

func TestEmptyEnvDoesNotOverrideDefault(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
t.Setenv("PIXELS_DEFAULT_IMAGE", "")
t.Setenv("PIXELS_DEFAULT_POOL", "")

cfg, err := Load()
if err != nil {
t.Fatalf("Load() error: %v", err)
}

if cfg.Defaults.Image != "ubuntu/24.04" {
t.Errorf("image = %q, want %q (empty env should not override default)", cfg.Defaults.Image, "ubuntu/24.04")
}
if cfg.Defaults.Pool != "tank" {
t.Errorf("pool = %q, want %q (empty env should not override default)", cfg.Defaults.Pool, "tank")
}
}

func TestInvalidEnvReturnsError(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
t.Setenv("PIXELS_TRUENAS_PORT", "not-a-number")

_, err := Load()
if err == nil {
t.Fatal("expected error for invalid PIXELS_TRUENAS_PORT, got nil")
}
}

func TestProvisionEnvOverride(t *testing.T) {
dir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", dir)
Expand Down