diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f99edca..c1b0070 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -8,13 +8,13 @@ SPDX-License-Identifier: MIT ## Architecture Overview -This is a **Fabrica-generated** REST API service for managing node boot configurations in OpenCHAMI HPC clusters. It provides both modern resource-based APIs and legacy BSS (Boot Script Service) compatibility. +This is a **Fabrica-generated** REST API service for managing node boot configurations in OpenCHAMI HPC clusters. It provides both modern resource-based APIs and boot endpoints at root paths, plus optional legacy BSS (Boot Script Service) compatibility. ### Key Components 1. **Fabrica Code Generation** - Resources define the API contract, handlers/storage/client are auto-generated 2. **BootScriptController** (`pkg/controllers/bootscript/`) - Core boot logic with iPXE template generation and intelligent config matching -3. **Legacy BSS API** (`pkg/handlers/legacy/`) - Compatibility layer for legacy systems +3. **Boot API Handlers** (`pkg/handlers/boot/`) - Modern and legacy (BSS-compatible) boot endpoints 4. **Authentication** (`pkg/auth/`) - TokenSmith JWT integration with scope-based authorization 5. **Storage Backend** (`internal/storage/`) - File-based storage (database support planned) @@ -233,7 +233,7 @@ plus `TOKENSMITH_*` for bootstrap-token exchange settings. **Current Integration Path**: 1. Build an HSM client in `cmd/server/main.go` 2. Create `FlexibleBootScriptController` in `cmd/server/server_extensions.go` -3. Register legacy routes with `NewLegacyHandlerWithController(...)` +3. Register boot routes with `boot.NewHandlerWithController(...)` 4. Start optional HSM background sync when enabled **Node resolution with HSM** (when integrated): @@ -243,8 +243,8 @@ plus `TOKENSMITH_*` for bootstrap-token exchange settings. **Caching**: HSM responses are cached (default: 5 minutes) to reduce load on HSM service. -**Current Limitation**: The legacy `/boot/v1/bootscript` HTTP route ignores the -`profile` query parameter and always asks the controller to auto-resolve the +**Current Limitation**: The boot HTTP endpoints (`/bootscript` and `/boot/v1/bootscript`) +ignore the `profile` query parameter and always ask the controller to auto-resolve the best configuration across profiles. ### TokenSmith @@ -253,7 +253,9 @@ Authentication service providing JWT tokens. Configure via `auth.jwks_url` or `a ### BSS Compatibility -Legacy API at `/boot/v1/*` enabled via `enable_legacy_api: true`. Wraps modern API with BSS-compatible endpoints. +Modern boot endpoints are always available at root paths (`/bootscript`, `/bootparameters`, etc.). +Legacy BSS-compatible endpoints at `/boot/v1/*` are available when `enable_legacy_api: true`. +Both modern and legacy endpoints use the same handler logic from `pkg/handlers/boot/`. ## Container Builds @@ -285,7 +287,7 @@ GoReleaser config: `.goreleaser.yaml` (v2.4.4 compatible, no sboms). - `cmd/server/main.go` - Server entrypoint with Cobra CLI and config loading - `apis/boot.openchami.io/v1/` - Resource definitions (edit these, not generated files) - `pkg/controllers/bootscript/` - Boot logic, config matching, iPXE generation -- `pkg/handlers/legacy/` - BSS compatibility layer +- `pkg/handlers/boot/` - Boot API handlers, both modern and legacy BSS- compatible - `pkg/auth/` - TokenSmith integration and testing utilities - `config.example.yaml` - Comprehensive config documentation - `docs/AUTHENTICATION.md` - JWT integration guide diff --git a/CHANGELOG.md b/CHANGELOG.md index 92163b9..e918fe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,24 @@ Changes remain under `Unreleased` until they ship in the next tagged release. ## [Unreleased] +### Added + +- Added modern boot API endpoints at root paths + - `GET /bootscript` + - `GET/POST/PUT/DELETE /bootparameters` + - `GET /service/status` + - `GET /service/version` + +### Changed + +- **BREAKING:** Moved boot endpoints from `/boot/v1/*` to root paths. + - `/boot/v1/*` prefix now exclusively for legacy BSS compatibility when `enable_legacy_api` is `true`. +- **BREAKING:** Renamed `pkg/handlers/legacy` package to `pkg/handlers/boot` with unified handler supporting both modern and legacy routing. +- Changed `enable_legacy_api` behavior + - When `false`, only modern endpoints are available; when `true`, both modern and legacy endpoints are available. +- Updated all documentation to reflect modern endpoint paths and legacy API behavior. +- Refactored server route registration to clearly separate modern and legacy endpoint registration. + ## [0.1.7] - 2026-06-17 ### Added diff --git a/README.md b/README.md index 65c6e08..411df39 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,15 @@ SPDX-License-Identifier: MIT OpenCHAMI Boot Service is a Fabrica-generated REST API for managing node boot configuration in HPC environments. It exposes modern resource APIs for -`BMC`, `BootConfiguration`, and `Node` objects, plus a legacy BSS-compatible -surface under `/boot/v1/`. +`BMC`, `BootConfiguration`, and `Node` objects, plus boot endpoints +(`/bootscript`, `/bootparameters`, `/service/status`). Legacy BSS-compatible +endpoints are available at `/boot/v1/*` when `enable_legacy_api: true`. ## What Is In This Repo - Generated CRUD and status endpoints for `/bmcs`, `/bootconfigurations`, and `/nodes` -- Legacy boot endpoints for `/boot/v1/bootparameters`, `/boot/v1/bootscript`, and service metadata +- Modern boot endpoints at `/bootscript`, `/bootparameters`, and `/service/*` +- Legacy BSS-compatible endpoints at `/boot/v1/*` (when enabled) - Boot script generation with node matching by XName, NID, or MAC address - A reusable TokenSmith auth package plus generated AuthZ classifier scaffolding - Optional HSM-backed node resolution, including TokenSmith service-token exchange @@ -100,14 +102,38 @@ The generated API supports the current resource set: The current generated surface includes `PATCH` support for these resources. Routes are registered with trailing slashes and normalized by Chi middleware. +### Boot API Endpoints + +The boot service provides modern boot API endpoints at root paths: + +- `GET /bootscript` - Generate iPXE boot script for a node +- `GET /bootparameters` - List boot configurations +- `POST /bootparameters` - Create boot configuration +- `PUT /bootparameters` - Update boot configuration +- `DELETE /bootparameters` - Delete boot configuration +- `GET /service/status` - Service status information +- `GET /service/version` - Service version information + +These endpoints accept node identifiers (`host`, `mac`, or `nid`) and support +intelligent boot configuration matching by score and priority. + ### Legacy BSS Compatibility -When `enable_legacy_api: true`, legacy routes are available under `/boot/v1/`. +When `enable_legacy_api: true`, legacy BSS-compatible routes are available at `/boot/v1/*`: + +- `GET /boot/v1/bootscript` +- `GET /boot/v1/bootparameters` +- `POST /boot/v1/bootparameters` +- `PUT /boot/v1/bootparameters` +- `DELETE /boot/v1/bootparameters` +- `GET /boot/v1/service/status` +- `GET /boot/v1/service/version` + +When legacy API is disabled, only the modern endpoints at root paths are available. -Important current behavior: the legacy `GET /boot/v1/bootscript` handler accepts -node identifiers (`host`, `mac`, or `nid`) but ignores the `profile` query -parameter. It always asks the controller to auto-resolve the best matching boot -configuration by score and priority. +**Important:** Both modern and legacy endpoints use the same handler logic and support +the same features. The `profile` query parameter is ignored and the controller always +auto-resolves the best matching boot configuration by score and priority. ### Boot Profiles diff --git a/cmd/server/main_test.go b/cmd/server/main_test.go index 81273ea..bba0816 100644 --- a/cmd/server/main_test.go +++ b/cmd/server/main_test.go @@ -20,7 +20,7 @@ import ( "github.com/openchami/boot-service/internal/storage" bootclient "github.com/openchami/boot-service/pkg/client" - "github.com/openchami/boot-service/pkg/handlers/legacy" + "github.com/openchami/boot-service/pkg/handlers/boot" ) func newGeneratedRouterForTest(t *testing.T) http.Handler { @@ -92,22 +92,34 @@ func TestGeneratedClientWorksAgainstSlashlessCollectionPaths(t *testing.T) { func TestBootScriptEndpointAvailabilityByLegacyFlag(t *testing.T) { tests := []struct { - name string - enableLegacyAPI bool - expectedBootParams int - expectedServiceState int + name string + enableLegacyAPI bool + expectedModernBootScript int + expectedLegacyBootScript int + expectedModernBootParams int + expectedLegacyBootParams int + expectedModernService int + expectedLegacyService int }{ { - name: "LegacyDisabled_BootScriptOnly", - enableLegacyAPI: false, - expectedBootParams: http.StatusNotFound, - expectedServiceState: http.StatusNotFound, + name: "LegacyDisabled_OnlyModernRoutes", + enableLegacyAPI: false, + expectedModernBootScript: http.StatusOK, + expectedLegacyBootScript: http.StatusNotFound, + expectedModernBootParams: http.StatusOK, + expectedLegacyBootParams: http.StatusNotFound, + expectedModernService: http.StatusOK, + expectedLegacyService: http.StatusNotFound, }, { - name: "LegacyEnabled_AllLegacyRoutes", - enableLegacyAPI: true, - expectedBootParams: http.StatusOK, - expectedServiceState: http.StatusOK, + name: "LegacyEnabled_BothModernAndLegacyRoutes", + enableLegacyAPI: true, + expectedModernBootScript: http.StatusOK, + expectedLegacyBootScript: http.StatusOK, + expectedModernBootParams: http.StatusOK, + expectedLegacyBootParams: http.StatusOK, + expectedModernService: http.StatusOK, + expectedLegacyService: http.StatusOK, }, } @@ -117,34 +129,70 @@ func TestBootScriptEndpointAvailabilityByLegacyFlag(t *testing.T) { server := httptest.NewServer(router) defer server.Close() - bootScriptResp, err := http.Get(server.URL + "/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff") + // Test modern bootscript endpoint + modernBootScriptResp, err := http.Get(server.URL + "/bootscript?mac=aa:bb:cc:dd:ee:ff") if err != nil { - t.Fatalf("GET bootscript failed: %v", err) + t.Fatalf("GET modern bootscript failed: %v", err) } - defer bootScriptResp.Body.Close() //nolint:errcheck + defer modernBootScriptResp.Body.Close() //nolint:errcheck - if bootScriptResp.StatusCode != http.StatusOK { - t.Fatalf("GET /boot/v1/bootscript returned %d, want %d", bootScriptResp.StatusCode, http.StatusOK) + if modernBootScriptResp.StatusCode != tc.expectedModernBootScript { + t.Errorf("GET /bootscript returned %d, want %d", modernBootScriptResp.StatusCode, tc.expectedModernBootScript) } - bootParamsResp, err := http.Get(server.URL + "/boot/v1/bootparameters") + // Test legacy bootscript endpoint + legacyBootScriptResp, err := http.Get(server.URL + "/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff") if err != nil { - t.Fatalf("GET bootparameters failed: %v", err) + t.Fatalf("GET legacy bootscript failed: %v", err) } - defer bootParamsResp.Body.Close() //nolint:errcheck + defer legacyBootScriptResp.Body.Close() //nolint:errcheck - if bootParamsResp.StatusCode != tc.expectedBootParams { - t.Fatalf("GET /boot/v1/bootparameters returned %d, want %d", bootParamsResp.StatusCode, tc.expectedBootParams) + if legacyBootScriptResp.StatusCode != tc.expectedLegacyBootScript { + t.Errorf("GET /boot/v1/bootscript returned %d, want %d", legacyBootScriptResp.StatusCode, tc.expectedLegacyBootScript) } - serviceResp, err := http.Get(server.URL + "/boot/v1/service/status") + // Test modern bootparameters endpoint + modernBootParamsResp, err := http.Get(server.URL + "/bootparameters") if err != nil { - t.Fatalf("GET service status failed: %v", err) + t.Fatalf("GET modern bootparameters failed: %v", err) } - defer serviceResp.Body.Close() //nolint:errcheck + defer modernBootParamsResp.Body.Close() //nolint:errcheck - if serviceResp.StatusCode != tc.expectedServiceState { - t.Fatalf("GET /boot/v1/service/status returned %d, want %d", serviceResp.StatusCode, tc.expectedServiceState) + if modernBootParamsResp.StatusCode != tc.expectedModernBootParams { + t.Errorf("GET /bootparameters returned %d, want %d", modernBootParamsResp.StatusCode, tc.expectedModernBootParams) + } + + // Test legacy bootparameters endpoint + legacyBootParamsResp, err := http.Get(server.URL + "/boot/v1/bootparameters") + if err != nil { + t.Fatalf("GET legacy bootparameters failed: %v", err) + } + defer legacyBootParamsResp.Body.Close() //nolint:errcheck + + if legacyBootParamsResp.StatusCode != tc.expectedLegacyBootParams { + t.Errorf("GET /boot/v1/bootparameters returned %d, want %d", legacyBootParamsResp.StatusCode, tc.expectedLegacyBootParams) + } + + // Test modern service status endpoint + modernServiceResp, err := http.Get(server.URL + "/service/status") + if err != nil { + t.Fatalf("GET modern service status failed: %v", err) + } + defer modernServiceResp.Body.Close() //nolint:errcheck + + if modernServiceResp.StatusCode != tc.expectedModernService { + t.Errorf("GET /service/status returned %d, want %d", modernServiceResp.StatusCode, tc.expectedModernService) + } + + // Test legacy service status endpoint + legacyServiceResp, err := http.Get(server.URL + "/boot/v1/service/status") + if err != nil { + t.Fatalf("GET legacy service status failed: %v", err) + } + defer legacyServiceResp.Body.Close() //nolint:errcheck + + if legacyServiceResp.StatusCode != tc.expectedLegacyService { + t.Errorf("GET /boot/v1/service/status returned %d, want %d", legacyServiceResp.StatusCode, tc.expectedLegacyService) } }) } @@ -183,11 +231,14 @@ func newRouterWithLegacyModeForTest(t *testing.T, enableLegacyAPI bool) http.Han r.Use(middleware.RedirectSlashes) RegisterGeneratedRoutes(r) - legacyHandler := legacy.NewLegacyHandler(*bootClient, log.New(io.Discard, "", 0)) + bootHandler := boot.NewHandler(*bootClient, log.New(io.Discard, "", 0)) + + // Always register modern routes + bootHandler.RegisterModernRoutes(r) + + // Conditionally register legacy routes if enableLegacyAPI { - legacyHandler.RegisterRoutes(r) - } else { - legacyHandler.RegisterBootScriptRoute(r) + bootHandler.RegisterLegacyRoutes(r) } return r diff --git a/cmd/server/server_extensions.go b/cmd/server/server_extensions.go index ea3e5b0..aa9ed14 100644 --- a/cmd/server/server_extensions.go +++ b/cmd/server/server_extensions.go @@ -17,7 +17,7 @@ import ( "github.com/openchami/boot-service/pkg/client" "github.com/openchami/boot-service/pkg/clients/hsm" "github.com/openchami/boot-service/pkg/controllers/bootscript" - "github.com/openchami/boot-service/pkg/handlers/legacy" + "github.com/openchami/boot-service/pkg/handlers/boot" ) // registerCustomServerIntegrations keeps generated route wiring and legacy compatibility @@ -37,9 +37,9 @@ func registerCustomServerIntegrations(r chi.Router, config Config, hsmClient *hs return fmt.Errorf("failed to create boot script API client: %v", err) } - logger := log.New(os.Stdout, "legacy: ", log.LstdFlags) + logger := log.New(os.Stdout, "boot: ", log.LstdFlags) - var legacyHandler *legacy.LegacyHandler + var bootHandler *boot.Handler if hsmClient != nil { // Use FlexibleBootScriptController with HSM provider. @@ -67,24 +67,27 @@ func registerCustomServerIntegrations(r chi.Router, config Config, hsmClient *hs log.Printf("HSM background sync enabled (interval: %d minutes)", config.HSMSyncInterval) } - legacyHandler = legacy.NewLegacyHandlerWithController(*bootClient, flexController, logger) + bootHandler = boot.NewHandlerWithController(*bootClient, flexController, logger) } else { // Use standard controller with local storage. - legacyHandler = legacy.NewLegacyHandler(*bootClient, logger) + bootHandler = boot.NewHandler(*bootClient, logger) } - // Register node bootscript endpoint always; keep additional legacy BSS - // compatibility endpoints behind enable_legacy_api. + // Always register "modern" boot API paths at /. + bootHandler.RegisterModernRoutes(r) + + // Only register legacy BSS-compatible API if enable_legacy_api is true. + // These live at /boot/v1/*. if config.EnableLegacyAPI { - legacyHandler.RegisterRoutes(r) + bootHandler.RegisterLegacyRoutes(r) if hsmClient != nil { - log.Println("Legacy BSS API enabled with HSM integration at: /boot/v1/") + log.Println("Legacy BSS API enabled with HSM integration at: /boot/v1/*") } else { - log.Println("Legacy BSS API enabled at: /boot/v1/") + log.Println("Legacy BSS API enabled at: /boot/v1/*") } + log.Println("Note: Both modern and legacy endpoints are available for BSS compatibility") } else { - legacyHandler.RegisterBootScriptRoute(r) - log.Println("Boot script endpoint enabled at: /boot/v1/bootscript") + log.Println("Legacy BSS API disabled (set enable_legacy_api to true to enable /boot/v1/* endpoints)") } return nil diff --git a/config.example.yaml b/config.example.yaml index 246a377..6619b3a 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -47,7 +47,9 @@ storage_type: "file" enable_auth: false # Enables Prometheus metrics endpoints. enable_metrics: false -# Controls legacy BSS-compatible endpoints other than /boot/v1/bootscript. +# Controls legacy BSS-compatible endpoints at /boot/v1/*. +# When false, only modern endpoints at root paths are available. +# When true, both modern and legacy endpoints are available. enable_legacy_api: true # Metrics listener port used when enable_metrics is true. metrics_port: 9090 @@ -92,8 +94,8 @@ hsm_sync_interval: 5 # active runtime auth path in the server entrypoint. # jwks_endpoint: "https://auth.example.com/.well-known/jwks.json" -# - The current server always exposes /boot/v1/bootscript. -# - enable_legacy_api controls the rest of the legacy BSS-compatible endpoints. +# - Boot endpoints are always available at root paths (e.g. /bootscript). +# - enable_legacy_api controls legacy BSS-compatible endpoints at /boot/v1/* # - enable_auth currently affects startup validation and HSM token exchange. # - The standalone server does not currently attach pkg/auth request middleware # to its route tree in cmd/server/main.go. diff --git a/docs/API.md b/docs/API.md index 11a5c8f..30986ec 100644 --- a/docs/API.md +++ b/docs/API.md @@ -45,15 +45,45 @@ For each resource type, the generated routes currently support: The generated router registers trailing-slash routes and the server applies Chi slash normalization so both slashless and slashful collection paths work. -## Legacy Compatibility API +## Boot API -The server always exposes: +The boot service exposes boot management endpoints at root paths that are +always available. -- `GET /boot/v1/bootscript` +### Boot Script Generation + +- `GET /bootscript` - Generate iPXE boot script for a node + +Query parameters: + +- `host` - Node XName (e.g., x0c0s0b0n0) +- `mac` - MAC address (e.g., aa:bb:cc:dd:ee:ff) +- `nid` - Node ID (e.g., 42) +- `profile` - Profile name (currently ignored; auto-selects best match) + +Example: + +```bash +curl "http://localhost:8080/bootscript?mac=aa:bb:cc:dd:ee:ff" +``` + +### Boot Parameters Management -When `enable_legacy_api: true`, it also exposes the rest of the BSS-compatible -surface: +- `GET /bootparameters` - List boot configurations +- `POST /bootparameters` - Create boot configuration +- `PUT /bootparameters` - Update boot configuration +- `DELETE /bootparameters` - Delete boot configuration +### Service Information + +- `GET /service/status` - Service status information +- `GET /service/version` - Service version information + +## Legacy BSS Compatibility API + +When `enable_legacy_api: true`, legacy BSS-compatible endpoints are available at `/boot/v1/*`: + +- `GET /boot/v1/bootscript` - `GET /boot/v1/bootparameters` - `POST /boot/v1/bootparameters` - `PUT /boot/v1/bootparameters` @@ -61,9 +91,17 @@ surface: - `GET /boot/v1/service/status` - `GET /boot/v1/service/version` -Current behavior note: the legacy `bootscript` route accepts `host`, `mac`, and -`nid` identifiers, but ignores the `profile` query parameter and auto-selects -the best matching configuration across profiles. +When legacy API is disabled (`enable_legacy_api` is `false`), these `/boot/v1/*` endpoints +return 404 Not Found. Only the modern endpoints at root paths are available. + +Example with legacy API enabled: +```bash +curl "http://localhost:8080/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff" +``` + +**Note:** Both modern and legacy endpoints use the same handler logic. The `profile` +query parameter is currently ignored; the controller auto-selects the best matching +configuration across profiles based on score and priority. ## Generated Client diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index dd3f18c..471d029 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -61,11 +61,21 @@ environment variables. | --- | --- | --- | | `enable_auth` | `false` | Enables TokenSmith-related startup validation and HSM service-token exchange. It does not currently attach request middleware in `cmd/server/main.go`. | | `enable_metrics` | `false` | Enables Prometheus metrics exposure. | -| `enable_legacy_api` | `true` | Keeps the legacy BSS-compatible endpoints enabled. `GET /boot/v1/bootscript` remains available even when this is `false`. | +| `enable_legacy_api` | `true` | Controls availability of legacy BSS-compatible endpoints at `/boot/v1/*`. When `false`, only modern endpoints at root paths are available. | | `metrics_port` | `9090` | Port used for the dedicated metrics listener once `enable_metrics` is set to `true`. | -`enable_legacy_api: false` still leaves `GET /boot/v1/bootscript` available for -node boot flow. It only disables the rest of the legacy BSS compatibility API. +**Modern vs Legacy API Endpoints:** + +When `enable_legacy_api` is `false` (default): + +- Modern endpoints at root paths are available: `/bootscript`, `/bootparameters`, `/service/*` +- Legacy endpoints at `/boot/v1/*` return 404 Not Found + +When `enable_legacy_api` is `true`: + +- Both modern and legacy, BSS-compatible endpoints are available +- Not meant for production use! Use modern endpoints for production. +- Legacy endpoints provided for BSS compatibility only ### TokenSmith and HSM diff --git a/docs/PROFILES.md b/docs/PROFILES.md index a9e2c45..0169791 100644 --- a/docs/PROFILES.md +++ b/docs/PROFILES.md @@ -15,7 +15,7 @@ There are two profile-related behaviors in this repository and they are not the same: 1. The boot script controller supports requested profiles and fallback to the default profile. -2. The legacy HTTP bootscript endpoint ignores the `profile` query parameter and auto-selects the best match across profiles. +2. The boot HTTP endpoints (both modern and legacy) ignore the `profile` query parameter and auto-select the best match across profiles. **That distinction matters more than anything else in this document.** @@ -101,25 +101,38 @@ Current controller rules are: This behavior is covered by tests in `pkg/controllers/bootscript/controller_profile_test.go`. -## Legacy HTTP Endpoint Behavior +## HTTP Endpoint Behavior -The legacy endpoint is: +The boot service provides bootscript endpoints at two locations: -- `GET /boot/v1/bootscript` +**Modern API:** -It accepts node identifiers through: +- `GET /bootscript` (always available) -- `?mac=` -- `?host=` -- `?nid=` +**Legacy BSS API:** -Current limitation: the handler ignores any `profile` query parameter and always -calls the controller with an empty profile. +- `GET /boot/v1/bootscript` (only when `enable_legacy_api` is `true`) + +Both endpoints accept node identifiers through: + +- `?mac=` - MAC address +- `?host=` - XName or hostname +- `?nid=` - Node ID + +**Current limitation:** Both endpoints ignore the `profile` query parameter and always +call the controller with an empty profile, which auto-selects the best match across +all profiles. That means all of these requests behave the same as far as profile selection is concerned: ```bash +# Modern endpoint examples +curl "http://boot-service:8080/bootscript?mac=aa:bb:cc:dd:ee:ff" +curl "http://boot-service:8080/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=compute" +curl "http://boot-service:8080/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=default" + +# Legacy endpoint examples (when enable_legacy_api: true) curl "http://boot-service:8080/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff" curl "http://boot-service:8080/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=compute" curl "http://boot-service:8080/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=default" @@ -150,13 +163,13 @@ Use profiles today for: - controller-level integrations that call `GenerateBootScript(..., profile)` directly - preparing for a future HTTP surface that may expose explicit profile selection -Do not assume profile-specific HTTP behavior from `/boot/v1/bootscript` yet. +Do not assume profile-specific HTTP behavior from the bootscript endpoints yet. ## Troubleshooting ### A profile query parameter does nothing -That is the current expected behavior of the legacy HTTP route. The handler +That is the current expected behavior of the boot HTTP endpoints. The handler accepts the parameter but ignores it, and matching is still based on best score and priority across profiles. diff --git a/examples/profile-requests.sh b/examples/profile-requests.sh index 88767fc..bc345ea 100644 --- a/examples/profile-requests.sh +++ b/examples/profile-requests.sh @@ -8,29 +8,32 @@ # Set boot service URL BOOT_SERVICE="http://localhost:8080" +echo "=== Modern Boot API Examples ===" +echo "" + # 1. Request boot script with compute profile (MAC address) echo "=== Compute Profile (by MAC) ===" -curl -s "${BOOT_SERVICE}/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=compute" | head -20 +curl -s "${BOOT_SERVICE}/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=compute" | head -20 # 2. Request boot script with login profile (XName/host) echo -e "\n=== Login Profile (by XName) ===" -curl -s "${BOOT_SERVICE}/boot/v1/bootscript?host=x0c0s0b0n0&profile=login" | head -20 +curl -s "${BOOT_SERVICE}/bootscript?host=x0c0s0b0n0&profile=login" | head -20 # 3. Request boot script with debug profile (NID) echo -e "\n=== Debug Profile (by NID) ===" -curl -s "${BOOT_SERVICE}/boot/v1/bootscript?nid=42&profile=debug" | head -20 +curl -s "${BOOT_SERVICE}/bootscript?nid=42&profile=debug" | head -20 # 4. Request with default profile (empty profile parameter) echo -e "\n=== Default Profile (empty profile) ===" -curl -s "${BOOT_SERVICE}/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=" | head -20 +curl -s "${BOOT_SERVICE}/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=" | head -20 # 5. Request without profile parameter (implicit default) echo -e "\n=== No Profile Parameter (defaults to default) ===" -curl -s "${BOOT_SERVICE}/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff" | head -20 +curl -s "${BOOT_SERVICE}/bootscript?mac=aa:bb:cc:dd:ee:ff" | head -20 # 6. Request that will fallback (profile doesn't exist) echo -e "\n=== Non-existent Profile (falls back to default) ===" -curl -s "${BOOT_SERVICE}/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=nonexistent" | head -20 +curl -s "${BOOT_SERVICE}/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=nonexistent" | head -20 # API Response Examples echo -e "\n=== Full Response (JSON from REST API for inspection) ===" @@ -41,3 +44,14 @@ echo -e "\n=== List all profile names in system ===" curl -s "${BOOT_SERVICE}/bootconfigurations" 2>/dev/null | \ jq -r '.[] | "\(.metadata.name): profile=\(.spec.profile // "default")"' || \ echo "(Install jq to parse JSON: brew install jq)" + +echo "" +echo "=== Legacy BSS API Examples (requires enable_legacy_api: true) ===" +echo "" + +# Legacy endpoint examples +echo "=== Legacy Bootscript Endpoint ===" +curl -s "${BOOT_SERVICE}/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff" | head -20 + +echo -e "\n=== Note: Profile parameter is ignored in both modern and legacy endpoints ===" +echo "The controller auto-selects the best match across all profiles." diff --git a/pkg/controllers/bootscript/bootstrap.go b/pkg/controllers/bootscript/bootstrap.go index 45ac79d..09cfd81 100644 --- a/pkg/controllers/bootscript/bootstrap.go +++ b/pkg/controllers/bootscript/bootstrap.go @@ -183,7 +183,7 @@ This package integrates with several other boot service components: - apis/boot.openchami.io/v1: Node and boot configuration resource definitions - pkg/clients/hsm: Hardware State Manager integration - pkg/clients/local: Local file-based node provider - - pkg/handlers/legacy: Legacy BSS API compatibility + - pkg/handlers/boot: Boot API handlers, both modern and legacy, BSS-compatible # Thread Safety diff --git a/pkg/handlers/legacy/converter.go b/pkg/handlers/boot/converter.go similarity index 99% rename from pkg/handlers/legacy/converter.go rename to pkg/handlers/boot/converter.go index 5f8eced..43628dd 100644 --- a/pkg/handlers/legacy/converter.go +++ b/pkg/handlers/boot/converter.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -package legacy +package boot import ( "strconv" diff --git a/pkg/handlers/legacy/handlers.go b/pkg/handlers/boot/handlers.go similarity index 76% rename from pkg/handlers/legacy/handlers.go rename to pkg/handlers/boot/handlers.go index 95ccf68..b911bd3 100644 --- a/pkg/handlers/legacy/handlers.go +++ b/pkg/handlers/boot/handlers.go @@ -2,8 +2,9 @@ // // SPDX-License-Identifier: MIT -// Package legacy provides legacy BSS API handlers for backward compatibility -package legacy +// Package boot provides boot API handlers for both modern and legacy, +// BSS-compatible endpoints +package boot import ( "context" @@ -25,34 +26,56 @@ type BootController interface { GenerateBootScript(ctx context.Context, identifier string, profile string) (string, error) } -// LegacyHandler handles legacy BSS API requests -type LegacyHandler struct { //nolint:revive +// Handler handles boot API requests for both modern and legacy endpoints +type Handler struct { client client.Client controller BootController logger *log.Logger } -// NewLegacyHandler creates a new legacy API handler with standard controller -func NewLegacyHandler(c client.Client, logger *log.Logger) *LegacyHandler { +// NewHandler creates a new boot API handler with standard controller +func NewHandler(c client.Client, logger *log.Logger) *Handler { controller := bootscript.NewBootScriptController(c, logger) - return &LegacyHandler{ + return &Handler{ client: c, controller: controller, logger: logger, } } -// NewLegacyHandlerWithController creates a new legacy API handler with a custom controller -func NewLegacyHandlerWithController(c client.Client, controller BootController, logger *log.Logger) *LegacyHandler { - return &LegacyHandler{ +// NewHandlerWithController creates a new boot API handler with a custom controller +func NewHandlerWithController(c client.Client, controller BootController, logger *log.Logger) *Handler { + return &Handler{ client: c, controller: controller, logger: logger, } } -// RegisterRoutes registers legacy BSS API routes -func (h *LegacyHandler) RegisterRoutes(r chi.Router) { +// RegisterModernRoutes registers modern boot API routes at root paths +// These are always available regardless of enable_legacy_api setting +func (h *Handler) RegisterModernRoutes(r chi.Router) { + // Boot parameters endpoints + r.Route("/bootparameters", func(r chi.Router) { + r.Get("/", h.GetBootParameters) + r.Post("/", h.CreateBootParameters) + r.Put("/", h.UpdateBootParameters) + r.Delete("/", h.DeleteBootParameters) + }) + + // Boot script endpoint + r.Get("/bootscript", h.GetBootScript) + + // Service endpoints + r.Route("/service", func(r chi.Router) { + r.Get("/status", h.GetServiceStatus) + r.Get("/version", h.GetServiceVersion) + }) +} + +// RegisterLegacyRoutes registers legacy BSS API routes at /boot/v1 +// These are ONLY available when enable_legacy_api: true +func (h *Handler) RegisterLegacyRoutes(r chi.Router) { r.Route("/boot/v1", func(r chi.Router) { // Boot parameters endpoints r.Route("/bootparameters", func(r chi.Router) { @@ -73,17 +96,8 @@ func (h *LegacyHandler) RegisterRoutes(r chi.Router) { }) } -// RegisterBootScriptRoute registers only the node bootscript endpoint. -// This endpoint is required for node boot flow even when full legacy API -// compatibility endpoints are disabled. -func (h *LegacyHandler) RegisterBootScriptRoute(r chi.Router) { - r.Route("/boot/v1", func(r chi.Router) { - r.Get("/bootscript", h.GetBootScript) - }) -} - -// GetBootParameters handles GET /boot/v1/bootparameters -func (h *LegacyHandler) GetBootParameters(w http.ResponseWriter, r *http.Request) { +// GetBootParameters handles GET /bootparameters and GET /boot/v1/bootparameters +func (h *Handler) GetBootParameters(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Parse query parameters for filtering @@ -122,8 +136,8 @@ func (h *LegacyHandler) GetBootParameters(w http.ResponseWriter, r *http.Request h.writeJSON(w, http.StatusOK, response) } -// CreateBootParameters handles POST /boot/v1/bootparameters -func (h *LegacyHandler) CreateBootParameters(w http.ResponseWriter, r *http.Request) { +// CreateBootParameters handles POST /bootparameters and POST /boot/v1/bootparameters +func (h *Handler) CreateBootParameters(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req BootParametersRequest @@ -160,8 +174,8 @@ func (h *LegacyHandler) CreateBootParameters(w http.ResponseWriter, r *http.Requ h.writeJSON(w, http.StatusCreated, response) } -// UpdateBootParameters handles PUT /boot/v1/bootparameters -func (h *LegacyHandler) UpdateBootParameters(w http.ResponseWriter, r *http.Request) { +// UpdateBootParameters handles PUT /bootparameters and PUT /boot/v1/bootparameters +func (h *Handler) UpdateBootParameters(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req BootParametersRequest @@ -225,8 +239,8 @@ func (h *LegacyHandler) UpdateBootParameters(w http.ResponseWriter, r *http.Requ h.writeJSON(w, http.StatusOK, response) } -// DeleteBootParameters handles DELETE /boot/v1/bootparameters -func (h *LegacyHandler) DeleteBootParameters(w http.ResponseWriter, r *http.Request) { +// DeleteBootParameters handles DELETE /bootparameters and DELETE /boot/v1/bootparameters +func (h *Handler) DeleteBootParameters(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Parse query parameters to identify which configurations to delete @@ -274,8 +288,8 @@ func (h *LegacyHandler) DeleteBootParameters(w http.ResponseWriter, r *http.Requ h.writeJSON(w, http.StatusOK, response) } -// GetBootScript handles GET /boot/v1/bootscript -func (h *LegacyHandler) GetBootScript(w http.ResponseWriter, r *http.Request) { +// GetBootScript handles GET /bootscript and GET /boot/v1/bootscript +func (h *Handler) GetBootScript(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Parse query parameters for node identification @@ -313,21 +327,21 @@ func (h *LegacyHandler) GetBootScript(w http.ResponseWriter, r *http.Request) { w.Write([]byte(script)) //nolint:errcheck } -// GetServiceStatus handles GET /boot/v1/service/status -func (h *LegacyHandler) GetServiceStatus(w http.ResponseWriter, r *http.Request) { //nolint:revive +// GetServiceStatus handles GET /service/status and GET /boot/v1/service/status +func (h *Handler) GetServiceStatus(w http.ResponseWriter, r *http.Request) { //nolint:revive status := CreateServiceStatus("2.0.0-fabrica") h.writeJSON(w, http.StatusOK, status) } -// GetServiceVersion handles GET /boot/v1/service/version -func (h *LegacyHandler) GetServiceVersion(w http.ResponseWriter, r *http.Request) { //nolint:revive +// GetServiceVersion handles GET /service/version and GET /boot/v1/service/version +func (h *Handler) GetServiceVersion(w http.ResponseWriter, r *http.Request) { //nolint:revive version := CreateServiceVersion("2.0.0-fabrica", "2025-10-08", "main") h.writeJSON(w, http.StatusOK, version) } // Helper methods -func (h *LegacyHandler) writeJSON(w http.ResponseWriter, status int, data interface{}) { +func (h *Handler) writeJSON(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(data); err != nil { @@ -335,12 +349,12 @@ func (h *LegacyHandler) writeJSON(w http.ResponseWriter, status int, data interf } } -func (h *LegacyHandler) writeError(w http.ResponseWriter, status int, title, detail string) { +func (h *Handler) writeError(w http.ResponseWriter, status int, title, detail string) { errorResp := CreateErrorResponse(status, title, detail) h.writeJSON(w, status, errorResp) } -func (h *LegacyHandler) generateConfigName(req BootParametersRequest) string { +func (h *Handler) generateConfigName(req BootParametersRequest) string { // Generate a name based on the first identifier if len(req.Hosts) > 0 { return fmt.Sprintf("legacy-%s", strings.ReplaceAll(req.Hosts[0], ".", "-")) @@ -354,7 +368,7 @@ func (h *LegacyHandler) generateConfigName(req BootParametersRequest) string { return fmt.Sprintf("legacy-config-%d", len(req.Hosts)+len(req.Macs)+len(req.Nids)) } -func (h *LegacyHandler) filterConfigurationsByIdentifiers(configs []apiv1.BootConfiguration, identifiers []string) []apiv1.BootConfiguration { +func (h *Handler) filterConfigurationsByIdentifiers(configs []apiv1.BootConfiguration, identifiers []string) []apiv1.BootConfiguration { var matching []apiv1.BootConfiguration for _, config := range configs { @@ -366,7 +380,7 @@ func (h *LegacyHandler) filterConfigurationsByIdentifiers(configs []apiv1.BootCo return matching } -func (h *LegacyHandler) configMatchesIdentifiers(config apiv1.BootConfiguration, identifiers []string) bool { +func (h *Handler) configMatchesIdentifiers(config apiv1.BootConfiguration, identifiers []string) bool { for _, identifier := range identifiers { // Check hosts for _, host := range config.Spec.Hosts { diff --git a/pkg/handlers/legacy/handlers_test.go b/pkg/handlers/boot/handlers_test.go similarity index 68% rename from pkg/handlers/legacy/handlers_test.go rename to pkg/handlers/boot/handlers_test.go index 8f7fd7f..77c8890 100644 --- a/pkg/handlers/legacy/handlers_test.go +++ b/pkg/handlers/boot/handlers_test.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -package legacy +package boot import ( "encoding/json" @@ -69,16 +69,16 @@ func TestGetBootScript_ProfileQueryParameterIgnored(t *testing.T) { t.Fatalf("failed to create boot client: %v", err) } - // Create legacy handler with bootscript controller - handler := NewLegacyHandler(*bootClient, log.New(io.Discard, "", 0)) + // Create boot handler with bootscript controller + handler := NewHandler(*bootClient, log.New(io.Discard, "", 0)) - // Create router and register legacy routes + // Create router and register modern routes router := chi.NewRouter() - handler.RegisterRoutes(router) + handler.RegisterModernRoutes(router) // Test Case 1: Request with explicit compute profile query should still // auto-select based on score/priority and ignore the query parameter. - req := httptest.NewRequest("GET", "/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=compute", nil) + req := httptest.NewRequest("GET", "/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=compute", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) @@ -95,7 +95,7 @@ func TestGetBootScript_ProfileQueryParameterIgnored(t *testing.T) { } // Test Case 2: Request with empty profile parameter (auto-select best across profiles) - req = httptest.NewRequest("GET", "/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=", nil) + req = httptest.NewRequest("GET", "/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=", nil) w = httptest.NewRecorder() router.ServeHTTP(w, req) @@ -109,7 +109,7 @@ func TestGetBootScript_ProfileQueryParameterIgnored(t *testing.T) { } // Test Case 3: Request without profile parameter (auto-select best across profiles) - req = httptest.NewRequest("GET", "/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff", nil) + req = httptest.NewRequest("GET", "/bootscript?mac=aa:bb:cc:dd:ee:ff", nil) w = httptest.NewRecorder() router.ServeHTTP(w, req) @@ -123,7 +123,7 @@ func TestGetBootScript_ProfileQueryParameterIgnored(t *testing.T) { } // Test Case 4: Request with XName identifier - req = httptest.NewRequest("GET", "/boot/v1/bootscript?host=x0c0s0b0n0&profile=compute", nil) + req = httptest.NewRequest("GET", "/bootscript?host=x0c0s0b0n0&profile=compute", nil) w = httptest.NewRecorder() router.ServeHTTP(w, req) @@ -138,7 +138,7 @@ func TestGetBootScript_ProfileQueryParameterIgnored(t *testing.T) { // Test Case 5: Request with profile=default should still ignore profile and // auto-select compute due to higher match score. - req = httptest.NewRequest("GET", "/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=default", nil) + req = httptest.NewRequest("GET", "/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=default", nil) w = httptest.NewRecorder() router.ServeHTTP(w, req) @@ -170,12 +170,12 @@ func TestGetBootScript_MissingNodeIdentifier(t *testing.T) { t.Fatalf("failed to create boot client: %v", err) } - handler := NewLegacyHandler(*bootClient, log.New(io.Discard, "", 0)) + handler := NewHandler(*bootClient, log.New(io.Discard, "", 0)) router := chi.NewRouter() - handler.RegisterRoutes(router) + handler.RegisterModernRoutes(router) // Request without any node identifier - req := httptest.NewRequest("GET", "/boot/v1/bootscript", nil) + req := httptest.NewRequest("GET", "/bootscript", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) @@ -184,7 +184,7 @@ func TestGetBootScript_MissingNodeIdentifier(t *testing.T) { } } -func TestRegisterBootScriptRoute_OnlyRegistersBootScriptEndpoint(t *testing.T) { +func TestRegisterModernAndLegacyRoutes_Separately(t *testing.T) { backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/nodes": @@ -202,24 +202,63 @@ func TestRegisterBootScriptRoute_OnlyRegistersBootScriptEndpoint(t *testing.T) { t.Fatalf("failed to create boot client: %v", err) } - handler := NewLegacyHandler(*bootClient, log.New(io.Discard, "", 0)) - router := chi.NewRouter() - handler.RegisterBootScriptRoute(router) + handler := NewHandler(*bootClient, log.New(io.Discard, "", 0)) - // bootscript endpoint should be present - req := httptest.NewRequest("GET", "/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff", nil) + // Test 1: Only modern routes registered + router1 := chi.NewRouter() + handler.RegisterModernRoutes(router1) + + // Modern bootscript endpoint should be present + req := httptest.NewRequest("GET", "/bootscript?mac=aa:bb:cc:dd:ee:ff", nil) w := httptest.NewRecorder() - router.ServeHTTP(w, req) + router1.ServeHTTP(w, req) if w.Code != http.StatusOK { - t.Fatalf("expected status 200 for bootscript route, got %d", w.Code) + t.Errorf("expected status 200 for modern bootscript route, got %d", w.Code) } - // other legacy endpoints should not be present - req = httptest.NewRequest("GET", "/boot/v1/bootparameters", nil) + // Legacy endpoint should not be present + req = httptest.NewRequest("GET", "/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff", nil) w = httptest.NewRecorder() - router.ServeHTTP(w, req) + router1.ServeHTTP(w, req) if w.Code != http.StatusNotFound { - t.Fatalf("expected status 404 for bootparameters route, got %d", w.Code) + t.Errorf("expected status 404 for legacy route when not registered, got %d", w.Code) + } + + // Test 2: Both modern and legacy routes registered + router2 := chi.NewRouter() + handler.RegisterModernRoutes(router2) + handler.RegisterLegacyRoutes(router2) + + // Modern bootscript endpoint should work + req = httptest.NewRequest("GET", "/bootscript?mac=aa:bb:cc:dd:ee:ff", nil) + w = httptest.NewRecorder() + router2.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Errorf("expected status 200 for modern bootscript route, got %d", w.Code) + } + + // Legacy bootscript endpoint should also work + req = httptest.NewRequest("GET", "/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff", nil) + w = httptest.NewRecorder() + router2.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Errorf("expected status 200 for legacy bootscript route, got %d", w.Code) + } + + // Modern bootparameters endpoint should work + req = httptest.NewRequest("GET", "/bootparameters", nil) + w = httptest.NewRecorder() + router2.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Errorf("expected status 200 for modern bootparameters route, got %d", w.Code) + } + + // Legacy bootparameters endpoint should also work + req = httptest.NewRequest("GET", "/boot/v1/bootparameters", nil) + w = httptest.NewRecorder() + router2.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Errorf("expected status 200 for legacy bootparameters route, got %d", w.Code) } } diff --git a/pkg/handlers/legacy/legacy_test.go b/pkg/handlers/boot/legacy_test.go similarity index 99% rename from pkg/handlers/legacy/legacy_test.go rename to pkg/handlers/boot/legacy_test.go index fbae12c..a1a30c4 100644 --- a/pkg/handlers/legacy/legacy_test.go +++ b/pkg/handlers/boot/legacy_test.go @@ -2,8 +2,8 @@ // // SPDX-License-Identifier: MIT -// Legacy BSS API integration tests -package legacy +// Boot API integration tests +package boot import ( "bytes" diff --git a/pkg/handlers/legacy/types.go b/pkg/handlers/boot/types.go similarity index 95% rename from pkg/handlers/legacy/types.go rename to pkg/handlers/boot/types.go index 96652ca..12f3a09 100644 --- a/pkg/handlers/legacy/types.go +++ b/pkg/handlers/boot/types.go @@ -2,9 +2,9 @@ // // SPDX-License-Identifier: MIT -// Package legacy implements the legacy BSS API types for backward compatibility -// This file defines the exact request/response formats expected by legacy BSS clients -package legacy +// Package boot implements the boot API types for both modern and legacy (BSS-compatible) endpoints +// This file defines the request/response formats for boot API handlers +package boot import ( "time"