mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
* feat(branding): white-label — public /branding, admin write, shell theming app_settings branding.* keys (product name, logo/favicon URLs, accent color, docs/support links). Public GET /branding so login and shell theme pre-auth; admin writes via PUT /settings. useBranding() applies document.title, accent CSS var and favicon; BrandWordmark replaces hardcoded wordmarks; admin Platform settings gain a Branding form. MCP containr_branding_get + settings_update. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat(invites): team invite links — admin create/revoke, public accept Admins mint single-use invite links (sha256-hashed token at rest, optional email binding, configurable TTL). Public endpoints let the accept page validate the link and register — working even while public signup is disabled. - migration + sqlc queries/model for user_invites - POST /admin/invites (returns one-time token+url), GET list, DELETE revoke - GET /auth/invites/:token public validation, POST /auth/accept-invite - CLI containr invites list|create|revoke; MCP invites tools - /auth/accept-invite page (expired/used/invalid states, bound-email lock) - Admin page Team invites section: create, copy link, revoke - OpenAPI + generated types + CLI guide Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat(admin): impersonate user — 15-min token, audit-logged POST /admin/users/:id/impersonate mints a short-lived bearer token (impersonated_by claim) for support/debugging. Admin accounts cannot be impersonated; every mint is audit-logged with actor + target. - CLI: containr admin impersonate <user-id> (works via CONTAINR_TOKEN) - MCP: containr_admin_impersonate - Admin UI: per-user Impersonate action → token modal with usage hint - OpenAPI + generated types Verified live: token resolves to target, admin target 403, audit row. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat(git): GitHub App manifest self-provisioning + app-level webhook Admins create the instance GitHub App in-browser instead of hand- setting GITHUB_APP_* env vars: - POST /admin/git/github-app/manifest builds the manifest + URL; the frontend auto-submits it to github.com - /git/github-app/callback page exchanges the code via POST /admin/git/github-app/convert; credentials persist in app_settings (precedence over env) - GET /admin/git/github-app reports configured/source - POST /api/git/github-app/webhook — single app-level push receiver, HMAC-verified, dispatches via extracted dispatchPushToServices - install-url + installation-token paths resolve settings→env - CLI: containr admin github-app status|manifest|convert; MCP tools - Git providers UI shows "Set up GitHub App" for admins when unconfigured Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat: activity feed — enriched audit events, global + per-project timelines audit_logs gains severity/category/label columns derived from resource+action at write time; legacy rows are classified on read and backfilled at boot in batches. - GET /activity: global feed scoped to the caller's visible projects (admins see all); severity/category/resource/project_id/since filters, page+limit - GET /projects/:id/activity: timeline correlated to the project's services, deployments, databases and the project record; access mirrors project read rules - Retention worker prunes rows older than AUDIT_RETENTION_DAYS (default 90, 0 disables), running once at boot then daily - project.create/delete now audited so timelines aren't empty - CLI `containr activity` with filter flags; MCP tools containr_activity_list + containr_project_activity - Frontend: /activity page (severity chips, icon per category) under Operate nav; project workspace gains an Activity view tab - OpenAPI + generated types + CLI docs Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
113 lines
3.6 KiB
Go
113 lines
3.6 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"containr/internal/cli/commands"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
var cfgFile string
|
|
|
|
var rootCmd = &cobra.Command{
|
|
Use: "containr",
|
|
Short: "Containr CLI - manage your self-hosted PaaS",
|
|
Long: `Containr CLI controls every part of a Containr instance: projects,
|
|
services, deployments, databases, cron jobs, templates, nodes, scaling,
|
|
HA, security, and the API gateway.
|
|
|
|
Authenticate once with a personal access token (Settings → Personal
|
|
Access Tokens in the web UI):
|
|
|
|
containr auth login cnp_...
|
|
|
|
Every command supports --json for machine-readable output, and
|
|
destructive actions accept --yes to skip confirmation.`,
|
|
Version: "2.0.0",
|
|
SilenceUsage: true,
|
|
SilenceErrors: true,
|
|
}
|
|
|
|
// Execute runs the root command and returns the process exit code.
|
|
func Execute() int {
|
|
if err := rootCmd.Execute(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
return commands.ExitCode(err)
|
|
}
|
|
return commands.ExitOK
|
|
}
|
|
|
|
// SetVersion overrides the version reported by `containr --version`.
|
|
func SetVersion(v string) {
|
|
rootCmd.Version = v
|
|
}
|
|
|
|
func init() {
|
|
cobra.OnInitialize(initConfig)
|
|
|
|
pf := rootCmd.PersistentFlags()
|
|
pf.StringVar(&cfgFile, "config", "", "config file (default $HOME/.containr.yaml)")
|
|
pf.String("api-url", "", "Containr API URL (overrides profile and CONTAINR_API_URL)")
|
|
pf.String("token", "", "auth token (overrides profile and CONTAINR_TOKEN)")
|
|
pf.StringP("profile", "p", "", "config profile to use (default current_profile)")
|
|
pf.Bool("json", false, "machine-readable JSON output")
|
|
pf.BoolP("yes", "y", false, "skip confirmation prompts (required for destructive ops in scripts)")
|
|
pf.String("idempotency-key", "", "Idempotency-Key header for POST mutations (safe retries)")
|
|
|
|
viper.BindPFlag("api-url", pf.Lookup("api-url"))
|
|
viper.BindPFlag("token", pf.Lookup("token"))
|
|
viper.BindPFlag("profile", pf.Lookup("profile"))
|
|
viper.BindPFlag("json", pf.Lookup("json"))
|
|
viper.BindPFlag("yes", pf.Lookup("yes"))
|
|
viper.BindPFlag("idempotency-key", pf.Lookup("idempotency-key"))
|
|
|
|
rootCmd.AddCommand(commands.AuthCmd)
|
|
rootCmd.AddCommand(commands.TokensCmd)
|
|
rootCmd.AddCommand(commands.ProjectsCmd)
|
|
rootCmd.AddCommand(commands.ServicesCmd)
|
|
rootCmd.AddCommand(commands.DeployCmd)
|
|
rootCmd.AddCommand(commands.DeploymentsCmd)
|
|
rootCmd.AddCommand(commands.LogsCmd)
|
|
rootCmd.AddCommand(commands.ExecCmd)
|
|
rootCmd.AddCommand(commands.VariablesCmd)
|
|
rootCmd.AddCommand(commands.VolumesCmd)
|
|
rootCmd.AddCommand(commands.RegistriesCmd)
|
|
rootCmd.AddCommand(commands.BackupTargetsCmd)
|
|
rootCmd.AddCommand(commands.DatabasesCmd)
|
|
rootCmd.AddCommand(commands.CronCmd)
|
|
rootCmd.AddCommand(commands.TemplatesCmd)
|
|
rootCmd.AddCommand(commands.ImportCmd)
|
|
rootCmd.AddCommand(commands.NodesCmd)
|
|
rootCmd.AddCommand(commands.ScalingCmd)
|
|
rootCmd.AddCommand(commands.HACmd)
|
|
rootCmd.AddCommand(commands.SecurityCmd)
|
|
rootCmd.AddCommand(commands.GatewayCmd)
|
|
rootCmd.AddCommand(commands.AdminCmd)
|
|
rootCmd.AddCommand(commands.NotificationsCmd)
|
|
rootCmd.AddCommand(commands.WebhooksCmd)
|
|
rootCmd.AddCommand(commands.OperationsCmd)
|
|
rootCmd.AddCommand(commands.ActivityCmd)
|
|
rootCmd.AddCommand(commands.BannersCmd)
|
|
rootCmd.AddCommand(commands.InvitesCmd)
|
|
rootCmd.AddCommand(commands.UpCmd)
|
|
}
|
|
|
|
func initConfig() {
|
|
if cfgFile != "" {
|
|
viper.SetConfigFile(cfgFile)
|
|
} else {
|
|
home, err := os.UserHomeDir()
|
|
cobra.CheckErr(err)
|
|
viper.AddConfigPath(home)
|
|
viper.SetConfigType("yaml")
|
|
viper.SetConfigName(".containr")
|
|
}
|
|
viper.AutomaticEnv()
|
|
// CONTAINR_TOKEN / CONTAINR_API_URL are read explicitly in config.go;
|
|
// AutomaticEnv covers them only when bound, so this is belt-and-braces.
|
|
viper.ReadInConfig()
|
|
}
|