mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
Rewrite the CLI as a thin client over the authenticated API so agents and scripts reach every resource the UI does (ROADMAP2 phase A2). - shared Client with bearer auth, error-envelope decoding, and stable exit codes: 0 ok / 1 error / 2 auth / 3 not-found / 4 validation / 5 unreachable - named profiles in ~/.containr.yaml (current_profile + profiles.*); resolution order --flag > env > profile > localhost default; CONTAINR_TOKEN / CONTAINR_API_URL / CONTAINR_PROFILE env support - --json prints raw API responses; --yes skips confirmations; destructive commands refuse non-interactively without it - auth login verifies the token against /user/profile before saving, interactive prompt via term.ReadPassword - commands for every resource: tokens, projects, services, deploy, deployments (logs/rollback), logs --follow (SSE), exec, variables (set/unset merge with secret masking preserved), databases (backup/restore/download), cron, templates, nodes + agent tokens, scaling, ha, security, gateway, admin, notifications - containr up: git remote + branch → create-or-reuse service → deploy Verified live: login/status/tokens/projects/services, up creating a service + deployment, non-TTY confirmation refusal, scope-capped admin rejection (exit 2), revoke. Authored By: TDvorak <[email protected]>
107 lines
2.7 KiB
Go
107 lines
2.7 KiB
Go
package commands
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
// ActiveProfile resolves which profile applies: --profile flag >
|
|
// CONTAINR_PROFILE env > current_profile in config > "default".
|
|
func ActiveProfile() string {
|
|
if p := viper.GetString("profile"); p != "" {
|
|
return p
|
|
}
|
|
if p := os.Getenv("CONTAINR_PROFILE"); p != "" {
|
|
return p
|
|
}
|
|
if p := viper.GetString("current_profile"); p != "" {
|
|
return p
|
|
}
|
|
return "default"
|
|
}
|
|
|
|
// ResolveAPIURL returns the effective API base URL:
|
|
// --api-url flag > CONTAINR_API_URL env > profile.api_url > localhost default.
|
|
func ResolveAPIURL() string {
|
|
if u := viper.GetString("api-url"); u != "" {
|
|
return normalizeBase(u)
|
|
}
|
|
if u := os.Getenv("CONTAINR_API_URL"); u != "" {
|
|
return normalizeBase(u)
|
|
}
|
|
if u := viper.GetString("profiles." + ActiveProfile() + ".api_url"); u != "" {
|
|
return normalizeBase(u)
|
|
}
|
|
return "http://localhost:8080/api/v1"
|
|
}
|
|
|
|
// ResolveToken returns the effective auth token:
|
|
// --token flag > CONTAINR_TOKEN env > profile.token.
|
|
func ResolveToken() string {
|
|
if t := viper.GetString("token"); t != "" {
|
|
return t
|
|
}
|
|
if t := os.Getenv("CONTAINR_TOKEN"); t != "" {
|
|
return t
|
|
}
|
|
return viper.GetString("profiles." + ActiveProfile() + ".token")
|
|
}
|
|
|
|
func normalizeBase(u string) string {
|
|
u = strings.TrimSuffix(strings.TrimSpace(u), "/")
|
|
// Accept the bare host too — append the API prefix when missing.
|
|
if u != "" && !strings.HasSuffix(u, "/api/v1") {
|
|
u += "/api/v1"
|
|
}
|
|
return u
|
|
}
|
|
|
|
// SaveProfile persists api_url/token under a named profile and makes it
|
|
// current. Creates the config file if none exists.
|
|
func SaveProfile(name, apiURL, token string) error {
|
|
base := "profiles." + name
|
|
if apiURL != "" {
|
|
viper.Set(base+".api_url", apiURL)
|
|
}
|
|
if token != "" {
|
|
viper.Set(base+".token", token)
|
|
}
|
|
viper.Set("current_profile", name)
|
|
return writeConfig()
|
|
}
|
|
|
|
// ClearProfileToken removes only the token from the active profile.
|
|
func ClearProfileToken(name string) error {
|
|
viper.Set("profiles."+name+".token", "")
|
|
return writeConfig()
|
|
}
|
|
|
|
// ListProfiles returns profile name → api_url for `auth status --all`.
|
|
func ListProfiles() map[string]string {
|
|
out := map[string]string{}
|
|
profiles := viper.GetStringMap("profiles")
|
|
for name := range profiles {
|
|
if u := viper.GetString("profiles." + name + ".api_url"); u != "" {
|
|
out[name] = u
|
|
} else {
|
|
out[name] = "(no url)"
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func writeConfig() error {
|
|
if err := viper.WriteConfig(); err != nil {
|
|
// No config file yet — create one.
|
|
home, herr := os.UserHomeDir()
|
|
if herr != nil {
|
|
return fmt.Errorf("cannot locate home directory: %w", herr)
|
|
}
|
|
return viper.WriteConfigAs(home + "/.containr.yaml")
|
|
}
|
|
return nil
|
|
}
|