Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 34 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
115 changes: 83 additions & 32 deletions cmd/server/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
},
}

Expand All @@ -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)
}
})
}
Expand Down Expand Up @@ -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
Expand Down
27 changes: 15 additions & 12 deletions cmd/server/server_extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Loading
Loading