Skip to content

Commit d35ca5b

Browse files
committed
added /scenarios/detail endpoint
1 parent 53b950e commit d35ca5b

File tree

4 files changed

+322
-9
lines changed

4 files changed

+322
-9
lines changed

PROGRESS.md

Lines changed: 195 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
## Current Status
44

5-
**Last updated:** 2026-01-12
6-
**Current phase:** REST API Enhancement - POST /scenarios COMPLETED
5+
**Last updated:** 2026-01-13
6+
**Current phase:** REST API Enhancement - POST /scenarios/detail COMPLETED
77

88
### Completed
99
- Project scaffolding with Kubebuilder
@@ -62,6 +62,14 @@
6262
- ✅ Returns list of available krkn scenario tags with metadata
6363
- ✅ Request/response types defined in internal/api/types.go
6464
- ✅ Handler registered at POST /scenarios
65+
- **POST /scenarios/detail/{scenario_name} endpoint completed:**
66+
- ✅ Extracts scenario_name from URL path
67+
- ✅ Reuses krknctl models.ScenarioDetail (no custom DTOs)
68+
- ✅ Same registry configuration pattern as /scenarios
69+
- ✅ Calls GetScenarioDetail(scenario_name, registry)
70+
- ✅ Returns detailed scenario information (title, description, input fields)
71+
- ✅ Returns 404 if scenario not found
72+
- ✅ Handler registered at POST /scenarios/detail/{scenario_name}
6573

6674
### In Progress
6775
- None
@@ -305,6 +313,139 @@
305313
- Supports multiple authentication methods: username/password, token
306314
- Optional TLS skip and insecure connection for private registries
307315

316+
#### POST /scenarios/detail/{scenario_name}
317+
**Purpose:** Retrieve detailed information about a specific chaos scenario
318+
319+
**Path Parameter:**
320+
- `scenario_name` (required): The name/tag of the scenario to retrieve
321+
322+
**Request Body (optional):**
323+
Same as POST /scenarios - registry configuration for private registry
324+
```json
325+
{
326+
"registryUrl": "registry.example.com",
327+
"scenarioRepository": "org/krkn-scenarios",
328+
"username": "user",
329+
"password": "pass",
330+
"token": "alternative-to-user-pass",
331+
"skipTls": false,
332+
"insecure": false
333+
}
334+
```
335+
336+
**Behavior:**
337+
1. Extract scenario_name from URL path
338+
2. Parse optional request body for registry configuration
339+
3. If body contains registry configuration → use RegistryV2 provider (Private mode)
340+
4. If no body or empty body → use Quay provider (default to quay.io)
341+
5. Load krknctl configuration (embedded config.json)
342+
6. Create provider factory and instantiate appropriate provider
343+
7. Call `GetScenarioDetail(scenario_name, registry)` to fetch scenario details
344+
8. Return scenario detail with input fields metadata
345+
9. Return 404 if scenario not found
346+
347+
**Status:** COMPLETED
348+
349+
**Response Format (200 OK):**
350+
```json
351+
{
352+
"name": "pod-scenarios",
353+
"digest": "sha256:abc123...",
354+
"size": 123456789,
355+
"lastModified": "2025-01-13T10:30:00Z",
356+
"title": "Pod Scenarios",
357+
"description": "Chaos engineering scenarios for Kubernetes pods",
358+
"fields": [
359+
{
360+
"name": "namespace",
361+
"variable": "NAMESPACE",
362+
"type": "string",
363+
"description": "Target namespace for pod scenarios",
364+
"required": true,
365+
"default": "default"
366+
},
367+
{
368+
"name": "label_selector",
369+
"variable": "LABEL_SELECTOR",
370+
"type": "string",
371+
"description": "Label selector to filter pods",
372+
"required": false
373+
},
374+
{
375+
"name": "config_file",
376+
"variable": "CONFIG_FILE",
377+
"type": "file",
378+
"description": "Configuration file for scenario",
379+
"required": false,
380+
"mount_path": "/config/scenario.yaml"
381+
}
382+
]
383+
}
384+
```
385+
386+
**Error Responses:**
387+
388+
400 Bad Request (missing scenario_name):
389+
```json
390+
{
391+
"error": "bad_request",
392+
"message": "scenario_name parameter is required in path"
393+
}
394+
```
395+
396+
400 Bad Request (invalid body):
397+
```json
398+
{
399+
"error": "bad_request",
400+
"message": "Invalid request body: ..."
401+
}
402+
```
403+
404+
400 Bad Request (partial registry info):
405+
```json
406+
{
407+
"error": "bad_request",
408+
"message": "Both registryUrl and scenarioRepository are required for private registry"
409+
}
410+
```
411+
412+
404 Not Found (scenario not found):
413+
```json
414+
{
415+
"error": "not_found",
416+
"message": "Scenario 'invalid-scenario' not found"
417+
}
418+
```
419+
420+
500 Internal Server Error:
421+
```json
422+
{
423+
"error": "internal_error",
424+
"message": "Failed to get scenario detail: ..."
425+
}
426+
```
427+
428+
**Implementation Details:**
429+
- Uses `github.com/krkn-chaos/krknctl/pkg/provider` package
430+
- Reuses krknctl `models.ScenarioDetail` structure (no custom DTOs)
431+
- Factory pattern: `factory.NewProviderFactory(config).NewInstance(mode)`
432+
- Same registry configuration pattern as POST /scenarios
433+
- Two modes:
434+
- `provider.Quay`: Default quay.io registry
435+
- `provider.Private`: Custom registry with RegistryV2 configuration
436+
- Returns complete scenario metadata including:
437+
- Basic info: name, digest, size, lastModified
438+
- Descriptive info: title, description
439+
- Input fields: detailed field configurations with types, validation, defaults
440+
441+
**Input Field Types:**
442+
- `string`: Text input with optional regex validation
443+
- `number`: Numeric input
444+
- `boolean`: Boolean flag (true/false)
445+
- `enum`: Enumerated values with allowed_values list
446+
- `file`: File mount (requires mount_path)
447+
- `file_base64`: Base64-encoded file content
448+
308449
## Architecture Decisions
309450

