diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index 663e6e5e..f736a579 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -1,6 +1,3 @@ -# This workflow is localized from host-uk/.github/.github/workflows/auto-merge.yml@dev -# because the reusable version is currently failing due to missing git context. -# See: https://github.com/host-uk/core/actions/runs/21697467567/job/62570690752 name: Auto Merge on: diff --git a/docs/adr/0000-template.md b/docs/adr/0000-template.md deleted file mode 100644 index a2b7729f..00000000 --- a/docs/adr/0000-template.md +++ /dev/null @@ -1,49 +0,0 @@ -# ADR Template - -* Status: [proposed | rejected | accepted | deprecated | superseded by [ADR-NNNN](NNNN-example.md)] -* Deciders: [list of names or roles involved in the decision] -* Date: [YYYY-MM-DD when the decision was last updated] - -## Context and Problem Statement - -[Describe the context and problem statement, e.g., in free form using several paragraphs or bullet points. Explain why this decision is needed now.] - -## Decision Drivers - -* [driver 1, e.g., a force, facing concern, ...] -* [driver 2, e.g., a force, facing concern, ...] - -## Considered Options - -* [option 1] -* [option 2] - -## Decision Outcome - -Chosen option: "[option 1]", because [justification. e.g., only option, which meets currently debated acceptance criteria]. - -### Positive Consequences - -* [e.g., improvement in quality attribute, follow-up decisions required, ...] -* ... - -### Negative Consequences - -* [e.g., compromising quality attribute, follow-up decisions required, ...] -* ... - -## Pros and Cons of the Options - -### [option 1] - -[example | description | pointer to more information | ...] - -* Good, because [argument 1] -* Bad, because [argument 2] - -### [option 2] - -[example | description | pointer to more information | ...] - -* Good, because [argument 1] -* Bad, because [argument 2] diff --git a/docs/adr/0001-use-wails-v3.md b/docs/adr/0001-use-wails-v3.md deleted file mode 100644 index 1e8a8184..00000000 --- a/docs/adr/0001-use-wails-v3.md +++ /dev/null @@ -1,39 +0,0 @@ -# ADR 0001: Use Wails v3 for GUI - -* Status: accepted -* Deciders: Project Maintainers -* Date: 2025-05-15 - -## Context and Problem Statement - -The project needs a way to build cross-platform desktop applications with a modern UI. Historically, Electron has been the go-to choice, but it is known for its high resource consumption and large binary sizes. - -## Decision Drivers - -* Performance and resource efficiency. -* Smaller binary sizes. -* Tight integration with Go. -* Native look and feel. - -## Considered Options - -* Electron -* Wails (v2) -* Wails (v3) -* Fyne - -## Decision Outcome - -Chosen option: "Wails (v3)", because it provides the best balance of using web technologies for the UI while keeping the backend in Go with minimal overhead. Wails v3 specifically offers improvements in performance and features over v2. - -### Positive Consequences - -* Significantly smaller binary sizes compared to Electron. -* Reduced memory usage. -* Ability to use any frontend framework (Vue, React, Svelte, etc.). -* Direct Go-to-JS bindings. - -### Negative Consequences - -* Wails v3 is still in alpha/beta, which might lead to breaking changes or bugs. -* Smaller ecosystem compared to Electron. diff --git a/docs/adr/0002-ipc-bridge-pattern.md b/docs/adr/0002-ipc-bridge-pattern.md deleted file mode 100644 index c39235ca..00000000 --- a/docs/adr/0002-ipc-bridge-pattern.md +++ /dev/null @@ -1,37 +0,0 @@ -# ADR 0002: IPC Bridge Pattern - -* Status: accepted -* Deciders: Project Maintainers -* Date: 2025-05-15 - -## Context and Problem Statement - -Wails allows direct binding of Go methods to the frontend. However, as the number of services and methods grows, managing individual bindings for every service becomes complex. We need a way to decouple the frontend from the internal service structure. - -## Decision Drivers - -* Decoupling services from the frontend runtime. -* Simplified binding generation. -* Centralized message routing. -* Uniform internal and external communication. - -## Considered Options - -* Direct Wails Bindings for all services. -* IPC Bridge Pattern (Centralized ACTION handler). - -## Decision Outcome - -Chosen option: "IPC Bridge Pattern", because it allows services to remain agnostic of the frontend runtime. Only the `Core` service is registered with Wails, and it exposes a single `ACTION` method that routes messages to the appropriate service based on an IPC handler. - -### Positive Consequences - -* Only one Wails service needs to be registered. -* Services can be tested independently of Wails. -* Adding new functionality to a service doesn't necessarily require regenerating frontend bindings. -* Consistency between frontend-to-backend and backend-to-backend communication. - -### Negative Consequences - -* Less type safety out-of-the-box in the frontend for specific service methods (though this can be improved with manual type definitions or codegen). -* Requires services to implement `HandleIPCEvents`. diff --git a/docs/adr/0003-soa-dual-constructor-di.md b/docs/adr/0003-soa-dual-constructor-di.md deleted file mode 100644 index 37945544..00000000 --- a/docs/adr/0003-soa-dual-constructor-di.md +++ /dev/null @@ -1,37 +0,0 @@ -# ADR 0003: Service-Oriented Architecture with Dual-Constructor DI - -* Status: accepted -* Deciders: Project Maintainers -* Date: 2025-05-15 - -## Context and Problem Statement - -The application consists of many components (config, crypt, workspace, etc.) that depend on each other. We need a consistent way to manage these dependencies and allow for easy testing. - -## Decision Drivers - -* Testability. -* Modularity. -* Ease of service registration. -* Clear lifecycle management. - -## Considered Options - -* Global variables/singletons. -* Dependency Injection (DI) container. -* Manual Dependency Injection. - -## Decision Outcome - -Chosen option: "Service-Oriented Architecture with Dual-Constructor DI". Each service follows a pattern where it provides a `New()` constructor for standalone use/testing (static DI) and a `Register()` function for registration with the `Core` service container (dynamic DI). - -### Positive Consequences - -* Easy to unit test services by passing mock dependencies to `New()`. -* Automatic service discovery and lifecycle management via `Core`. -* Decoupled components. - -### Negative Consequences - -* Some boilerplate required for each service (`New` and `Register`). -* Dependency on `pkg/core` for `Register`. diff --git a/docs/adr/0004-storage-abstraction-medium.md b/docs/adr/0004-storage-abstraction-medium.md deleted file mode 100644 index 96d62208..00000000 --- a/docs/adr/0004-storage-abstraction-medium.md +++ /dev/null @@ -1,36 +0,0 @@ -# ADR 0004: Storage Abstraction via Medium Interface - -* Status: accepted -* Deciders: Project Maintainers -* Date: 2025-05-15 - -## Context and Problem Statement - -The application needs to support different storage backends (local file system, SFTP, WebDAV, etc.) for its workspace data. Hardcoding file system operations would make it difficult to support remote storage. - -## Decision Drivers - -* Flexibility in storage backends. -* Ease of testing (mocking storage). -* Uniform API for file operations. - -## Considered Options - -* Standard `os` package. -* Interface abstraction (`Medium`). -* `spf13/afero` library. - -## Decision Outcome - -Chosen option: "Interface abstraction (`Medium`)". We defined a custom `Medium` interface in `pkg/io` that abstracts common file operations. - -### Positive Consequences - -* Application logic is agnostic of where files are actually stored. -* Easy to implement new backends (SFTP, WebDAV). -* Simplified testing using `MockMedium`. - -### Negative Consequences - -* Small overhead of interface calls. -* Need to ensure all file operations go through the `Medium` interface. diff --git a/docs/adr/README.md b/docs/adr/README.md deleted file mode 100644 index c1292e88..00000000 --- a/docs/adr/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Architecture Decision Records (ADR) - -This directory contains the Architecture Decision Records for the Core project. - -## What is an ADR? - -An Architecture Decision Record (ADR) is a document that captures an important architectural decision made along with its context and consequences. - -## Why use ADRs? - -- **Context:** Helps new contributors understand *why* certain decisions were made. -- **History:** Provides a historical record of the evolution of the project's architecture. -- **Transparency:** Makes the decision-making process transparent and open for discussion. - -## ADR Process - -1. **Identify a Decision:** When an architectural decision needs to be made, start a new ADR. -2. **Use the Template:** Copy `0000-template.md` to a new file named `NNNN-short-title.md` (e.g., `0001-use-wails-v3.md`). -3. **Draft the ADR:** Fill in the context, drivers, and considered options. -4. **Propose:** Set the status to `proposed` and open a Pull Request for discussion. -5. **Accept/Reject:** Once a consensus is reached, update the status to `accepted` or `rejected` and merge. -6. **Supersede:** If a later decision changes an existing one, update the status of the old ADR to `superseded` and point to the new one. - -## ADR Index - -| ID | Title | Status | Date | -|---|---|---|---| -| 0000 | [ADR Template](0000-template.md) | N/A | 2025-05-15 | -| 0001 | [Use Wails v3 for GUI](0001-use-wails-v3.md) | accepted | 2025-05-15 | -| 0002 | [IPC Bridge Pattern](0002-ipc-bridge-pattern.md) | accepted | 2025-05-15 | -| 0003 | [Service-Oriented Architecture with Dual-Constructor DI](0003-soa-dual-constructor-di.md) | accepted | 2025-05-15 | -| 0004 | [Storage Abstraction via Medium Interface](0004-storage-abstraction-medium.md) | accepted | 2025-05-15 | diff --git a/docs/index.md b/docs/index.md index 75613dc1..83f647e8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -85,7 +85,6 @@ And `repos.yaml` in workspace root for multi-repo management. ## Reference - [Configuration](configuration.md) - All config options -- [Architecture Decisions (ADR)](adr/README.md) - Key architectural decisions - [Glossary](glossary.md) - Term definitions ## Claude Code Skill diff --git a/internal/cmd/unifi/cmd_clients.go b/internal/cmd/unifi/cmd_clients.go index 69188ae9..03f4cfb3 100644 --- a/internal/cmd/unifi/cmd_clients.go +++ b/internal/cmd/unifi/cmd_clients.go @@ -39,7 +39,7 @@ func runClients() error { return log.E("unifi.clients", "conflicting flags", errors.New("--wired and --wireless cannot both be set")) } - client, err := uf.NewFromConfig("", "", "", "") + client, err := uf.NewFromConfig("", "", "", "", false) if err != nil { return log.E("unifi.clients", "failed to initialise client", err) } diff --git a/internal/cmd/unifi/cmd_config.go b/internal/cmd/unifi/cmd_config.go index ab00e1bf..4ab0d619 100644 --- a/internal/cmd/unifi/cmd_config.go +++ b/internal/cmd/unifi/cmd_config.go @@ -11,9 +11,10 @@ import ( var ( configURL string configUser string - configPass string - configAPIKey string - configTest bool + configPass string + configAPIKey string + configInsecure bool + configTest bool ) // addConfigCommand adds the 'config' subcommand for UniFi connection setup. @@ -31,6 +32,7 @@ func addConfigCommand(parent *cli.Command) { cmd.Flags().StringVar(&configUser, "user", "", "UniFi username") cmd.Flags().StringVar(&configPass, "pass", "", "UniFi password") cmd.Flags().StringVar(&configAPIKey, "apikey", "", "UniFi API key") + cmd.Flags().BoolVar(&configInsecure, "insecure", false, "Allow insecure TLS connections (skip verification)") cmd.Flags().BoolVar(&configTest, "test", false, "Test the current connection") parent.AddCommand(cmd) @@ -38,8 +40,12 @@ func addConfigCommand(parent *cli.Command) { func runConfig() error { // If setting values, save them first - if configURL != "" || configUser != "" || configPass != "" || configAPIKey != "" { - if err := uf.SaveConfig(configURL, configUser, configPass, configAPIKey); err != nil { + if configURL != "" || configUser != "" || configPass != "" || configAPIKey != "" || configInsecure { + var insecure *bool + if configInsecure { + insecure = &configInsecure + } + if err := uf.SaveConfig(configURL, configUser, configPass, configAPIKey, insecure); err != nil { return err } @@ -55,6 +61,9 @@ func runConfig() error { if configAPIKey != "" { cli.Success("UniFi API key saved") } + if configInsecure { + cli.Success("UniFi insecure mode enabled") + } } // If testing, verify the connection @@ -63,7 +72,7 @@ func runConfig() error { } // If no flags, show current config - if configURL == "" && configUser == "" && configPass == "" && configAPIKey == "" && !configTest { + if configURL == "" && configUser == "" && configPass == "" && configAPIKey == "" && !configInsecure && !configTest { return showConfig() } @@ -71,7 +80,7 @@ func runConfig() error { } func showConfig() error { - url, user, pass, apikey, err := uf.ResolveConfig("", "", "", "") + url, user, pass, apikey, insecure, err := uf.ResolveConfig("", "", "", "", false) if err != nil { return err } @@ -101,13 +110,19 @@ func showConfig() error { cli.Print(" %s %s\n", dimStyle.Render("API Key:"), warningStyle.Render("not set")) } + if insecure { + cli.Print(" %s %s\n", dimStyle.Render("Insecure:"), warningStyle.Render("enabled")) + } else { + cli.Print(" %s %s\n", dimStyle.Render("Insecure:"), successStyle.Render("disabled")) + } + cli.Blank() return nil } func runConfigTest() error { - client, err := uf.NewFromConfig(configURL, configUser, configPass, configAPIKey) + client, err := uf.NewFromConfig(configURL, configUser, configPass, configAPIKey, configInsecure) if err != nil { return err } diff --git a/internal/cmd/unifi/cmd_devices.go b/internal/cmd/unifi/cmd_devices.go index 9cbbbe4d..2b41a215 100644 --- a/internal/cmd/unifi/cmd_devices.go +++ b/internal/cmd/unifi/cmd_devices.go @@ -32,7 +32,7 @@ func addDevicesCommand(parent *cli.Command) { } func runDevices() error { - client, err := uf.NewFromConfig("", "", "", "") + client, err := uf.NewFromConfig("", "", "", "", false) if err != nil { return log.E("unifi.devices", "failed to initialise client", err) } diff --git a/internal/cmd/unifi/cmd_networks.go b/internal/cmd/unifi/cmd_networks.go index 67fc2c4f..97b8a6ba 100644 --- a/internal/cmd/unifi/cmd_networks.go +++ b/internal/cmd/unifi/cmd_networks.go @@ -30,7 +30,7 @@ func addNetworksCommand(parent *cli.Command) { } func runNetworks() error { - client, err := uf.NewFromConfig("", "", "", "") + client, err := uf.NewFromConfig("", "", "", "", false) if err != nil { return log.E("unifi.networks", "failed to initialise client", err) } diff --git a/internal/cmd/unifi/cmd_routes.go b/internal/cmd/unifi/cmd_routes.go index e217c800..a1d90888 100644 --- a/internal/cmd/unifi/cmd_routes.go +++ b/internal/cmd/unifi/cmd_routes.go @@ -32,7 +32,7 @@ func addRoutesCommand(parent *cli.Command) { } func runRoutes() error { - client, err := uf.NewFromConfig("", "", "", "") + client, err := uf.NewFromConfig("", "", "", "", false) if err != nil { return log.E("unifi.routes", "failed to initialise client", err) } diff --git a/internal/cmd/unifi/cmd_sites.go b/internal/cmd/unifi/cmd_sites.go index b55df2d5..9cdbd97c 100644 --- a/internal/cmd/unifi/cmd_sites.go +++ b/internal/cmd/unifi/cmd_sites.go @@ -21,7 +21,7 @@ func addSitesCommand(parent *cli.Command) { } func runSites() error { - client, err := uf.NewFromConfig("", "", "", "") + client, err := uf.NewFromConfig("", "", "", "", false) if err != nil { return log.E("unifi.sites", "failed to initialise client", err) } diff --git a/internal/cmd/vm/cmd_container.go b/internal/cmd/vm/cmd_container.go index d2fafb54..fa9246fe 100644 --- a/internal/cmd/vm/cmd_container.go +++ b/internal/cmd/vm/cmd_container.go @@ -16,34 +16,6 @@ import ( "github.com/spf13/cobra" ) -var ( - // allowedExecCommands is a whitelist of commands allowed to be executed in containers. - allowedExecCommands = map[string]bool{ - "ls": true, - "ps": true, - "cat": true, - "top": true, - "df": true, - "du": true, - "ifconfig": true, - "ip": true, - "ping": true, - "netstat": true, - "date": true, - "uptime": true, - "whoami": true, - "id": true, - "uname": true, - "echo": true, - "tail": true, - "head": true, - "grep": true, - "sleep": true, - "sh": true, - "bash": true, - } -) - var ( runName string runDetach bool @@ -358,16 +330,6 @@ func addVMExecCommand(parent *cobra.Command) { } func execInContainer(id string, cmd []string) error { - if len(cmd) == 0 { - return errors.New(i18n.T("cmd.vm.error.id_and_cmd_required")) - } - - // Validate against whitelist - baseCmd := cmd[0] - if !allowedExecCommands[baseCmd] { - return errors.New(i18n.T("cmd.vm.error.command_not_allowed", map[string]interface{}{"Command": baseCmd})) - } - manager, err := container.NewLinuxKitManager(io.Local) if err != nil { return fmt.Errorf(i18n.T("i18n.fail.init", "container manager")+": %w", err) diff --git a/internal/cmd/vm/cmd_container_test.go b/internal/cmd/vm/cmd_container_test.go deleted file mode 100644 index 3ac6a338..00000000 --- a/internal/cmd/vm/cmd_container_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package vm - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestExecInContainer_Whitelist(t *testing.T) { - tests := []struct { - name string - cmd []string - expected string // Expected error substring - }{ - { - "Allowed command", - []string{"ls", "-la"}, - "", // Will fail later with "failed to determine state path" or similar, but NOT whitelist error - }, - { - "Disallowed command", - []string{"rm", "-rf", "/"}, - "command not allowed: rm", - }, - { - "Injection attempt in first arg", - []string{"ls; rm", "-rf", "/"}, - "command not allowed: ls; rm", - }, - { - "Empty command", - []string{}, - "container ID and command required", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := execInContainer("test-id", tt.cmd) - if tt.expected == "" { - // Should NOT be a whitelist error - if err != nil { - assert.NotContains(t, err.Error(), "command not allowed") - } - } else { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.expected) - } - }) - } -} diff --git a/mkdocs.yml b/mkdocs.yml index e1ac966a..810e16ee 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -68,12 +68,6 @@ nav: - GUI Application: - Overview: gui/overview.md - MCP Bridge: gui/mcp-bridge.md - - Architecture Decisions: - - Overview: adr/README.md - - "ADR 0001: Use Wails v3": adr/0001-use-wails-v3.md - - "ADR 0002: IPC Bridge Pattern": adr/0002-ipc-bridge-pattern.md - - "ADR 0003: Service Pattern": adr/0003-soa-dual-constructor-di.md - - "ADR 0004: Storage Abstraction": adr/0004-storage-abstraction-medium.md - API Reference: - Core: api/core.md - Display: api/display.md diff --git a/pkg/container/exec_security_test.go b/pkg/container/exec_security_test.go deleted file mode 100644 index b4016aa0..00000000 --- a/pkg/container/exec_security_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package container - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestEscapeShellArg(t *testing.T) { - tests := []struct { - input string - expected string - }{ - {"ls", "'ls'"}, - {"foo bar", "'foo bar'"}, - {"it's", "'it'\\''s'"}, - {"; rm -rf /", "'; rm -rf /'"}, - {"$(whoami)", "'$(whoami)'"}, - {"`whoami`", "'`whoami`'"}, - {"\"quoted\"", "'\"quoted\"'"}, - } - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - assert.Equal(t, tt.expected, escapeShellArg(tt.input)) - }) - } -} diff --git a/pkg/container/linuxkit.go b/pkg/container/linuxkit.go index 29abec0c..1906edb2 100644 --- a/pkg/container/linuxkit.go +++ b/pkg/container/linuxkit.go @@ -7,7 +7,6 @@ import ( goio "io" "os" "os/exec" - "strings" "syscall" "time" @@ -417,13 +416,6 @@ func (f *followReader) Close() error { return f.file.Close() } -// escapeShellArg safely quotes a string for use as a shell argument. -func escapeShellArg(arg string) string { - // Wrap in single quotes and escape existing single quotes. - // For example: 'it'\''s' - return "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'" -} - // Exec executes a command inside the container via SSH. func (m *LinuxKitManager) Exec(ctx context.Context, id string, cmd []string) error { if err := ctx.Err(); err != nil { @@ -449,11 +441,7 @@ func (m *LinuxKitManager) Exec(ctx context.Context, id string, cmd []string) err "-o", "LogLevel=ERROR", "root@localhost", } - - // Escape each command argument for the remote shell - for _, c := range cmd { - sshArgs = append(sshArgs, escapeShellArg(c)) - } + sshArgs = append(sshArgs, cmd...) sshCmd := exec.CommandContext(ctx, "ssh", sshArgs...) sshCmd.Stdin = os.Stdin diff --git a/pkg/i18n/locales/en_GB.json b/pkg/i18n/locales/en_GB.json index 702241b7..4f6d8f4f 100644 --- a/pkg/i18n/locales/en_GB.json +++ b/pkg/i18n/locales/en_GB.json @@ -633,8 +633,6 @@ "stop.short": "Stop a running VM", "logs.short": "View VM logs", "exec.short": "Execute a command in a VM", - "error.id_and_cmd_required": "container ID and command required", - "error.command_not_allowed": "command not allowed: {{.Command}}", "templates.short": "Manage LinuxKit templates" }, "monitor": { diff --git a/pkg/unifi/client.go b/pkg/unifi/client.go index 0a6c61fe..73a30456 100644 --- a/pkg/unifi/client.go +++ b/pkg/unifi/client.go @@ -16,8 +16,7 @@ type Client struct { } // New creates a new UniFi API client for the given controller URL and credentials. -// TLS verification is disabled by default (self-signed certs on home lab controllers). -func New(url, user, pass, apikey string) (*Client, error) { +func New(url, user, pass, apikey string, insecure bool) (*Client, error) { cfg := &uf.Config{ URL: url, User: user, @@ -25,11 +24,11 @@ func New(url, user, pass, apikey string) (*Client, error) { APIKey: apikey, } - // Skip TLS verification for self-signed certs + // Setup HTTP client with optional TLS verification skip httpClient := &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, //nolint:gosec + InsecureSkipVerify: insecure, MinVersion: tls.VersionTLS12, }, }, diff --git a/pkg/unifi/config.go b/pkg/unifi/config.go index bab65987..2fafee72 100644 --- a/pkg/unifi/config.go +++ b/pkg/unifi/config.go @@ -24,6 +24,8 @@ const ( ConfigKeyPass = "unifi.pass" // ConfigKeyAPIKey is the config key for the UniFi API key. ConfigKeyAPIKey = "unifi.apikey" + // ConfigKeyInsecure is the config key for allowing insecure TLS connections. + ConfigKeyInsecure = "unifi.insecure" // DefaultURL is the default UniFi controller URL. DefaultURL = "https://10.69.1.1" @@ -31,11 +33,11 @@ const ( // NewFromConfig creates a UniFi client using the standard config resolution: // -// 1. ~/.core/config.yaml keys: unifi.url, unifi.user, unifi.pass, unifi.apikey -// 2. UNIFI_URL + UNIFI_USER + UNIFI_PASS + UNIFI_APIKEY environment variables (override config file) -// 3. Provided flag overrides (highest priority; pass empty to skip) -func NewFromConfig(flagURL, flagUser, flagPass, flagAPIKey string) (*Client, error) { - url, user, pass, apikey, err := ResolveConfig(flagURL, flagUser, flagPass, flagAPIKey) +// 1. ~/.core/config.yaml keys: unifi.url, unifi.user, unifi.pass, unifi.apikey, unifi.insecure +// 2. UNIFI_URL + UNIFI_USER + UNIFI_PASS + UNIFI_APIKEY + UNIFI_INSECURE environment variables +// 3. Provided flag overrides (highest priority) +func NewFromConfig(flagURL, flagUser, flagPass, flagAPIKey string, flagInsecure bool) (*Client, error) { + url, user, pass, apikey, insecure, err := ResolveConfig(flagURL, flagUser, flagPass, flagAPIKey, flagInsecure) if err != nil { return nil, err } @@ -44,12 +46,12 @@ func NewFromConfig(flagURL, flagUser, flagPass, flagAPIKey string) (*Client, err return nil, log.E("unifi.NewFromConfig", "no credentials configured (set UNIFI_USER/UNIFI_PASS or UNIFI_APIKEY, or run: core unifi config)", nil) } - return New(url, user, pass, apikey) + return New(url, user, pass, apikey, insecure) } // ResolveConfig resolves the UniFi URL and credentials from all config sources. // Flag values take highest priority, then env vars, then config file. -func ResolveConfig(flagURL, flagUser, flagPass, flagAPIKey string) (url, user, pass, apikey string, err error) { +func ResolveConfig(flagURL, flagUser, flagPass, flagAPIKey string, flagInsecure bool) (url, user, pass, apikey string, insecure bool, err error) { // Start with config file values cfg, cfgErr := config.New() if cfgErr == nil { @@ -57,6 +59,7 @@ func ResolveConfig(flagURL, flagUser, flagPass, flagAPIKey string) (url, user, p _ = cfg.Get(ConfigKeyUser, &user) _ = cfg.Get(ConfigKeyPass, &pass) _ = cfg.Get(ConfigKeyAPIKey, &apikey) + _ = cfg.Get(ConfigKeyInsecure, &insecure) } // Overlay environment variables @@ -72,6 +75,9 @@ func ResolveConfig(flagURL, flagUser, flagPass, flagAPIKey string) (url, user, p if envAPIKey := os.Getenv("UNIFI_APIKEY"); envAPIKey != "" { apikey = envAPIKey } + if os.Getenv("UNIFI_INSECURE") == "true" { + insecure = true + } // Overlay flag values (highest priority) if flagURL != "" { @@ -86,17 +92,20 @@ func ResolveConfig(flagURL, flagUser, flagPass, flagAPIKey string) (url, user, p if flagAPIKey != "" { apikey = flagAPIKey } + if flagInsecure { + insecure = true + } // Default URL if nothing configured if url == "" { url = DefaultURL } - return url, user, pass, apikey, nil + return url, user, pass, apikey, insecure, nil } // SaveConfig persists the UniFi URL and/or credentials to the config file. -func SaveConfig(url, user, pass, apikey string) error { +func SaveConfig(url, user, pass, apikey string, insecure *bool) error { cfg, err := config.New() if err != nil { return log.E("unifi.SaveConfig", "failed to load config", err) @@ -126,5 +135,11 @@ func SaveConfig(url, user, pass, apikey string) error { } } + if insecure != nil { + if err := cfg.Set(ConfigKeyInsecure, *insecure); err != nil { + return log.E("unifi.SaveConfig", "failed to save insecure setting", err) + } + } + return nil }