mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
feat(cli): v2 — full API coverage, profiles, --json/--yes, containr up
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]>
This commit is contained in:
1 parent
17e429b86b
commit
e51b20b2d4
26 files changed
+2798
-350
No files matched your search
@@ -0,0 +1,190 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ServicesCmd manages services.
|
||||
var ServicesCmd = &cobra.Command{
|
||||
Use: "services",
|
||||
Aliases: []string{"service", "svc"},
|
||||
Short: "Manage services",
|
||||
}
|
||||
|
||||
var servicesListCmd = &cobra.Command{
|
||||
Use: "list [project-id]",
|
||||
Short: "List services in a project",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := c.Do("GET", "/projects/"+args[0]+"/services", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items, err := unwrapList(data, "services")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows := make([][]string, 0, len(items))
|
||||
for _, s := range items {
|
||||
rows = append(rows, []string{
|
||||
str(s, "id"), str(s, "name"), str(s, "type"), str(s, "status"), str(s, "environment"),
|
||||
})
|
||||
}
|
||||
printRows(data, []string{"ID", "NAME", "TYPE", "STATUS", "ENV"}, rows)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var servicesGetCmd = &cobra.Command{
|
||||
Use: "get <id>",
|
||||
Short: "Show a service",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := c.Do("GET", "/services/"+args[0], nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if JSONMode() {
|
||||
PrintRaw(data)
|
||||
return nil
|
||||
}
|
||||
s, _ := unwrapObject(data, "service")
|
||||
for _, k := range []string{"id", "name", "type", "status", "environment", "image", "git_repo", "git_branch", "domain", "port"} {
|
||||
fmt.Printf("%-14s %s\n", k+":", str(s, k))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var servicesCreateCmd = &cobra.Command{
|
||||
Use: "create <project-id> <name>",
|
||||
Short: "Create a service (image- or git-based)",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body := map[string]interface{}{
|
||||
"name": args[1],
|
||||
}
|
||||
for _, f := range []string{"type", "image", "git-repo", "git-branch", "build-path", "command", "environment", "domain", "restart-policy", "healthcheck-path", "cpu", "memory"} {
|
||||
if v, _ := cmd.Flags().GetString(f); v != "" {
|
||||
key := f
|
||||
// JSON uses snake_case keys matching the API schema.
|
||||
switch f {
|
||||
case "git-repo":
|
||||
key = "git_repo"
|
||||
case "git-branch":
|
||||
key = "git_branch"
|
||||
case "build-path":
|
||||
key = "build_path"
|
||||
case "restart-policy":
|
||||
key = "restart_policy"
|
||||
case "healthcheck-path":
|
||||
key = "healthcheck_path"
|
||||
}
|
||||
body[key] = v
|
||||
}
|
||||
}
|
||||
if v, _ := cmd.Flags().GetInt("port"); v > 0 {
|
||||
body["port"] = v
|
||||
}
|
||||
if v, _ := cmd.Flags().GetInt("replicas"); v > 0 {
|
||||
body["replicas"] = v
|
||||
}
|
||||
data, err := c.Do("POST", "/projects/"+args[0]+"/services", body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if JSONMode() {
|
||||
PrintRaw(data)
|
||||
return nil
|
||||
}
|
||||
s, _ := unwrapObject(data, "service")
|
||||
fmt.Printf("Created service %s (%s)\n", str(s, "name"), str(s, "id"))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var servicesDeleteCmd = &cobra.Command{
|
||||
Use: "delete <id>",
|
||||
Short: "Delete a service",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !Confirm(fmt.Sprintf("Delete service %s?", args[0])) {
|
||||
return &APIError{Message: "aborted (pass --yes to skip confirmation)", ExitCode: ExitError}
|
||||
}
|
||||
if _, err := c.Do("DELETE", "/services/"+args[0], nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if JSONMode() {
|
||||
return PrintJSON(map[string]string{"status": "deleted", "id": args[0]})
|
||||
}
|
||||
fmt.Printf("Deleted service %s\n", args[0])
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// serviceAction returns a RunE for the simple POST action endpoints.
|
||||
func serviceAction(verb string) func(*cobra.Command, []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
c, err := client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := c.Do("POST", fmt.Sprintf("/services/%s/%s", args[0], verb), map[string]interface{}{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if JSONMode() {
|
||||
PrintRaw(data)
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("%s: %s\n", verb, args[0])
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
f := servicesCreateCmd.Flags()
|
||||
f.String("type", "web", "service type: web|worker|database|cron")
|
||||
f.String("image", "", "container image (image-based service)")
|
||||
f.String("git-repo", "", "git repository URL (build-from-source service)")
|
||||
f.String("git-branch", "main", "git branch")
|
||||
f.String("build-path", "", "build context subdirectory")
|
||||
f.String("command", "", "container start command override")
|
||||
f.String("environment", "production", "production|preview|development")
|
||||
f.String("domain", "", "public domain")
|
||||
f.String("restart-policy", "unless-stopped", "docker restart policy")
|
||||
f.String("healthcheck-path", "", "HTTP health check path")
|
||||
f.String("cpu", "", "CPU limit (e.g. 0.5)")
|
||||
f.String("memory", "", "memory limit (e.g. 512m)")
|
||||
f.Int("port", 0, "container port")
|
||||
f.Int("replicas", 0, "replica count")
|
||||
|
||||
for _, a := range []string{"start", "stop", "restart", "redeploy"} {
|
||||
verb := a
|
||||
ServicesCmd.AddCommand(&cobra.Command{
|
||||
Use: verb + " <id>",
|
||||
Short: verb + " a service",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: serviceAction(verb),
|
||||
})
|
||||
}
|
||||
ServicesCmd.AddCommand(servicesListCmd, servicesGetCmd, servicesCreateCmd, servicesDeleteCmd)
|
||||
}
|
||||
Reference in new issue
Block a user