310451
### REST API Framework
@@ -543,6 +684,54 @@
543684
- **Authentication**: Supports username/password or token
544685
- **Flexibility**: SkipTLS and Insecure options for development environments
545686

687+
### Phase 10: POST /scenarios/detail/{scenario_name} Endpoint ✅
688+
**Status:** COMPLETED
689+
690+
1. ✅ Architecture Decision
691+
- Decided to reuse krknctl `models.ScenarioDetail` structure
692+
- No custom DTOs created - direct use of upstream models
693+
- Maintains consistency with krknctl ecosystem
694+
695+
2. ✅ Handler Implementation (internal/api/handlers.go)
696+
- Implemented `PostScenarioDetail(w, r)` handler
697+
- Path parameter extraction for scenario_name
698+
- Same registry configuration pattern as POST /scenarios
699+
- Request body parsing with ContentLength check
700+
- Validation logic: both registryUrl and scenarioRepository required for private registry
701+
- Mode selection: provider.Quay (default) vs provider.Private
702+
- Factory pattern: `factory.NewProviderFactory(&cfg).NewInstance(mode)`
703+
- Call to `GetScenarioDetail(scenario_name, registry)` for detailed scenario retrieval
704+
- 404 response when scenario not found
705+
- Direct JSON marshaling of krknctl models.ScenarioDetail
706+
- Comprehensive error handling (400, 404, 500)
707+
708+
3. ✅ Route Registration (internal/api/server.go)
709+
- Registered POST /scenarios/detail/{scenario_name} route
710+
- Handler accessible at http://operator:8080/scenarios/detail/{scenario_name}
711+
712+
4. ✅ Build Verification
713+
- Successful compilation with no errors
714+
- All dependencies resolved
715+
- Binary built and ready for testing
716+
717+
5. ✅ Documentation
718+
- Updated REQUIREMENTS.md with ✅ COMPLETED marker
719+
- Added complete endpoint documentation to PROGRESS.md
720+
- Documented all request/response formats
721+
- Documented all input field types (string, number, boolean, enum, file, file_base64)
722+
723+
**Implementation Highlights:**
724+
- **Reuses upstream models**: No DTOs, direct krknctl models.ScenarioDetail
725+
- **Consistent pattern**: Same registry config as POST /scenarios
726+
- **Rich metadata**: Returns title, description, and complete field configurations
727+
- **Field metadata includes**:
728+
- Field type and validation rules
729+
- Required/optional flags
730+
- Default values
731+
- Mount paths for file types
732+
- Dependencies and mutual exclusions
733+
- Secret field marking
734+
546735
## Technical Notes
547736

