feat(sdk): Add username/password authentication matching existing client

Implemented dynamic token authentication using existing RetrieveToken pattern:

## Authentication Enhancements:
- **UsernamePasswordProvider**: Implements existing `POST /api/v1/login` flow
- **Token Caching**: 1-hour cache with thread-safe refresh logic
- **NewClientWithCredentials()**: Convenience constructor for username/password auth
- **Dual Auth Support**: Both static token and dynamic username/password flows

## Key Features:
- **Exact API Match**: Mirrors existing `client/client.go RetrieveToken()` implementation
- **Thread Safety**: Concurrent token refresh with mutex protection
- **Caching Strategy**: Reduces login calls, configurable expiry
- **Error Handling**: Structured login failures with context
- **Token Invalidation**: Manual cache clearing for token refresh

## Implementation Details:
```go
// Static token (existing)
client := client.NewClient(baseURL,
  client.WithAuthProvider(client.NewStaticTokenProvider(token)))

// Username/password (new - matches existing pattern)
client := client.NewClientWithCredentials(baseURL, username, password)
```

## Testing:
- **Comprehensive Auth Tests**: Login success/failure, caching, expiry
- **Mock Server Tests**: httptest-based token flow validation
- **Concurrent Safety**: Token refresh under concurrent access
- **Updated Examples**: Support both auth methods

## Backward Compatibility:
- Existing StaticTokenProvider unchanged
- All existing APIs maintain same signatures
- Example updated to support both auth methods via environment variables

This matches the existing prototype's authentication exactly while adding
production features like caching and thread safety.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Waldemar 2025-09-25 14:21:31 +02:00
parent 9a06c608b2
commit e6de69551e
4 changed files with 412 additions and 17 deletions

View file

@ -16,20 +16,34 @@ import (
func main() {
// Configure SDK client
baseURL := getEnvOrDefault("EDGEXR_BASE_URL", "https://hub.apps.edge.platform.mg3.mdb.osc.live/api/v1")
baseURL := getEnvOrDefault("EDGEXR_BASE_URL", "https://hub.apps.edge.platform.mg3.mdb.osc.live")
// Support both token-based and username/password authentication
token := getEnvOrDefault("EDGEXR_TOKEN", "")
username := getEnvOrDefault("EDGEXR_USERNAME", "")
password := getEnvOrDefault("EDGEXR_PASSWORD", "")
if token == "" {
log.Fatal("EDGEXR_TOKEN environment variable is required")
var edgeClient *client.Client
if token != "" {
// Use static token authentication
fmt.Println("🔐 Using Bearer token authentication")
edgeClient = client.NewClient(baseURL,
client.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
client.WithAuthProvider(client.NewStaticTokenProvider(token)),
client.WithLogger(log.Default()),
)
} else if username != "" && password != "" {
// Use username/password authentication (matches existing client pattern)
fmt.Println("🔐 Using username/password authentication")
edgeClient = client.NewClientWithCredentials(baseURL, username, password,
client.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
client.WithLogger(log.Default()),
)
} else {
log.Fatal("Authentication required: Set either EDGEXR_TOKEN or both EDGEXR_USERNAME and EDGEXR_PASSWORD")
}
// Create SDK client with authentication and logging
client := client.NewClient(baseURL,
client.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
client.WithAuthProvider(client.NewStaticTokenProvider(token)),
client.WithLogger(log.Default()),
)
ctx := context.Background()
// Example application to deploy
@ -49,14 +63,14 @@ func main() {
}
// Demonstrate app lifecycle
if err := demonstrateAppLifecycle(ctx, client, app); err != nil {
if err := demonstrateAppLifecycle(ctx, edgeClient, app); err != nil {
log.Fatalf("App lifecycle demonstration failed: %v", err)
}
fmt.Println("✅ SDK example completed successfully!")
}
func demonstrateAppLifecycle(ctx context.Context, c *client.Client, input *client.NewAppInput) error {
func demonstrateAppLifecycle(ctx context.Context, edgeClient *client.Client, input *client.NewAppInput) error {
appKey := input.App.Key
region := input.Region
@ -65,14 +79,14 @@ func demonstrateAppLifecycle(ctx context.Context, c *client.Client, input *clien
// Step 1: Create the application
fmt.Println("\n1. Creating application...")
if err := c.CreateApp(ctx, input); err != nil {
if err := edgeClient.CreateApp(ctx, input); err != nil {
return fmt.Errorf("failed to create app: %w", err)
}
fmt.Printf("✅ App created: %s/%s v%s\n", appKey.Organization, appKey.Name, appKey.Version)
// Step 2: Query the application
fmt.Println("\n2. Querying application...")
app, err := c.ShowApp(ctx, appKey, region)
app, err := edgeClient.ShowApp(ctx, appKey, region)
if err != nil {
return fmt.Errorf("failed to show app: %w", err)
}
@ -82,7 +96,7 @@ func demonstrateAppLifecycle(ctx context.Context, c *client.Client, input *clien
// Step 3: List applications in the organization
fmt.Println("\n3. Listing applications...")
filter := client.AppKey{Organization: appKey.Organization}
apps, err := c.ShowApps(ctx, filter, region)
apps, err := edgeClient.ShowApps(ctx, filter, region)
if err != nil {
return fmt.Errorf("failed to list apps: %w", err)
}
@ -90,14 +104,14 @@ func demonstrateAppLifecycle(ctx context.Context, c *client.Client, input *clien
// Step 4: Clean up - delete the application
fmt.Println("\n4. Cleaning up...")
if err := c.DeleteApp(ctx, appKey, region); err != nil {
if err := edgeClient.DeleteApp(ctx, appKey, region); err != nil {
return fmt.Errorf("failed to delete app: %w", err)
}
fmt.Printf("✅ App deleted: %s/%s v%s\n", appKey.Organization, appKey.Name, appKey.Version)
// Step 5: Verify deletion
fmt.Println("\n5. Verifying deletion...")
_, err = c.ShowApp(ctx, appKey, region)
_, err = edgeClient.ShowApp(ctx, appKey, region)
if err != nil {
if fmt.Sprintf("%v", err) == client.ErrResourceNotFound.Error() {
fmt.Printf("✅ App successfully deleted (not found)\n")