Skip to content

Commit b4f4100

Browse files
feat: improve error handling and retry logic across services
- Added user-friendly prompt for uninitialized Restic repository with guided initialization flow - Enhanced UnifiedClient with Retry-After header support for smarter rate limit handling - Improved error classification to prevent retries on deterministic failures (400, 401, 403, 404) - Added comprehensive dotenv file handling with atomic writes and proper permissions - Updated backup client to detect and handle repository initialization errors
1 parent c41c747 commit b4f4100

20 files changed

Lines changed: 8585 additions & 23 deletions

cmd/backup/quick.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ Restore:
120120
return eos_err.NewExpectedError(rc.Ctx, userErr)
121121
}
122122

123+
if errors.Is(err, backup.ErrRepositoryNotInitialized) {
124+
logger.Info("terminal prompt:", zap.String("output",
125+
"Restic repository is not initialized. Initialize it (e.g., eos backup create repository local --path /var/lib/eos/backups) and rerun the command."))
126+
return eos_err.NewExpectedError(rc.Ctx, err)
127+
}
128+
123129
logger.Error("Backup failed", zap.Error(err), zap.String("output", string(output)))
124130
return fmt.Errorf("backup failed: %w", err)
125131
}

cmd/list/authentik_api.go

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// cmd/list/authentik_api.go
2+
// List Authentik resources using declarative API client framework
3+
//
4+
// ARCHITECTURE: Thin orchestration layer - delegates to pkg/apiclient/executor.go
5+
// HUMAN-CENTRIC: Plain language CLI → REST API translation
6+
// COMPLIANCE: Follows CLAUDE.md P0 rules (structured logging, RuntimeContext, error handling)
7+
// NOTE: Complements existing authentik.go (flows/bindings) - this handles users/groups/applications
8+
9+
package list
10+
11+
import (
12+
"fmt"
13+
14+
"github.com/CodeMonkeyCybersecurity/eos/pkg/apiclient"
15+
eos "github.com/CodeMonkeyCybersecurity/eos/pkg/eos_cli"
16+
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_io"
17+
"github.com/spf13/cobra"
18+
"github.com/uptrace/opentelemetry-go-extra/otelzap"
19+
"go.uber.org/zap"
20+
)
21+
22+
var authentikAPICmd = &cobra.Command{
23+
Use: "authentik-api [resource]",
24+
Short: "List Authentik API resources (users, groups, applications)",
25+
Long: `List Authentik API resources using the declarative API client framework.
26+
27+
Available resources:
28+
users - List Authentik users
29+
groups - List Authentik groups
30+
applications - List Authentik applications
31+
providers - List Authentik proxy providers
32+
brands - List Authentik brands
33+
34+
Examples:
35+
# List all users
36+
eos list authentik-api users
37+
38+
# List external users only
39+
eos list authentik-api users --type=external
40+
41+
# List active superusers
42+
eos list authentik-api users --superuser --active
43+
44+
# List groups containing specific user
45+
eos list authentik-api groups --member=123e4567-e89b-12d3-a456-426614174000
46+
47+
# List applications
48+
eos list authentik-api applications
49+
50+
# Output as JSON
51+
eos list authentik-api users --format=json
52+
53+
# Output as CSV (for spreadsheets)
54+
eos list authentik-api users --format=csv
55+
56+
NOTE: For flow and stage binding inspection, use 'eos list authentik flows' instead.`,
57+
RunE: eos.Wrap(func(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string) error {
58+
logger := otelzap.Ctx(rc.Ctx)
59+
60+
// Require resource argument
61+
if len(args) < 1 {
62+
return fmt.Errorf("resource type required\n\n" +
63+
"Available resources:\n" +
64+
" users - Authentik users\n" +
65+
" groups - Authentik groups\n" +
66+
" applications - Authentik applications\n" +
67+
" providers - Authentik proxy providers\n" +
68+
" brands - Authentik brands\n\n" +
69+
"Examples:\n" +
70+
" eos list authentik-api users\n" +
71+
" eos list authentik-api groups\n" +
72+
" eos list authentik-api applications")
73+
}
74+
75+
resource := args[0]
76+
77+
logger.Info("Listing Authentik API resources",
78+
zap.String("resource", resource))
79+
80+
// Create executor (loads API definition, discovers auth)
81+
executor, err := apiclient.NewExecutor(rc, "authentik")
82+
if err != nil {
83+
return fmt.Errorf("failed to initialize Authentik API client: %w\n\n"+
84+
"Troubleshooting:\n"+
85+
" 1. Ensure Authentik is configured in /opt/hecate/.env\n"+
86+
" 2. Check AUTHENTIK_TOKEN and AUTHENTIK_URL are set\n"+
87+
" 3. Verify Authentik is running: curl https://localhost/api/v3/\n"+
88+
" 4. Run: eos debug hecate", err)
89+
}
90+
91+
// Extract filters from flags based on resource type
92+
filters := make(map[string]interface{})
93+
94+
switch resource {
95+
case "users":
96+
// Users filters
97+
if cmd.Flags().Changed("superuser") {
98+
superuser, _ := cmd.Flags().GetBool("superuser")
99+
filters["is_superuser"] = superuser
100+
}
101+
if cmd.Flags().Changed("active") {
102+
active, _ := cmd.Flags().GetBool("active")
103+
filters["is_active"] = active
104+
}
105+
if cmd.Flags().Changed("type") {
106+
userType, _ := cmd.Flags().GetString("type")
107+
filters["type"] = userType
108+
}
109+
if cmd.Flags().Changed("email") {
110+
email, _ := cmd.Flags().GetString("email")
111+
filters["email"] = email
112+
}
113+
if cmd.Flags().Changed("username") {
114+
username, _ := cmd.Flags().GetString("username")
115+
filters["username"] = username
116+
}
117+
118+
case "groups":
119+
// Groups filters
120+
if cmd.Flags().Changed("member") {
121+
member, _ := cmd.Flags().GetString("member")
122+
filters["members_by_pk"] = member
123+
}
124+
if cmd.Flags().Changed("name") {
125+
name, _ := cmd.Flags().GetString("name")
126+
filters["name"] = name
127+
}
128+
129+
case "applications":
130+
// Applications filters
131+
if cmd.Flags().Changed("name") {
132+
name, _ := cmd.Flags().GetString("name")
133+
filters["name"] = name
134+
}
135+
if cmd.Flags().Changed("slug") {
136+
slug, _ := cmd.Flags().GetString("slug")
137+
filters["slug"] = slug
138+
}
139+
140+
case "providers":
141+
// Providers filters
142+
if cmd.Flags().Changed("name") {
143+
name, _ := cmd.Flags().GetString("name")
144+
filters["name"] = name
145+
}
146+
147+
case "brands":
148+
// Brands filters
149+
if cmd.Flags().Changed("domain") {
150+
domain, _ := cmd.Flags().GetString("domain")
151+
filters["domain"] = domain
152+
}
153+
}
154+
155+
logger.Debug("Extracted filters from flags",
156+
zap.String("resource", resource),
157+
zap.Any("filters", filters))
158+
159+
// Execute list operation
160+
result, err := executor.List(rc.Ctx, resource, filters)
161+
if err != nil {
162+
return fmt.Errorf("failed to list %s: %w\n\n"+
163+
"Troubleshooting:\n"+
164+
" 1. Check if resource name is correct (users, groups, applications, etc.)\n"+
165+
" 2. Verify Authentik API token has read permissions\n"+
166+
" 3. Check Authentik API logs for errors\n"+
167+
" 4. Run: eos debug hecate",
168+
resource, err)
169+
}
170+
171+
logger.Info("Retrieved resources",
172+
zap.String("resource", resource),
173+
zap.Int("count", len(result.Items)),
174+
zap.Int("total", result.TotalCount))
175+
176+
// Format and display output
177+
format, _ := cmd.Flags().GetString("format")
178+
if err := apiclient.FormatOutput(result, format); err != nil {
179+
return fmt.Errorf("failed to format output: %w", err)
180+
}
181+
182+
return nil
183+
}),
184+
}
185+
186+
func init() {
187+
// ─────────────────────────────────────────────────────────────────────────
188+
// Standard flags (all resources)
189+
// ─────────────────────────────────────────────────────────────────────────
190+
authentikAPICmd.Flags().String("format", "table", "Output format (table, json, yaml, csv)")
191+
192+
// ─────────────────────────────────────────────────────────────────────────
193+
// Users filters
194+
// ─────────────────────────────────────────────────────────────────────────
195+
authentikAPICmd.Flags().Bool("superuser", false, "Filter by superuser status")
196+
authentikAPICmd.Flags().Bool("active", false, "Filter by active status")
197+
authentikAPICmd.Flags().String("type", "", "Filter by user type (internal, external, service_account)")
198+
authentikAPICmd.Flags().String("email", "", "Filter by email address")
199+
authentikAPICmd.Flags().String("username", "", "Filter by username")
200+
201+
// ─────────────────────────────────────────────────────────────────────────
202+
// Groups filters
203+
// ─────────────────────────────────────────────────────────────────────────
204+
authentikAPICmd.Flags().String("member", "", "Filter groups by member UUID")
205+
206+
// ─────────────────────────────────────────────────────────────────────────
207+
// Common filters (name, slug, domain)
208+
// ─────────────────────────────────────────────────────────────────────────
209+
authentikAPICmd.Flags().String("name", "", "Filter by name")
210+
authentikAPICmd.Flags().String("slug", "", "Filter by slug")
211+
authentikAPICmd.Flags().String("domain", "", "Filter by domain (brands only)")
212+
213+
// Register command
214+
ListCmd.AddCommand(authentikAPICmd)
215+
}

0 commit comments

Comments
 (0)