548737
### KrknTargetRequest Structure
@@ -635,9 +824,10 @@ type KrknTargetRequestStatus struct {
635824
7. ✅ Podman support and container tooling
636825
8. ✅ Kubernetes Service for REST API access
637826
9. ✅ Implement POST /scenarios endpoint with krknctl integration
638-
10. Create API documentation (OpenAPI/Swagger spec)
639-
11. Add additional endpoints as requirements evolve
640-
12. Implement controller logic for KrknTargetRequest
827+
10. ✅ Implement POST /scenarios/detail/{scenario_name} endpoint
828+
11. Create API documentation (OpenAPI/Swagger spec)
829+
12. Add additional endpoints as requirements evolve
830+
13. Implement controller logic for KrknTargetRequest
641831

642832
## Future Work (Not in Current Scope)
643833

REQUIREMENTS.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,16 @@ container in the same pod.
5454
- the method must instantiate the factory of the scenario providers based on the user parameters
5555
- - if the payload contains private registry infos `RegistryV2` provider must be instantiated
5656
- - if no payload is passed it must default on quay.io
57-
- the purpose is to return the list of the available krkn scenarioag
57+
- the purpose is to return the list of the available krkn scenarios
5858

59+
### /scenarios/detail/{scenario_name} ✅ COMPLETED
60+
- this method must be built using the already available golang package made for krknctl to retrieve
61+
the available krkn scenarios either from quay.io or from a private registry
62+
- the package is on the following repo https://github.com/krkn-chaos/krknctl/tree/main/pkg/provider
63+
- the method must instantiate the factory of the scenario providers based on the user parameters
64+
- - if the payload contains private registry infos `RegistryV2` provider must be instantiated
65+
- - if no payload is passed it must default on quay.io
66+
- the purpose is to return the detail of the scenario in json format using the `GetScenarioDetail` method or 404 if not found
5967

6068
# Grpc python service requirement
6169

internal/api/handlers.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,3 +475,117 @@ func (h *Handler) PostScenarios(w http.ResponseWriter, r *http.Request) {
475475

476476
writeJSON(w, http.StatusOK, response)
477477
}
478+
479+
// PostScenarioDetail handles POST /scenarios/detail/{scenario_name} endpoint
480+
// It returns detailed information about a specific scenario including input fields
481+
func (h *Handler) PostScenarioDetail(w http.ResponseWriter, r *http.Request) {
482+
// Extract scenario_name from path: /scenarios/detail/{scenario_name}
483+
path := r.URL.Path
484+
prefix := "/scenarios/detail/"
485+
486+
if len(path) <= len(prefix) {
487+
writeJSONError(w, http.StatusBadRequest, ErrorResponse{
488+
Error: "bad_request",
489+
Message: "scenario_name parameter is required in path",
490+
})
491+
return
492+
}
493+
494+
scenarioName := path[len(prefix):]
495+
if scenarioName == "" {
496+
writeJSONError(w, http.StatusBadRequest, ErrorResponse{
497+
Error: "bad_request",
498+
Message: "scenario_name parameter cannot be empty",
499+
})
500+
return
501+
}
502+
503+
// Parse optional request body (same as /scenarios for registry config)
504+
var req ScenariosRequest
505+
var registry *models.RegistryV2
506+
var mode provider.Mode
507+
508+
// Check if body is provided
509+
if r.ContentLength > 0 {
510+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
511+
writeJSONError(w, http.StatusBadRequest, ErrorResponse{
512+
Error: "bad_request",
513+
Message: "Invalid request body: " + err.Error(),
514+
})
515+
return
516+
}
517+
518+
// If registry info is provided, validate and use private registry mode
519+
if req.RegistryURL != "" && req.ScenarioRepository != "" {
520+
registry = &models.RegistryV2{
521+
Username: req.Username,
522+
Password: req.Password,
523+
Token: req.Token,
524+
RegistryURL: req.RegistryURL,
525+
ScenarioRepository: req.ScenarioRepository,
526+
SkipTLS: req.SkipTLS,
527+
Insecure: req.Insecure,
528+
}
529+
mode = provider.Private
530+
} else if req.RegistryURL != "" || req.ScenarioRepository != "" {
531+
// Partial registry info provided - error
532+
writeJSONError(w, http.StatusBadRequest, ErrorResponse{
533+
Error: "bad_request",
534+
Message: "Both registryUrl and scenarioRepository are required for private registry",
535+
})
536+
return
537+
} else {
538+
// Body provided but no registry info - use quay.io
539+
mode = provider.Quay
540+
}
541+
} else {
542+
// No body provided - default to quay.io
543+
mode = provider.Quay
544+
}
545+
546+
// Load krknctl config
547+
cfg, err := config.LoadConfig()
548+
if err != nil {
549+
writeJSONError(w, http.StatusInternalServerError, ErrorResponse{
550+
Error: "internal_error",
551+
Message: "Failed to load krknctl config: " + err.Error(),
552+
})
553+
return
554+
}
555+
556+
// Create provider factory
557+
providerFactory := factory.NewProviderFactory(&cfg)
558+
559+
// Get provider instance based on mode
560+
scenarioProvider := providerFactory.NewInstance(mode)
561+
if scenarioProvider == nil {
562+
writeJSONError(w, http.StatusInternalServerError, ErrorResponse{
563+
Error: "internal_error",
564+
Message: "Failed to create scenario provider",
565+
})
566+
return
567+
}
568+
569+
// Get scenario detail
570+
scenarioDetail, err := scenarioProvider.GetScenarioDetail(scenarioName, registry)
571+
if err != nil {
572+
writeJSONError(w, http.StatusInternalServerError, ErrorResponse{
573+
Error: "internal_error",
574+
Message: "Failed to get scenario detail: " + err.Error(),
575+
})
576+
return
577+
}
578+
579+
// Check if scenario was found
580+
if scenarioDetail == nil {
581+
writeJSONError(w, http.StatusNotFound, ErrorResponse{
582+
Error: "not_found",
583+
Message: "Scenario '" + scenarioName + "' not found",
584+
})
585+
return
586+
}
587+
588+
// Return the scenario detail directly (krknctl models.ScenarioDetail)
589+
// It will be JSON-marshaled automatically
590+
writeJSON(w, http.StatusOK, scenarioDetail)
591+
}

internal/api/server.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ func NewServer(port int, client client.Client, namespace string, grpcServerAddr
4242
mux.HandleFunc("/health", handler.HealthCheck)
4343
mux.HandleFunc("/clusters", handler.GetClusters)
4444
mux.HandleFunc("/nodes", handler.GetNodes)
45-
mux.HandleFunc("/targets", handler.PostTarget) // POST /targets
46-
mux.HandleFunc("/targets/", handler.GetTargetByUUID) // GET /targets/{uuid}
47-
mux.HandleFunc("/scenarios", handler.PostScenarios) // POST /scenarios
45+
mux.HandleFunc("/targets", handler.PostTarget) // POST /targets
46+
mux.HandleFunc("/targets/", handler.GetTargetByUUID) // GET /targets/{uuid}
47+
mux.HandleFunc("/scenarios", handler.PostScenarios) // POST /scenarios
48+
mux.HandleFunc("/scenarios/detail/", handler.PostScenarioDetail) // POST /scenarios/detail/{scenario_name}
4849

4950
// Wrap mux with logging middleware
5051
server := &http.Server{

0 commit comments

Comments
 (0)