From f4b5c513d6f1a1c669372d266a04d6a4c6b33f42 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Mon, 11 May 2026 07:44:26 -0400 Subject: [PATCH 01/10] chore: update Fabrica version to v0.4.4 and modify code generation checks Signed-off-by: Alex Lovell-Troy --- .github/workflows/codegen-check.yaml | 10 +--------- .github/workflows/golangci-lint.yaml | 2 +- go.mod | 4 ++-- go.sum | 4 ++-- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/.github/workflows/codegen-check.yaml b/.github/workflows/codegen-check.yaml index 685538c..7418449 100644 --- a/.github/workflows/codegen-check.yaml +++ b/.github/workflows/codegen-check.yaml @@ -31,13 +31,5 @@ jobs: with: go-version: stable - - name: Run code generation - run: make generate - - name: Verify generated code is committed - run: | - if ! git diff --quiet; then - echo "Generated files are out of sync. Run 'make generate' and commit the results." - git --no-pager diff --stat - exit 1 - fi + run: make generate-check diff --git a/.github/workflows/golangci-lint.yaml b/.github/workflows/golangci-lint.yaml index 1320641..5503e98 100644 --- a/.github/workflows/golangci-lint.yaml +++ b/.github/workflows/golangci-lint.yaml @@ -12,7 +12,7 @@ on: jobs: lint: runs-on: ubuntu-latest - + steps: - name: Set up latest stable Go uses: actions/setup-go@v6.4.0 diff --git a/go.mod b/go.mod index 633b804..1d5d8b3 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,13 @@ module github.com/openchami/boot-service -go 1.26.2 +go 1.26.3 require ( github.com/getkin/kin-openapi v0.133.0 github.com/go-chi/chi/v5 v5.2.3 github.com/golang-jwt/jwt/v5 v5.3.0 - github.com/openchami/fabrica v0.4.3 + github.com/openchami/fabrica v0.4.4 github.com/openchami/tokensmith v0.4.0 github.com/prometheus/client_golang v1.23.2 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index a2d29b1..fa0e668 100644 --- a/go.sum +++ b/go.sum @@ -99,8 +99,8 @@ github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletI github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/openchami/chi-middleware/log v0.0.0-20240812224658-b16b83c70700 h1:Gzt5f6RK39CHvY3SJudzBb/RK4tVh/S3CpJ0eQlbNdg= github.com/openchami/chi-middleware/log v0.0.0-20240812224658-b16b83c70700/go.mod h1:UuXvr2loD4MtvZeKr57W0WpBs+gm0KM1kdtcXrE8M6s= -github.com/openchami/fabrica v0.4.3 h1:QSg1+4BuG8B5Jv4xa7pdpUv9FGz0mm9+3XxMpkjqwNs= -github.com/openchami/fabrica v0.4.3/go.mod h1:Kmii+YJz6fYfIU2ZH+LkfriLhXB8AVf8vUDxfG9Acnk= +github.com/openchami/fabrica v0.4.4 h1:M7VncVIIywfdWpim5lPjvyKxxxe0LLiVK/ZOw1/4BhU= +github.com/openchami/fabrica v0.4.4/go.mod h1:Kmii+YJz6fYfIU2ZH+LkfriLhXB8AVf8vUDxfG9Acnk= github.com/openchami/tokensmith v0.4.0 h1:+HBzW0ilH3/P5RNlGJfgVtSbc9r5ClzPHij9CnySVxI= github.com/openchami/tokensmith v0.4.0/go.mod h1:L4ZCMX/vPGwXUUn9otw+UdfFTbarv+ZVO/FjhZmoOAE= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= From 7cd06e437382d7489d2484568a834259278e4af2 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Mon, 11 May 2026 08:58:53 -0400 Subject: [PATCH 02/10] feat: add /health endpoint and patch operations for BMC, BootConfiguration, and Node resources Signed-off-by: Alex Lovell-Troy --- Makefile | 4 +- cmd/server/openapi_generated.go | 245 ++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index cf6d696..e016f76 100644 --- a/Makefile +++ b/Makefile @@ -16,12 +16,14 @@ DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") LDFLAGS=-ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)" FABRICA_CMD ?= go run github.com/openchami/fabrica/cmd/fabrica@latest FABRICA_SOURCE_ARG ?= +FABRICA_FORCE_FLAG ?= FABRICA_ENV ?= LOCAL_FABRICA ?= ifneq ($(strip $(LOCAL_FABRICA)),) FABRICA_CMD := $(LOCAL_FABRICA)/bin/fabrica FABRICA_SOURCE_ARG := --fabrica-source $(LOCAL_FABRICA) +FABRICA_FORCE_FLAG := --force FABRICA_ENV := GOTOOLCHAIN=auto endif @@ -41,7 +43,7 @@ ifneq ($(strip $(LOCAL_FABRICA)),) exit 1; \ fi endif - $(FABRICA_ENV) $(FABRICA_CMD) generate $(FABRICA_SOURCE_ARG) + $(FABRICA_ENV) $(FABRICA_CMD) generate $(FABRICA_SOURCE_ARG) $(FABRICA_FORCE_FLAG) generate-check: ## Fail if generated files are out of sync (requires clean git tree) @if ! git diff --quiet || ! git diff --cached --quiet; then \ diff --git a/cmd/server/openapi_generated.go b/cmd/server/openapi_generated.go index 0081f82..c7468d3 100644 --- a/cmd/server/openapi_generated.go +++ b/cmd/server/openapi_generated.go @@ -108,6 +108,20 @@ func GenerateOpenAPISpec() *openapi3.T { registerBootConfigurationPaths(spec) registerNodePaths(spec) + // Register /health endpoint + healthOp := openapi3.NewOperation() + healthOp.OperationID = "health" + healthOp.Summary = "Service health" + healthOp.Description = "Returns service health information" + healthOp.Tags = []string{"Service"} + healthOp.Responses = openapi3.NewResponses() + healthOp.Responses.Set("200", &openapi3.ResponseRef{ + Value: openapi3.NewResponse(). + WithDescription("Healthy"). + WithJSONSchemaRef(&openapi3.SchemaRef{Value: openapi3.NewObjectSchema()}), + }) + spec.Paths.Set("/health", &openapi3.PathItem{Get: healthOp}) + // Register custom (non-Fabrica-generated) paths. // Defined in openapi_extensions.go – safe to edit, never overwritten. registerCustomOpenAPIPaths(spec) @@ -127,6 +141,9 @@ func registerBMCPaths(spec *openapi3.T) { updateReqSchema, _ := openapi3gen.NewSchemaRefForValue(&UpdateBMCRequest{}, spec.Components.Schemas) spec.Components.Schemas["UpdateBMCRequest"] = updateReqSchema + statusSchema, _ := openapi3gen.NewSchemaRefForValue(&v1.BMCStatus{}, spec.Components.Schemas) + spec.Components.Schemas["BMCStatus"] = statusSchema + // Error response schema if _, exists := spec.Components.Schemas["ErrorResponse"]; !exists { errorSchema := openapi3.NewObjectSchema(). @@ -223,6 +240,65 @@ func registerBMCPaths(spec *openapi3.T) { updateOp.Responses.Set("404", errorResponse()) updateOp.Responses.Set("500", errorResponse()) + // Patch BMC operation + patchOp := openapi3.NewOperation() + patchOp.OperationID = "patchBMC" + patchOp.Summary = "Patch a BMC resource" + patchOp.Description = "Partially updates an existing BMC resource using patch semantics" + patchOp.Tags = []string{"BMC"} + patchOp.RequestBody = patchRequestBody() + patchOp.Responses = openapi3.NewResponses() + patchOp.Responses.Set("200", &openapi3.ResponseRef{ + Value: openapi3.NewResponse(). + WithDescription("Resource patched successfully"). + WithJSONSchemaRef(&openapi3.SchemaRef{ + Ref: "#/components/schemas/BMC", + }), + }) + patchOp.Responses.Set("400", errorResponse()) + patchOp.Responses.Set("404", errorResponse()) + patchOp.Responses.Set("422", errorResponse()) + patchOp.Responses.Set("500", errorResponse()) + + // Update BMC status operation + updateStatusOp := openapi3.NewOperation() + updateStatusOp.OperationID = "updateBMCStatus" + updateStatusOp.Summary = "Update BMC status" + updateStatusOp.Description = "Updates only the status subresource for an existing BMC" + updateStatusOp.Tags = []string{"BMC"} + updateStatusOp.RequestBody = &openapi3.RequestBodyRef{ + Value: openapi3.NewRequestBody(). + WithRequired(true). + WithJSONSchemaRef(&openapi3.SchemaRef{Ref: "#/components/schemas/BMCStatus"}), + } + updateStatusOp.Responses = openapi3.NewResponses() + updateStatusOp.Responses.Set("200", &openapi3.ResponseRef{ + Value: openapi3.NewResponse(). + WithDescription("Status updated successfully"). + WithJSONSchemaRef(&openapi3.SchemaRef{Ref: "#/components/schemas/BMC"}), + }) + updateStatusOp.Responses.Set("400", errorResponse()) + updateStatusOp.Responses.Set("404", errorResponse()) + updateStatusOp.Responses.Set("500", errorResponse()) + + // Patch BMC status operation + patchStatusOp := openapi3.NewOperation() + patchStatusOp.OperationID = "patchBMCStatus" + patchStatusOp.Summary = "Patch BMC status" + patchStatusOp.Description = "Partially updates only the status subresource for an existing BMC" + patchStatusOp.Tags = []string{"BMC"} + patchStatusOp.RequestBody = patchRequestBody() + patchStatusOp.Responses = openapi3.NewResponses() + patchStatusOp.Responses.Set("200", &openapi3.ResponseRef{ + Value: openapi3.NewResponse(). + WithDescription("Status patched successfully"). + WithJSONSchemaRef(&openapi3.SchemaRef{Ref: "#/components/schemas/BMC"}), + }) + patchStatusOp.Responses.Set("400", errorResponse()) + patchStatusOp.Responses.Set("404", errorResponse()) + patchStatusOp.Responses.Set("422", errorResponse()) + patchStatusOp.Responses.Set("500", errorResponse()) + // Delete BMC operation deleteOp := openapi3.NewOperation() deleteOp.OperationID = "deleteBMC" @@ -255,15 +331,25 @@ func registerBMCPaths(spec *openapi3.T) { itemPath := &openapi3.PathItem{ Get: getOp, Put: updateOp, + Patch: patchOp, Delete: deleteOp, Parameters: []*openapi3.ParameterRef{ {Value: uidParam}, }, } + statusPath := &openapi3.PathItem{ + Put: updateStatusOp, + Patch: patchStatusOp, + Parameters: []*openapi3.ParameterRef{ + {Value: uidParam}, + }, + } + // Add paths to spec spec.Paths.Set("/bmcs", collectionPath) spec.Paths.Set("/bmcs/{uid}", itemPath) + spec.Paths.Set("/bmcs/{uid}/status", statusPath) } // registerBootConfigurationPaths registers OpenAPI paths for BootConfiguration resources @@ -278,6 +364,9 @@ func registerBootConfigurationPaths(spec *openapi3.T) { updateReqSchema, _ := openapi3gen.NewSchemaRefForValue(&UpdateBootConfigurationRequest{}, spec.Components.Schemas) spec.Components.Schemas["UpdateBootConfigurationRequest"] = updateReqSchema + statusSchema, _ := openapi3gen.NewSchemaRefForValue(&v1.BootConfigurationStatus{}, spec.Components.Schemas) + spec.Components.Schemas["BootConfigurationStatus"] = statusSchema + // Error response schema if _, exists := spec.Components.Schemas["ErrorResponse"]; !exists { errorSchema := openapi3.NewObjectSchema(). @@ -374,6 +463,65 @@ func registerBootConfigurationPaths(spec *openapi3.T) { updateOp.Responses.Set("404", errorResponse()) updateOp.Responses.Set("500", errorResponse()) + // Patch BootConfiguration operation + patchOp := openapi3.NewOperation() + patchOp.OperationID = "patchBootConfiguration" + patchOp.Summary = "Patch a BootConfiguration resource" + patchOp.Description = "Partially updates an existing BootConfiguration resource using patch semantics" + patchOp.Tags = []string{"BootConfiguration"} + patchOp.RequestBody = patchRequestBody() + patchOp.Responses = openapi3.NewResponses() + patchOp.Responses.Set("200", &openapi3.ResponseRef{ + Value: openapi3.NewResponse(). + WithDescription("Resource patched successfully"). + WithJSONSchemaRef(&openapi3.SchemaRef{ + Ref: "#/components/schemas/BootConfiguration", + }), + }) + patchOp.Responses.Set("400", errorResponse()) + patchOp.Responses.Set("404", errorResponse()) + patchOp.Responses.Set("422", errorResponse()) + patchOp.Responses.Set("500", errorResponse()) + + // Update BootConfiguration status operation + updateStatusOp := openapi3.NewOperation() + updateStatusOp.OperationID = "updateBootConfigurationStatus" + updateStatusOp.Summary = "Update BootConfiguration status" + updateStatusOp.Description = "Updates only the status subresource for an existing BootConfiguration" + updateStatusOp.Tags = []string{"BootConfiguration"} + updateStatusOp.RequestBody = &openapi3.RequestBodyRef{ + Value: openapi3.NewRequestBody(). + WithRequired(true). + WithJSONSchemaRef(&openapi3.SchemaRef{Ref: "#/components/schemas/BootConfigurationStatus"}), + } + updateStatusOp.Responses = openapi3.NewResponses() + updateStatusOp.Responses.Set("200", &openapi3.ResponseRef{ + Value: openapi3.NewResponse(). + WithDescription("Status updated successfully"). + WithJSONSchemaRef(&openapi3.SchemaRef{Ref: "#/components/schemas/BootConfiguration"}), + }) + updateStatusOp.Responses.Set("400", errorResponse()) + updateStatusOp.Responses.Set("404", errorResponse()) + updateStatusOp.Responses.Set("500", errorResponse()) + + // Patch BootConfiguration status operation + patchStatusOp := openapi3.NewOperation() + patchStatusOp.OperationID = "patchBootConfigurationStatus" + patchStatusOp.Summary = "Patch BootConfiguration status" + patchStatusOp.Description = "Partially updates only the status subresource for an existing BootConfiguration" + patchStatusOp.Tags = []string{"BootConfiguration"} + patchStatusOp.RequestBody = patchRequestBody() + patchStatusOp.Responses = openapi3.NewResponses() + patchStatusOp.Responses.Set("200", &openapi3.ResponseRef{ + Value: openapi3.NewResponse(). + WithDescription("Status patched successfully"). + WithJSONSchemaRef(&openapi3.SchemaRef{Ref: "#/components/schemas/BootConfiguration"}), + }) + patchStatusOp.Responses.Set("400", errorResponse()) + patchStatusOp.Responses.Set("404", errorResponse()) + patchStatusOp.Responses.Set("422", errorResponse()) + patchStatusOp.Responses.Set("500", errorResponse()) + // Delete BootConfiguration operation deleteOp := openapi3.NewOperation() deleteOp.OperationID = "deleteBootConfiguration" @@ -406,15 +554,25 @@ func registerBootConfigurationPaths(spec *openapi3.T) { itemPath := &openapi3.PathItem{ Get: getOp, Put: updateOp, + Patch: patchOp, Delete: deleteOp, Parameters: []*openapi3.ParameterRef{ {Value: uidParam}, }, } + statusPath := &openapi3.PathItem{ + Put: updateStatusOp, + Patch: patchStatusOp, + Parameters: []*openapi3.ParameterRef{ + {Value: uidParam}, + }, + } + // Add paths to spec spec.Paths.Set("/bootconfigurations", collectionPath) spec.Paths.Set("/bootconfigurations/{uid}", itemPath) + spec.Paths.Set("/bootconfigurations/{uid}/status", statusPath) } // registerNodePaths registers OpenAPI paths for Node resources @@ -429,6 +587,9 @@ func registerNodePaths(spec *openapi3.T) { updateReqSchema, _ := openapi3gen.NewSchemaRefForValue(&UpdateNodeRequest{}, spec.Components.Schemas) spec.Components.Schemas["UpdateNodeRequest"] = updateReqSchema + statusSchema, _ := openapi3gen.NewSchemaRefForValue(&v1.NodeStatus{}, spec.Components.Schemas) + spec.Components.Schemas["NodeStatus"] = statusSchema + // Error response schema if _, exists := spec.Components.Schemas["ErrorResponse"]; !exists { errorSchema := openapi3.NewObjectSchema(). @@ -525,6 +686,65 @@ func registerNodePaths(spec *openapi3.T) { updateOp.Responses.Set("404", errorResponse()) updateOp.Responses.Set("500", errorResponse()) + // Patch Node operation + patchOp := openapi3.NewOperation() + patchOp.OperationID = "patchNode" + patchOp.Summary = "Patch a Node resource" + patchOp.Description = "Partially updates an existing Node resource using patch semantics" + patchOp.Tags = []string{"Node"} + patchOp.RequestBody = patchRequestBody() + patchOp.Responses = openapi3.NewResponses() + patchOp.Responses.Set("200", &openapi3.ResponseRef{ + Value: openapi3.NewResponse(). + WithDescription("Resource patched successfully"). + WithJSONSchemaRef(&openapi3.SchemaRef{ + Ref: "#/components/schemas/Node", + }), + }) + patchOp.Responses.Set("400", errorResponse()) + patchOp.Responses.Set("404", errorResponse()) + patchOp.Responses.Set("422", errorResponse()) + patchOp.Responses.Set("500", errorResponse()) + + // Update Node status operation + updateStatusOp := openapi3.NewOperation() + updateStatusOp.OperationID = "updateNodeStatus" + updateStatusOp.Summary = "Update Node status" + updateStatusOp.Description = "Updates only the status subresource for an existing Node" + updateStatusOp.Tags = []string{"Node"} + updateStatusOp.RequestBody = &openapi3.RequestBodyRef{ + Value: openapi3.NewRequestBody(). + WithRequired(true). + WithJSONSchemaRef(&openapi3.SchemaRef{Ref: "#/components/schemas/NodeStatus"}), + } + updateStatusOp.Responses = openapi3.NewResponses() + updateStatusOp.Responses.Set("200", &openapi3.ResponseRef{ + Value: openapi3.NewResponse(). + WithDescription("Status updated successfully"). + WithJSONSchemaRef(&openapi3.SchemaRef{Ref: "#/components/schemas/Node"}), + }) + updateStatusOp.Responses.Set("400", errorResponse()) + updateStatusOp.Responses.Set("404", errorResponse()) + updateStatusOp.Responses.Set("500", errorResponse()) + + // Patch Node status operation + patchStatusOp := openapi3.NewOperation() + patchStatusOp.OperationID = "patchNodeStatus" + patchStatusOp.Summary = "Patch Node status" + patchStatusOp.Description = "Partially updates only the status subresource for an existing Node" + patchStatusOp.Tags = []string{"Node"} + patchStatusOp.RequestBody = patchRequestBody() + patchStatusOp.Responses = openapi3.NewResponses() + patchStatusOp.Responses.Set("200", &openapi3.ResponseRef{ + Value: openapi3.NewResponse(). + WithDescription("Status patched successfully"). + WithJSONSchemaRef(&openapi3.SchemaRef{Ref: "#/components/schemas/Node"}), + }) + patchStatusOp.Responses.Set("400", errorResponse()) + patchStatusOp.Responses.Set("404", errorResponse()) + patchStatusOp.Responses.Set("422", errorResponse()) + patchStatusOp.Responses.Set("500", errorResponse()) + // Delete Node operation deleteOp := openapi3.NewOperation() deleteOp.OperationID = "deleteNode" @@ -557,15 +777,40 @@ func registerNodePaths(spec *openapi3.T) { itemPath := &openapi3.PathItem{ Get: getOp, Put: updateOp, + Patch: patchOp, Delete: deleteOp, Parameters: []*openapi3.ParameterRef{ {Value: uidParam}, }, } + statusPath := &openapi3.PathItem{ + Put: updateStatusOp, + Patch: patchStatusOp, + Parameters: []*openapi3.ParameterRef{ + {Value: uidParam}, + }, + } + // Add paths to spec spec.Paths.Set("/nodes", collectionPath) spec.Paths.Set("/nodes/{uid}", itemPath) + spec.Paths.Set("/nodes/{uid}/status", statusPath) +} + +func patchRequestBody() *openapi3.RequestBodyRef { + patchSchema := &openapi3.SchemaRef{Value: openapi3.NewObjectSchema()} + return &openapi3.RequestBodyRef{ + Value: &openapi3.RequestBody{ + Required: true, + Content: openapi3.Content{ + "application/merge-patch+json": &openapi3.MediaType{Schema: patchSchema}, + "application/json-patch+json": &openapi3.MediaType{Schema: patchSchema}, + "application/shorthand-patch": &openapi3.MediaType{Schema: patchSchema}, + "application/json": &openapi3.MediaType{Schema: patchSchema}, + }, + }, + } } // Helper function for error responses From 1aa5a55d640e6c6ab832f9128503f1e61705a709 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Mon, 11 May 2026 11:00:31 -0400 Subject: [PATCH 03/10] feat: enhance Docker build process with dynamic build arguments Addresses Issue Building local docker containers with make docker-build is broken Fixes #8 Signed-off-by: Alex Lovell-Troy --- Makefile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e016f76..0edd23d 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ TEST_TIMEOUT ?= 5m VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") +DOCKER_GO_VERSION ?= $(shell awk '/^go / {print $$2; exit}' go.mod) LDFLAGS=-ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)" FABRICA_CMD ?= go run github.com/openchami/fabrica/cmd/fabrica@latest FABRICA_SOURCE_ARG ?= @@ -87,7 +88,12 @@ run: build ## Build and run the application ./bin/$(BINARY_NAME) docker-build: ## Build Docker image - docker build -t $(BINARY_NAME):latest . + docker build -f Dockerfile.standalone \ + --build-arg GO_VERSION=$(DOCKER_GO_VERSION) \ + --build-arg VERSION=$(VERSION) \ + --build-arg COMMIT=$(COMMIT) \ + --build-arg DATE=$(DATE) \ + -t $(BINARY_NAME):latest . docker-run: docker-build ## Build and run Docker container docker run --rm $(BINARY_NAME):latest From fedac2b4c59fbd56c0b00e0b8491894568147048 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Mon, 11 May 2026 12:46:09 -0400 Subject: [PATCH 04/10] feat: update generated code to Fabrica v0.4.4 and add custom validation logic for BMC, BootConfiguration, and Node handlers Signed-off-by: Alex Lovell-Troy --- cmd/server/bmc_handlers_generated.go | 28 +++++++++++++++++-- .../bootconfiguration_handlers_generated.go | 28 +++++++++++++++++-- cmd/server/node_handlers_generated.go | 28 +++++++++++++++++-- pkg/client/client_generated.go | 16 ++++++++++- 4 files changed, 93 insertions(+), 7 deletions(-) diff --git a/cmd/server/bmc_handlers_generated.go b/cmd/server/bmc_handlers_generated.go index 5777fd5..cca6c4d 100644 --- a/cmd/server/bmc_handlers_generated.go +++ b/cmd/server/bmc_handlers_generated.go @@ -1,6 +1,6 @@ -// Code generated by Fabrica dev. DO NOT EDIT. +// Code generated by Fabrica v0.4.4-9-g4df0460 (commit: 4df0460). DO NOT EDIT. // Template: server/handlers.go.tmpl -// Generated: 2026-05-05T09:18:40Z +// Generated: 2026-05-11T16:34:21Z // // # Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // @@ -216,6 +216,12 @@ func UpdateBMC(w http.ResponseWriter, r *http.Request) { // Update timestamp bMC.Metadata.UpdatedAt = time.Now() + // Layer 2: Custom business logic validation + if err := validation.ValidateWithContext(r.Context(), bMC); err != nil { + respondError(w, http.StatusBadRequest, fmt.Errorf("validation failed: %w", err)) + return + } + if err := storage.SaveBMC(r.Context(), bMC); err != nil { respondError(w, http.StatusInternalServerError, fmt.Errorf("failed to save BMC: %w", err)) return @@ -288,6 +294,12 @@ func PatchBMC(w http.ResponseWriter, r *http.Request) { bMC.Metadata.UpdatedAt = time.Now() + // Layer 2: Custom business logic validation + if err := validation.ValidateWithContext(r.Context(), bMC); err != nil { + respondError(w, http.StatusBadRequest, fmt.Errorf("validation failed: %w", err)) + return + } + // Save the patched resource if err := storage.SaveBMC(r.Context(), bMC); err != nil { respondError(w, http.StatusInternalServerError, fmt.Errorf("failed to save patched BMC: %w", err)) @@ -342,6 +354,12 @@ func UpdateBMCStatus(w http.ResponseWriter, r *http.Request) { res.Metadata.UpdatedAt = time.Now() + // Layer 2: Custom business logic validation + if err := validation.ValidateWithContext(r.Context(), res); err != nil { + respondError(w, http.StatusBadRequest, fmt.Errorf("validation failed: %w", err)) + return + } + if err := storage.SaveBMC(r.Context(), res); err != nil { respondError(w, http.StatusInternalServerError, fmt.Errorf("failed to save BMC status: %w", err)) return @@ -414,6 +432,12 @@ func PatchBMCStatus(w http.ResponseWriter, r *http.Request) { res.Metadata.UpdatedAt = time.Now() + // Layer 2: Custom business logic validation + if err := validation.ValidateWithContext(r.Context(), res); err != nil { + respondError(w, http.StatusBadRequest, fmt.Errorf("validation failed: %w", err)) + return + } + if err := storage.SaveBMC(r.Context(), res); err != nil { respondError(w, http.StatusInternalServerError, fmt.Errorf("failed to save patched BMC status: %w", err)) return diff --git a/cmd/server/bootconfiguration_handlers_generated.go b/cmd/server/bootconfiguration_handlers_generated.go index 0db2e5e..75d82f4 100644 --- a/cmd/server/bootconfiguration_handlers_generated.go +++ b/cmd/server/bootconfiguration_handlers_generated.go @@ -1,6 +1,6 @@ -// Code generated by Fabrica dev. DO NOT EDIT. +// Code generated by Fabrica v0.4.4-9-g4df0460 (commit: 4df0460). DO NOT EDIT. // Template: server/handlers.go.tmpl -// Generated: 2026-05-05T09:18:40Z +// Generated: 2026-05-11T16:34:21Z // // # Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // @@ -216,6 +216,12 @@ func UpdateBootConfiguration(w http.ResponseWriter, r *http.Request) { // Update timestamp bootConfiguration.Metadata.UpdatedAt = time.Now() + // Layer 2: Custom business logic validation + if err := validation.ValidateWithContext(r.Context(), bootConfiguration); err != nil { + respondError(w, http.StatusBadRequest, fmt.Errorf("validation failed: %w", err)) + return + } + if err := storage.SaveBootConfiguration(r.Context(), bootConfiguration); err != nil { respondError(w, http.StatusInternalServerError, fmt.Errorf("failed to save BootConfiguration: %w", err)) return @@ -288,6 +294,12 @@ func PatchBootConfiguration(w http.ResponseWriter, r *http.Request) { bootConfiguration.Metadata.UpdatedAt = time.Now() + // Layer 2: Custom business logic validation + if err := validation.ValidateWithContext(r.Context(), bootConfiguration); err != nil { + respondError(w, http.StatusBadRequest, fmt.Errorf("validation failed: %w", err)) + return + } + // Save the patched resource if err := storage.SaveBootConfiguration(r.Context(), bootConfiguration); err != nil { respondError(w, http.StatusInternalServerError, fmt.Errorf("failed to save patched BootConfiguration: %w", err)) @@ -342,6 +354,12 @@ func UpdateBootConfigurationStatus(w http.ResponseWriter, r *http.Request) { res.Metadata.UpdatedAt = time.Now() + // Layer 2: Custom business logic validation + if err := validation.ValidateWithContext(r.Context(), res); err != nil { + respondError(w, http.StatusBadRequest, fmt.Errorf("validation failed: %w", err)) + return + } + if err := storage.SaveBootConfiguration(r.Context(), res); err != nil { respondError(w, http.StatusInternalServerError, fmt.Errorf("failed to save BootConfiguration status: %w", err)) return @@ -414,6 +432,12 @@ func PatchBootConfigurationStatus(w http.ResponseWriter, r *http.Request) { res.Metadata.UpdatedAt = time.Now() + // Layer 2: Custom business logic validation + if err := validation.ValidateWithContext(r.Context(), res); err != nil { + respondError(w, http.StatusBadRequest, fmt.Errorf("validation failed: %w", err)) + return + } + if err := storage.SaveBootConfiguration(r.Context(), res); err != nil { respondError(w, http.StatusInternalServerError, fmt.Errorf("failed to save patched BootConfiguration status: %w", err)) return diff --git a/cmd/server/node_handlers_generated.go b/cmd/server/node_handlers_generated.go index fc21c0b..2e6c21c 100644 --- a/cmd/server/node_handlers_generated.go +++ b/cmd/server/node_handlers_generated.go @@ -1,6 +1,6 @@ -// Code generated by Fabrica dev. DO NOT EDIT. +// Code generated by Fabrica v0.4.4-9-g4df0460 (commit: 4df0460). DO NOT EDIT. // Template: server/handlers.go.tmpl -// Generated: 2026-05-05T09:18:40Z +// Generated: 2026-05-11T16:34:21Z // // # Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // @@ -216,6 +216,12 @@ func UpdateNode(w http.ResponseWriter, r *http.Request) { // Update timestamp node.Metadata.UpdatedAt = time.Now() + // Layer 2: Custom business logic validation + if err := validation.ValidateWithContext(r.Context(), node); err != nil { + respondError(w, http.StatusBadRequest, fmt.Errorf("validation failed: %w", err)) + return + } + if err := storage.SaveNode(r.Context(), node); err != nil { respondError(w, http.StatusInternalServerError, fmt.Errorf("failed to save Node: %w", err)) return @@ -288,6 +294,12 @@ func PatchNode(w http.ResponseWriter, r *http.Request) { node.Metadata.UpdatedAt = time.Now() + // Layer 2: Custom business logic validation + if err := validation.ValidateWithContext(r.Context(), node); err != nil { + respondError(w, http.StatusBadRequest, fmt.Errorf("validation failed: %w", err)) + return + } + // Save the patched resource if err := storage.SaveNode(r.Context(), node); err != nil { respondError(w, http.StatusInternalServerError, fmt.Errorf("failed to save patched Node: %w", err)) @@ -342,6 +354,12 @@ func UpdateNodeStatus(w http.ResponseWriter, r *http.Request) { res.Metadata.UpdatedAt = time.Now() + // Layer 2: Custom business logic validation + if err := validation.ValidateWithContext(r.Context(), res); err != nil { + respondError(w, http.StatusBadRequest, fmt.Errorf("validation failed: %w", err)) + return + } + if err := storage.SaveNode(r.Context(), res); err != nil { respondError(w, http.StatusInternalServerError, fmt.Errorf("failed to save Node status: %w", err)) return @@ -414,6 +432,12 @@ func PatchNodeStatus(w http.ResponseWriter, r *http.Request) { res.Metadata.UpdatedAt = time.Now() + // Layer 2: Custom business logic validation + if err := validation.ValidateWithContext(r.Context(), res); err != nil { + respondError(w, http.StatusBadRequest, fmt.Errorf("validation failed: %w", err)) + return + } + if err := storage.SaveNode(r.Context(), res); err != nil { respondError(w, http.StatusInternalServerError, fmt.Errorf("failed to save patched Node status: %w", err)) return diff --git a/pkg/client/client_generated.go b/pkg/client/client_generated.go index bc6ae33..4aca78a 100644 --- a/pkg/client/client_generated.go +++ b/pkg/client/client_generated.go @@ -1,4 +1,4 @@ -// Code generated by Fabrica. DO NOT EDIT. +// Code generated by Fabrica v0.4.4-9-g4df0460-dirty (commit: 4df0460). DO NOT EDIT. // Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // // SPDX-License-Identifier: MIT @@ -12,6 +12,7 @@ // 3. Do NOT edit this file directly - changes will be lost // // Generated client methods for each resource: +// - GetHealth(ctx) - Get service health status // - GetResources(ctx) - List all resources // - GetResource(ctx, uid) - Get specific resource by UID // - CreateResource(ctx, req) - Create new resource @@ -77,6 +78,10 @@ type ErrorResponse struct { Error string `json:"error"` } +// HealthResponse represents a generic service health payload. +// Services may return additional fields beyond the common ones. +type HealthResponse map[string]interface{} + // NewClient creates a new API client func NewClient(baseURL string, httpClient *http.Client) (*Client, error) { if httpClient == nil { @@ -237,6 +242,15 @@ func (c *Client) doPatchRequest(ctx context.Context, endpoint string, patchData return nil } +// GetHealth retrieves the service health endpoint response. +func (c *Client) GetHealth(ctx context.Context) (HealthResponse, error) { + result := HealthResponse{} + if err := c.doRequest(ctx, "GET", "health", nil, &result); err != nil { + return nil, err + } + return result, nil +} + // GetBMCs retrieves all bmcs func (c *Client) GetBMCs(ctx context.Context) ([]v1.BMC, error) { var response []v1.BMC From 5933841726d362966a07e7ecbdec258da0ffb5eb Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Tue, 12 May 2026 08:44:10 -0400 Subject: [PATCH 05/10] feat: update OpenAPI specification generation to include /openapi.json and /docs endpoints Signed-off-by: Alex Lovell-Troy --- cmd/server/openapi_generated.go | 42 ++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/cmd/server/openapi_generated.go b/cmd/server/openapi_generated.go index c7468d3..5c997de 100644 --- a/cmd/server/openapi_generated.go +++ b/cmd/server/openapi_generated.go @@ -1,13 +1,13 @@ -// Code generated by Fabrica. DO NOT EDIT. +// Code generated by Fabrica v0.4.4-9-g4df0460-dirty (commit: 4df0460). DO NOT EDIT. // Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // // SPDX-License-Identifier: MIT // // This file contains OpenAPI 3.0 specification generation for all resources. -// Generated from: pkg/codegen/templates/openapi.go.tmpl +// Generated from: pkg/codegen/templates/server/openapi.go.tmpl // // To modify OpenAPI spec: -// 1. Edit the template file: pkg/codegen/templates/openapi.go.tmpl +// 1. Edit the template file: pkg/codegen/templates/serveropenapi.go.tmpl // 2. Run 'fabrica generate' to regenerate // 3. Do NOT edit this file directly - changes will be lost // @@ -122,6 +122,42 @@ func GenerateOpenAPISpec() *openapi3.T { }) spec.Paths.Set("/health", &openapi3.PathItem{Get: healthOp}) + // Register /openapi.json endpoint (self-referential spec endpoint) + openAPIJsonOp := openapi3.NewOperation() + openAPIJsonOp.OperationID = "getOpenAPISpec" + openAPIJsonOp.Summary = "Get OpenAPI specification" + openAPIJsonOp.Description = "Returns the complete OpenAPI 3.0 specification for this API" + openAPIJsonOp.Tags = []string{"Service"} + openAPIJsonOp.Responses = openapi3.NewResponses() + openAPIJsonOp.Responses.Set("200", &openapi3.ResponseRef{ + Value: openapi3.NewResponse(). + WithDescription("OpenAPI specification"). + WithContent(map[string]*openapi3.MediaType{ + "application/json": { + Schema: &openapi3.SchemaRef{Value: openapi3.NewObjectSchema()}, + }, + }), + }) + spec.Paths.Set("/openapi.json", &openapi3.PathItem{Get: openAPIJsonOp}) + + // Register /docs endpoint (Swagger UI) + docsOp := openapi3.NewOperation() + docsOp.OperationID = "getSwaggerUI" + docsOp.Summary = "API documentation UI" + docsOp.Description = "Returns an interactive Swagger UI for exploring the API" + docsOp.Tags = []string{"Service"} + docsOp.Responses = openapi3.NewResponses() + docsOp.Responses.Set("200", &openapi3.ResponseRef{ + Value: openapi3.NewResponse(). + WithDescription("Swagger UI HTML page"). + WithContent(map[string]*openapi3.MediaType{ + "text/html": { + Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()}, + }, + }), + }) + spec.Paths.Set("/docs", &openapi3.PathItem{Get: docsOp}) + // Register custom (non-Fabrica-generated) paths. // Defined in openapi_extensions.go – safe to edit, never overwritten. registerCustomOpenAPIPaths(spec) From 7957be2d66cb962f28c378919e3a1d859654c542 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Tue, 12 May 2026 09:35:26 -0400 Subject: [PATCH 06/10] feat: update generated code header to include Fabrica version and add health command Signed-off-by: Alex Lovell-Troy --- cmd/client/main.go | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/cmd/client/main.go b/cmd/client/main.go index 1895e43..86f0854 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -1,4 +1,4 @@ -// Code generated by Fabrica. DO NOT EDIT. +// Code generated by Fabrica v0.4.4-10-gd2cc8d5-dirty (commit: d2cc8d5). DO NOT EDIT. // Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // // SPDX-License-Identifier: MIT @@ -12,6 +12,7 @@ // 3. Do NOT edit this file directly - changes will be lost // // Generated commands for each resource: +// - client health // - client bmc [list|get|create|update|patch|delete] // - client bootconfiguration [list|get|create|update|patch|delete] // - client node [list|get|create|update|patch|delete] @@ -126,6 +127,7 @@ func init() { viper.AutomaticEnv() // Add resource commands + rootCmd.AddCommand(healthCmd) rootCmd.AddCommand(bmcCmd) rootCmd.AddCommand(bootconfigurationCmd) rootCmd.AddCommand(nodeCmd) @@ -192,6 +194,27 @@ func printOutput(data interface{}) error { } } +var healthCmd = &cobra.Command{ + Use: "health", + Short: "Get service health status", + RunE: func(cmd *cobra.Command, args []string) error { + c, err := getClient() + if err != nil { + return fmt.Errorf("failed to create client: %w", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + health, err := c.GetHealth(ctx) + if err != nil { + return fmt.Errorf("failed to get health status: %w", err) + } + + return printOutput(health) + }, +} + // setNestedField sets a field in a nested map using dot notation // Example: setNestedField(map, "status.health", "OK") sets map["status"]["health"] = "OK" func setNestedField(target map[string]interface{}, path string, value interface{}) { From 8b23c05ea83465477c68ac5c9b8f46a39c768671 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Tue, 12 May 2026 09:50:24 -0400 Subject: [PATCH 07/10] feat: update health endpoint response schema and example in OpenAPI specification Signed-off-by: Alex Lovell-Troy --- cmd/server/openapi_generated.go | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/cmd/server/openapi_generated.go b/cmd/server/openapi_generated.go index 5c997de..2f247f4 100644 --- a/cmd/server/openapi_generated.go +++ b/cmd/server/openapi_generated.go @@ -1,4 +1,4 @@ -// Code generated by Fabrica v0.4.4-9-g4df0460-dirty (commit: 4df0460). DO NOT EDIT. +// Code generated by Fabrica v0.4.4-12-ga94e778-dirty (commit: a94e778). DO NOT EDIT. // Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // // SPDX-License-Identifier: MIT @@ -114,11 +114,29 @@ func GenerateOpenAPISpec() *openapi3.T { healthOp.Summary = "Service health" healthOp.Description = "Returns service health information" healthOp.Tags = []string{"Service"} + healthResponseSchema := openapi3.NewObjectSchema(). + WithProperty("status", openapi3.NewStringSchema()). + WithProperty("service", openapi3.NewStringSchema()). + WithProperty("version", openapi3.NewStringSchema()). + WithProperty("time", openapi3.NewStringSchema()). + WithRequired([]string{"status"}) + healthExample := map[string]interface{}{ + "status": "ok", + "service": "boot_service", + "version": "1.0.0", + "time": "2026-01-01T00:00:00Z", + } + healthResponseSchema.Example = healthExample healthOp.Responses = openapi3.NewResponses() healthOp.Responses.Set("200", &openapi3.ResponseRef{ Value: openapi3.NewResponse(). WithDescription("Healthy"). - WithJSONSchemaRef(&openapi3.SchemaRef{Value: openapi3.NewObjectSchema()}), + WithContent(map[string]*openapi3.MediaType{ + "application/json": { + Schema: &openapi3.SchemaRef{Value: healthResponseSchema}, + Example: healthExample, + }, + }), }) spec.Paths.Set("/health", &openapi3.PathItem{Get: healthOp}) From d942ef3a3042d1782078b700aaecde2a9437f22e Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Tue, 12 May 2026 17:33:47 -0400 Subject: [PATCH 08/10] Update boot profiles documentation and dependencies - Revised the PROFILES.md documentation to clarify boot profile behaviors, including the distinction between requested profiles and the default profile. - Updated examples for creating boot configurations and outlined the selection algorithm for boot scripts. - Incremented the version of the 'fabrica' dependency from v0.4.4 to v0.4.5 in go.mod and updated the corresponding entries in go.sum. Signed-off-by: Alex Lovell-Troy --- .github/copilot-instructions.md | 42 +- CHANGELOG.md | 83 +++ README.md | 172 +++--- cmd/server/openapi_generated.go | 14 +- config.example.yaml | 248 ++------- docs/API.md | 79 +++ docs/AUTHENTICATION.md | 435 ++++------------ docs/AUTHENTICATION_TESTING.md | 183 +++---- docs/CONFIGURATION.md | 341 ++++-------- docs/PROFILES.md | 892 +++----------------------------- go.mod | 2 +- go.sum | 4 +- 12 files changed, 693 insertions(+), 1802 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 docs/API.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a49ea7c..cc0f1f2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -59,8 +59,6 @@ GOPROXY=direct go build -o bin/boot-service ./cmd/server make build ``` -**Note**: `go.mod` has `replace github.com/openchami/fabrica => ../fabrica` for local development. - ### Running ```bash @@ -68,10 +66,10 @@ make build cp config.example.yaml config.yaml # Run with config file -./bin/boot-service serve +./bin/server serve # Override with flags -./bin/boot-service serve --port 8082 --enable-auth --hsm-url http://localhost:27779 +./bin/server serve --port 8082 --enable-auth --hsm-url http://localhost:27779 ``` ### Testing @@ -148,7 +146,7 @@ Three templates exist: `DefaultIPXETemplate`, `MinimalIPXETemplate`, `ErrorIPXET ### TokenSmith Integration -Authentication is **optional** and controlled via config. Three modes: +The repository contains a reusable `pkg/auth` package with three common modes: ```go // Development - auth disabled @@ -163,6 +161,11 @@ config.JWKSURL = "https://auth.openchami.org/.well-known/jwks.json" config.RequiredScopes = []string{"boot:read"} ``` +**Important current runtime note**: the standalone server in `cmd/server/main.go` +does not currently attach `pkg/auth.CreateMiddleware(...)` to its route tree. +`enable_auth` currently affects startup validation and TokenSmith-backed HSM +service-token exchange, not documented request-time route enforcement. + ### Middleware Application **IMPORTANT**: Apply middleware to router **before** registering routes: @@ -200,17 +203,15 @@ Common scopes: `boot:read`, `boot:write`, `boot:admin`, `node:read`, `node:write # config.yaml structure port: 8080 enable_auth: false -enable_metrics: true +enable_metrics: false enable_legacy_api: true +metrics_port: 9090 hsm_url: "http://localhost:27779" - -auth: - enabled: false - jwks_url: "https://auth.example.com/.well-known/jwks.json" - required_scopes: ["boot:read"] +tokensmith_url: "http://localhost:8080" ``` -Environment variables use prefix `BOOT_SERVICE_` (e.g., `BOOT_SERVICE_PORT=8082`). +Environment variables use prefix `BOOT_SERVICE_` for standard server settings, +plus `TOKENSMITH_*` for bootstrap-token exchange settings. ## External Service Integration @@ -218,17 +219,20 @@ Environment variables use prefix `BOOT_SERVICE_` (e.g., `BOOT_SERVICE_PORT=8082` **Auto-enabled** when `--hsm-url` flag is provided or `hsm_url` is set in config. -**Current Status**: HSM client is initialized and validates connectivity, but not yet fully integrated into the boot script generation pipeline. +**Current Status**: HSM-backed node resolution is wired into the server through +`FlexibleBootScriptController` in `cmd/server/server_extensions.go` when +`hsm_url` is configured. **Implementation**: - HSM client: `pkg/clients/hsm/client.go` - HTTP client for HSM v2 API with caching - Integration service: `pkg/clients/hsm/integration.go` - Wraps HSM client with node provider interface - Flexible controller: `pkg/controllers/bootscript/flexible_controller.go` - Supports pluggable node providers -**Integration Options** (see TODOs in `cmd/server/main.go`): -1. **FlexibleBootScriptController**: Use `NewFlexibleBootScriptController` with HSM provider config -2. **Controller-level**: Add NodeProvider parameter to BootScriptController -3. **Storage-level**: Add HSM fallback in storage.GetNode() for transparent integration +**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(...)` +4. Start optional HSM background sync when enabled **Node resolution with HSM** (when integrated): - XName lookups: Direct HSM component query (`/hsm/v2/State/Components/{xname}`) @@ -237,7 +241,9 @@ Environment variables use prefix `BOOT_SERVICE_` (e.g., `BOOT_SERVICE_PORT=8082` **Caching**: HSM responses are cached (default: 5 minutes) to reduce load on HSM service. -**Current Limitation**: Legacy BSS API handlers use standard BootScriptController which queries local storage only. To enable HSM for boot scripts, modify handlers to accept controller interface and pass FlexibleBootScriptController instance. +**Current Limitation**: The legacy `/boot/v1/bootscript` HTTP route ignores the +`profile` query parameter and always asks the controller to auto-resolve the +best configuration across profiles. ### TokenSmith diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e3bdf38 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,83 @@ + + +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.5] - Unreleased + +### Added + +- Added `GET /health` and a generated `client health` command for quick service checks. +- Added OpenAPI publication endpoints at `GET /openapi.json` and `GET /docs`. +- Added `PATCH` operations for `BMC`, `BootConfiguration`, and `Node` resources. +- Added custom validation hooks for `BMC`, `BootConfiguration`, and `Node` handlers. + +### Changed + +- Regenerated server, client, storage, and OpenAPI surfaces against Fabrica `v0.4.4`. +- Updated generated file headers to include Fabrica version metadata. +- Updated the Docker release build to pass dynamic build arguments into image builds. +- Tightened code generation drift checks around the current Fabrica workflow. +- Refreshed the OpenAPI health schema and example payload to match the current `/health` response. + +## [0.1.4] - 2026-05-06 + +### Added + +- Added HSM group membership lookups and response caching to improve node resolution. + +### Changed + +- Added missing configuration aliases used by HSM-related settings. + +### Fixed + +- Cleaned up HSM client handling and a small lint-related response body close issue. + +## [0.1.3] - 2026-05-05 + +### Added + +- Added the legacy boot script endpoint behind the `enable_legacy_api` feature flag. +- Added explicit code generation drift checks via `make generate-check`. + +### Changed + +- Clarified boot profile behavior and validation in the docs. +- Changed empty-profile boot script selection to auto-resolve the best matching configuration across profiles. +- Updated the local Fabrica workflow in the Makefile and regenerated outputs for the newer generator. +- Refactored server integration setup for clearer handler registration. + +## [0.1.2] - 2026-04-26 + +### Fixed + +- Added the missing OpenAPI API routes. + +## [0.1.1] - 2026-04-15 + +### Changed + +- Added Docker Buildx setup with a custom build image in the release pipeline. + +## [0.1.0] - 2026-04-15 + +### Added + +- Initial tagged release of the Fabrica-generated boot-service API. +- File-backed `BMC`, `BootConfiguration`, and `Node` resource APIs. +- Legacy BSS-compatible boot endpoints and generated Go client support. + +[0.1.5]: https://github.com/OpenCHAMI/boot-service/compare/v0.1.4...HEAD +[0.1.4]: https://github.com/OpenCHAMI/boot-service/compare/v0.1.3...v0.1.4 +[0.1.3]: https://github.com/OpenCHAMI/boot-service/compare/v0.1.2...v0.1.3 +[0.1.2]: https://github.com/OpenCHAMI/boot-service/compare/v0.1.1...v0.1.2 +[0.1.1]: https://github.com/OpenCHAMI/boot-service/compare/v0.1.0...v0.1.1 \ No newline at end of file diff --git a/README.md b/README.md index c32a589..65c6e08 100644 --- a/README.md +++ b/README.md @@ -7,15 +7,27 @@ SPDX-License-Identifier: MIT # boot-service OpenCHAMI Boot Service is a Fabrica-generated REST API for managing node boot -configuration in HPC environments, with legacy BSS-compatible endpoints. +configuration in HPC environments. It exposes modern resource APIs for +`BMC`, `BootConfiguration`, and `Node` objects, plus a legacy BSS-compatible +surface under `/boot/v1/`. + +## 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 +- 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 +- OpenAPI publishing at `/openapi.json` and `/docs` +- A generated CLI client with commands such as `./bin/client health` ## Quick Start ### Prerequisites -- Go 1.24+ - GNU Make -- `pre-commit` (optional, for local CI-style checks) +- A Go toolchain compatible with `go.mod` (this branch currently declares `go 1.26.3`) +- `pre-commit` if you want local CI-style checks ### Configure @@ -23,20 +35,24 @@ configuration in HPC environments, with legacy BSS-compatible endpoints. cp config.example.yaml config.yaml ``` -Configuration precedence (highest to lowest): +Configuration precedence is: 1. Command-line flags -2. Environment variables (prefix `BOOT_SERVICE_`) +2. Environment variables 3. `config.yaml` 4. Built-in defaults +The server reads `BOOT_SERVICE_*` environment variables. TokenSmith bootstrap +settings for HSM auth also support the standardized `TOKENSMITH_*` variables +documented in `config.example.yaml`. + ### Build ```bash make build ``` -Build artifacts: +Local build artifacts: - `bin/server` - `bin/client` @@ -47,124 +63,138 @@ Build artifacts: # Run from source go run ./cmd/server serve -# Run built binary +# Run the built server ./bin/server serve -# Example overrides -./bin/server serve --port 8082 --enable-auth --hsm-url http://localhost:27779 +# Optional client smoke test +./bin/client --server http://localhost:8080 health ``` -## API Surface +Example overrides: -### Health and Docs +```bash +./bin/server serve \ + --port 8082 \ + --enable-auth \ + --hsm-url http://localhost:27779 \ + --tokensmith_url http://localhost:8080 +``` -- `GET /health` -- `GET /openapi.json` -- `GET /docs` +## Current API Behavior -### Resource APIs +### Health, Docs, and Metrics -Generated CRUD/status endpoints for: +- `GET /health` returns a small JSON health response +- `GET /openapi.json` serves the generated OpenAPI document +- `GET /docs` serves Swagger UI +- When metrics are enabled, Prometheus metrics are exposed at `/metrics` and on the separate metrics listener configured by `metrics_port` + +### Modern Resource APIs + +The generated API supports the current resource set: - `/bmcs` - `/bootconfigurations` - `/nodes` -Routes are generated with trailing slashes and normalized by Chi middleware. +The current generated surface includes `PATCH` support for these resources. +Routes are registered with trailing slashes and normalized by Chi middleware. ### Legacy BSS Compatibility When `enable_legacy_api: true`, legacy routes are available under `/boot/v1/`. -## Development Workflow +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. -### Fabrica Generation +### Boot Profiles -Resource definitions live in: +Boot profiles are supported in the boot script controller and modern +`BootConfiguration` resources. When a requested profile is empty, the controller +selects the best matching configuration across profiles; when a requested +profile has no match, it falls back to the default profile. -- `.fabrica.yaml` -- `apis.yaml` -- `apis/boot.openchami.io/v1/*_types.go` +See `docs/PROFILES.md` for the full model and examples. -Regenerate handlers/storage/client/openapi after API changes: +### Authentication and HSM Integration -```bash -# Build automatically re-runs Fabrica generation first -make build +- The repository includes a reusable `pkg/auth` package for JWT, JWKS, scope, and service-token middleware patterns +- The current server binary does not attach `pkg/auth` request middleware in `cmd/server/main.go` +- `enable_auth: true` currently gates TokenSmith-dependent startup behavior and requires `tokensmith_url` +- Supplying `hsm_url` enables HSM-backed node lookups +- If both `enable_auth: true` and `tokensmith_url` are set, the server can exchange a bootstrap token for short-lived HSM service tokens -# CI-style drift check (requires a clean working tree) -make generate-check +## Development Workflow -# Optional: use local Fabrica checkout (sibling ../fabrica) -(cd ../fabrica && go build -o bin/fabrica ./cmd/fabrica) -# Makefile local mode passes --fabrica-source ../fabrica automatically -make generate FABRICA_LOCAL=1 -make generate-check FABRICA_LOCAL=1 -``` +### Fabrica Generation + +Resource definitions live under `apis/boot.openchami.io/v1/` and are wired by +`.fabrica.yaml` and `apis.yaml`. Do not edit `*_generated.go` files manually. -### Tests +Regenerate handlers, storage, client code, and OpenAPI after API changes: ```bash -# Main unit/integration-safe suite (examples excluded) -make test - -# Bootscript integration test (opt-in) -make test-integration - -# Override test timeout -make test TEST_TIMEOUT=4m +make generate +make generate-check ``` -`make test-integration` sets `BOOT_SERVICE_RUN_INTEGRATION=1` and runs: +`make generate-check` requires a clean git tree and fails if regeneration would +change tracked files. -- `TestBootLogicWithExistingData` +If you are working against a local Fabrica checkout, point the Makefile at that +directory instead of using the old `FABRICA_LOCAL=1` pattern: + +```bash +(cd ../fabrica && go build -o bin/fabrica ./cmd/fabrica) +make generate LOCAL_FABRICA=../fabrica +make generate-check LOCAL_FABRICA=../fabrica +``` -### Lint and Local CI +### Test and Lint ```bash +make test +make test-integration make lint make pre-commit-run ``` +`make test-integration` sets `BOOT_SERVICE_RUN_INTEGRATION=1` and runs +`TestBootLogicWithExistingData`. + Useful setup: ```bash make setup-dev ``` -## Configuration Notes - -Key settings are documented in `config.example.yaml` and in -`docs/CONFIGURATION.md`. +## Release Notes -Common environment variable examples: - -```bash -export BOOT_SERVICE_PORT=8082 -export BOOT_SERVICE_ENABLE_AUTH=true -export BOOT_SERVICE_HSM_URL=http://localhost:27779 -./bin/server serve -``` +- `CHANGELOG.md` tracks release history and the next unreleased entry +- `.github/workflows/Release.yaml` publishes tagged releases with GoReleaser on `v*` tags +- `make release-snapshot` creates a local snapshot release for verification ## Docker -- `Dockerfile`: runtime image expecting a prebuilt binary -- `Dockerfile.standalone`: multi-stage standalone build -- The distroless runtime image does not ship `curl` or `wget`; use `/health` - from an external probe instead of an in-container Docker `HEALTHCHECK`. +- `Dockerfile` expects a prebuilt binary and is used by the release flow +- `Dockerfile.standalone` performs a multi-stage container build +- The distroless runtime image does not include `curl` or `wget`; probe `/health` externally instead of using an in-container Docker `HEALTHCHECK` ## Troubleshooting -- If building with local Fabrica replacements and hitting module proxy issues, - try: `GOPROXY=direct go build -o bin/server ./cmd/server` -- If a legacy test appears to require an externally running server, use - `make test-integration` instead of `make test`. +- If local Fabrica development hits Go proxy issues, try `GOPROXY=direct go build -o bin/server ./cmd/server` +- If you want to verify only generated-file drift, start from a clean tree and run `make generate-check` +- If an integration test seems to assume a running server, use `make test-integration` instead of `make test` ## Documentation -- `docs/PROFILES.md` - Boot profiles for configuration management -- `docs/CONFIGURATION.md` - Service configuration guide -- `docs/AUTHENTICATION.md` - JWT authentication with TokenSmith -- `docs/AUTHENTICATION_TESTING.md` - Testing authentication flows +- `docs/PROFILES.md` for boot profile behavior and examples +- `docs/API.md` for the current HTTP endpoint surface +- `docs/CONFIGURATION.md` for configuration details +- `docs/AUTHENTICATION.md` for TokenSmith JWT integration +- `docs/AUTHENTICATION_TESTING.md` for auth test coverage and examples +- `CHANGELOG.md` for release history diff --git a/cmd/server/openapi_generated.go b/cmd/server/openapi_generated.go index 2f247f4..18f8653 100644 --- a/cmd/server/openapi_generated.go +++ b/cmd/server/openapi_generated.go @@ -1,4 +1,4 @@ -// Code generated by Fabrica v0.4.4-12-ga94e778-dirty (commit: a94e778). DO NOT EDIT. +// Code generated by Fabrica. DO NOT EDIT. // Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // // SPDX-License-Identifier: MIT @@ -7,7 +7,7 @@ // Generated from: pkg/codegen/templates/server/openapi.go.tmpl // // To modify OpenAPI spec: -// 1. Edit the template file: pkg/codegen/templates/serveropenapi.go.tmpl +// 1. Edit the template file: pkg/codegen/templates/server/openapi.go.tmpl // 2. Run 'fabrica generate' to regenerate // 3. Do NOT edit this file directly - changes will be lost // @@ -141,6 +141,13 @@ func GenerateOpenAPISpec() *openapi3.T { spec.Paths.Set("/health", &openapi3.PathItem{Get: healthOp}) // Register /openapi.json endpoint (self-referential spec endpoint) + openAPIJSONExample := map[string]interface{}{ + "openapi": "3.0.0", + "info": map[string]interface{}{ + "title": "boot_service API", + "version": "1.0.0", + }, + } openAPIJsonOp := openapi3.NewOperation() openAPIJsonOp.OperationID = "getOpenAPISpec" openAPIJsonOp.Summary = "Get OpenAPI specification" @@ -152,7 +159,8 @@ func GenerateOpenAPISpec() *openapi3.T { WithDescription("OpenAPI specification"). WithContent(map[string]*openapi3.MediaType{ "application/json": { - Schema: &openapi3.SchemaRef{Value: openapi3.NewObjectSchema()}, + Schema: &openapi3.SchemaRef{Value: openapi3.NewObjectSchema()}, + Example: openAPIJSONExample, }, }), }) diff --git a/config.example.yaml b/config.example.yaml index 5a22eec..25dc4c1 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,237 +1,101 @@ -# SPDX-FileCopyrightText: 2025 OpenCHAMI Contributors +# SPDX-FileCopyrightText: 2026 OpenCHAMI Contributors # # SPDX-License-Identifier: MIT # OpenCHAMI Boot Service Configuration Example # -# This is a comprehensive example configuration file for the OpenCHAMI boot service. -# To use this configuration: -# 1. Copy this file to config.yaml: cp config.example.yaml config.yaml -# 2. Customize the settings below for your environment -# 3. Remove or comment out sections you don't need +# This file documents the configuration keys currently read by cmd/server/main.go. +# Older nested sections such as auth:, logging:, tokensmith:, development:, and +# bss: are intentionally omitted because the current server runtime does not +# unmarshal them. # # Configuration precedence (highest to lowest): # 1. Command-line flags -# 2. Environment variables (e.g., BOOT_SERVICE_PORT=8082) +# 2. Environment variables # 3. Configuration file (config.yaml) # 4. Default values # ============================================================================= -# SERVER CONFIGURATION +# SERVER # ============================================================================= -# HTTP server settings -port: 8082 # Port to listen on -host: "0.0.0.0" # Interface to bind to (0.0.0.0 for all interfaces) -read_timeout: 30 # HTTP read timeout in seconds -write_timeout: 30 # HTTP write timeout in seconds -idle_timeout: 120 # HTTP idle timeout in seconds +port: 8080 +host: "0.0.0.0" +read_timeout: 30 +write_timeout: 30 +idle_timeout: 120 # ============================================================================= -# STORAGE CONFIGURATION +# STORAGE # ============================================================================= -# Data storage settings -data_dir: "./data" # Directory for storing boot configurations -storage_type: "file" # Storage backend: "file", "database" (future) - -# Database settings (when storage_type: "database") -# database: -# driver: "postgres" -# host: "localhost" -# port: 5432 -# name: "boot_service" -# user: "boot_user" -# password: "boot_password" -# ssl_mode: "require" -# max_connections: 25 -# connection_timeout: 30 +data_dir: "./data" +storage_type: "file" # ============================================================================= -# FEATURE TOGGLES +# FEATURE FLAGS # ============================================================================= -# Authentication -enable_auth: false # Enable TokenSmith JWT authentication - # Set to true for production environments - -# Metrics and monitoring -enable_metrics: true # Enable Prometheus metrics endpoint -metrics_port: 9092 # Port for metrics endpoint (/metrics) - -# API compatibility -enable_legacy_api: true # Enable legacy BSS-compatible endpoints - # Disable to force use of new API only +enable_auth: false +enable_metrics: false +enable_legacy_api: true +metrics_port: 9090 # ============================================================================= -# AUTHENTICATION CONFIGURATION (when enable_auth: true) +# TOKENSMITH / HSM # ============================================================================= -auth: - # Core authentication settings - enabled: false # Must match enable_auth above - - # JWT validation method (choose one): - - # Option 1: JWKS URL (recommended for production) - jwks_url: "https://auth.openchami.org/.well-known/jwks.json" - jwks_refresh_interval: "1h" # How often to refresh JWKS cache - - # Option 2: Static RSA public key (for development/testing) - # jwt_public_key: | - # -----BEGIN PUBLIC KEY----- - # MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... - # -----END PUBLIC KEY----- - - # JWT validation options - jwt_issuer: "https://auth.openchami.org" # Expected token issuer - jwt_audience: "boot-service" # Expected token audience - validate_expiration: true # Check token expiration - validate_issuer: true # Validate issuer claim - validate_audience: true # Validate audience claim - - # Authorization requirements - required_claims: ["sub", "iss", "aud"] # Required JWT claims - required_scopes: ["boot:read"] # Required OAuth2 scopes +# Top-level TokenSmith URL used by current auth-related startup checks and HSM +# service-token exchange. +tokensmith_url: "" - # Development/testing options (never use in production) - allow_empty_token: false # Allow requests without tokens - non_enforcing: false # Log auth failures but don't block requests +# Optional bootstrap token for HSM service-token exchange. +# Prefer environment variable TOKENSMITH_BOOTSTRAP_TOKEN for real deployments. +# tokensmith_bootstrap_token: "" -# ============================================================================= -# HARDWARE STATE MANAGER INTEGRATION -# ============================================================================= - -# HSM (Hardware State Manager) settings -hsm_url: "http://localhost:27779" # URL of the HSM service - # Set to your HSM endpoint - -# TokenSmith-backed HSM service authentication -# When both hsm_url and tokensmith_url are configured, boot-service exchanges a -# bootstrap token for short-lived service tokens and adds them to HSM requests. -# If enable_auth is false, tokensmith_url is ignored. -# Standardized env vars: TOKENSMITH_URL, TOKENSMITH_BOOTSTRAP_TOKEN, -# TOKENSMITH_TARGET_SERVICE, TOKENSMITH_BOOTSTRAP_POLICY_SCOPES_HINT, -# TOKENSMITH_REFRESH_SKEW_SEC -# Deprecated legacy env var still supported: TOKENSMITH_SCOPES -tokensmith_url: "http://localhost:8080" tokensmith_target_service: "hsm" -tokensmith_bootstrap_policy_scopes_hint: "hsm:read" -# tokensmith_scopes: "hsm:read" # Deprecated legacy key -tokensmith_refresh_skew_sec: 120 -# tokensmith_bootstrap_token: "" # Prefer env var for secrets -# Environment fallback: TOKENSMITH_BOOTSTRAP_TOKEN -# HSM authentication (when HSM requires auth) -# hsm_auth: -# type: "service_token" # Authentication type for HSM -# service_name: "boot-service" -# token_endpoint: "http://tokensmith:8080/token" - -# ============================================================================= -# EXTERNAL SERVICES -# ============================================================================= +# Optional comma-separated scope hint used for diagnostics when exchanging HSM +# service tokens. +tokensmith_bootstrap_policy_scopes_hint: "" -# TokenSmith authentication service (when enable_auth: true) -tokensmith: - url: "http://localhost:8080" # TokenSmith service URL - timeout: 30 # Request timeout in seconds +# Deprecated legacy alias still accepted by current code. +# tokensmith_scopes: "hsm:read" - # Service-to-service authentication - service_auth: - enabled: false # Enable service tokens - service_name: "boot-service" # This service's identifier - token_endpoint: "/token" # Token endpoint path - -# BSS (Boot Script Service) integration -bss: - enabled: false # Enable BSS integration - url: "http://localhost:27778" # BSS service URL - timeout: 30 # Request timeout in seconds - -# ============================================================================= -# LOGGING AND MONITORING -# ============================================================================= - -# Logging configuration -logging: - level: "info" # Log level: debug, info, warn, error - format: "json" # Log format: json, text - output: "stdout" # Log output: stdout, stderr, file - # file: "/var/log/boot-service.log" # Log file (when output: file) +tokensmith_refresh_skew_sec: 120 -# Health check configuration -health: - enabled: true # Enable health check endpoint - endpoint: "/health" # Health check URL path - timeout: 5 # Health check timeout in seconds +# HSM-backed node resolution is enabled when this URL is provided. +hsm_url: "" +hsm_sync_enabled: true +hsm_sync_interval: 5 # ============================================================================= -# PERFORMANCE AND SCALING +# NOTES # ============================================================================= -# Request limits -limits: - max_request_size: "10MB" # Maximum request body size - max_concurrent: 100 # Maximum concurrent requests - rate_limit: 1000 # Requests per minute per IP +# Reserved flag present in the current config struct but not documented as an +# active runtime auth path in the server entrypoint. +# jwks_endpoint: "https://auth.example.com/.well-known/jwks.json" -# Caching (future feature) -# cache: -# enabled: false -# type: "memory" # Cache type: memory, redis -# ttl: "5m" # Cache TTL -# max_size: "100MB" # Maximum cache size +# - The current server always exposes /boot/v1/bootscript. +# - enable_legacy_api controls the rest of the legacy BSS-compatible endpoints. +# - 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. # ============================================================================= -# DEVELOPMENT AND TESTING +# EXAMPLE OVERRIDES # ============================================================================= -# Development mode settings -development: - enabled: false # Enable development mode - cors_enabled: true # Enable CORS for browser testing - cors_origins: ["*"] # Allowed CORS origins - debug_endpoints: false # Enable debug/diagnostic endpoints - mock_services: false # Use mock external services - -# ============================================================================= -# DEPLOYMENT ENVIRONMENT EXAMPLES -# ============================================================================= - -# Uncomment and modify one of these sections for your deployment environment: - -# --- Development Environment --- -# enable_auth: false +# Minimal metrics-enabled setup: # enable_metrics: true -# logging: -# level: "debug" -# development: -# enabled: true -# debug_endpoints: true -# --- Production Environment --- +# HSM integration without auth-backed token exchange: +# hsm_url: "http://localhost:27779" + +# HSM integration with TokenSmith-backed service tokens: # enable_auth: true -# enable_metrics: true -# auth: -# enabled: true -# jwks_url: "https://auth.openchami.org/.well-known/jwks.json" -# jwt_issuer: "https://auth.openchami.org" -# jwt_audience: "boot-service" -# required_scopes: ["boot:read"] -# logging: -# level: "info" -# format: "json" - -# --- Kubernetes/Container Environment --- -# port: 8080 -# host: "0.0.0.0" -# data_dir: "/data" -# auth: -# jwks_url: "http://tokensmith:8080/.well-known/jwks.json" -# jwt_issuer: "openchami-tokensmith" -# jwt_audience: "openchami-cluster" -# hsm_url: "http://smd:27779" -# logging: -# format: "json" -# output: "stdout" +# tokensmith_url: "http://localhost:8080" +# hsm_url: "http://localhost:27779" +# tokensmith_target_service: "hsm" +# tokensmith_bootstrap_policy_scopes_hint: "hsm:read" diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..11a5c8f --- /dev/null +++ b/docs/API.md @@ -0,0 +1,79 @@ + + +# API Reference + +This document summarizes the HTTP API surface currently exposed by the boot +service server. + +## Public Endpoints + +These routes are registered directly in the server entrypoint: + +- `GET /health` +- `GET /openapi.json` +- `GET /docs` + +When metrics are enabled, Prometheus metrics are also exposed at: + +- `GET /metrics` + +and on the separate metrics listener configured by `metrics_port`. + +## Modern Resource APIs + +The generated REST API exposes three resource types: + +- `BMC` at `/bmcs` +- `BootConfiguration` at `/bootconfigurations` +- `Node` at `/nodes` + +For each resource type, the generated routes currently support: + +- `GET /resource` +- `POST /resource` +- `GET /resource/{uid}` +- `PUT /resource/{uid}` +- `PATCH /resource/{uid}` +- `DELETE /resource/{uid}` +- `PUT /resource/{uid}/status` +- `PATCH /resource/{uid}/status` + +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 + +The server always exposes: + +- `GET /boot/v1/bootscript` + +When `enable_legacy_api: true`, it also exposes the rest of the BSS-compatible +surface: + +- `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` + +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. + +## Generated Client + +`make build` produces a generated CLI client at `bin/client`. + +Current top-level commands include: + +- `client health` +- `client bmc ...` +- `client bootconfiguration ...` +- `client node ...` + +Use `./bin/client --help` for the full generated command tree. diff --git a/docs/AUTHENTICATION.md b/docs/AUTHENTICATION.md index 625a1e9..9805c5d 100644 --- a/docs/AUTHENTICATION.md +++ b/docs/AUTHENTICATION.md @@ -1,415 +1,156 @@ # Authentication with OpenCHAMI TokenSmith -This document describes how the OpenCHAMI boot service integrates with the [OpenCHAMI TokenSmith](https://github.com/OpenCHAMI/tokensmith) middleware for JWT-based authentication and authorization. +This repository has two authentication-related surfaces: -## Overview +1. a reusable `pkg/auth` package that supports JWT, JWKS, scopes, and service-token middleware +2. the `cmd/server` runtime configuration, which currently uses only a narrower subset of auth-related settings -The boot service uses the OpenCHAMI TokenSmith middleware to provide: +This document covers both, and keeps them separate. -- **JWT Token Validation**: Support for RSA and ECDSA signed tokens -- **JWKS Integration**: Automatic key rotation via JSON Web Key Sets -- **Scope-based Authorization**: Fine-grained access control -- **Service-to-Service Authentication**: Internal service communication -- **Flexible Configuration**: Multiple deployment scenarios +## Package-Level Auth Support -## Architecture +The `pkg/auth` package currently supports: -``` -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Client/User │ │ TokenSmith │ │ Boot Service │ -│ │ │ Auth Service │ │ │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ - │ │ │ - │ 1. Authenticate │ │ - ├──────────────────────→│ │ - │ │ │ - │ 2. JWT Token │ │ - │←──────────────────────┤ │ - │ │ │ - │ 3. API Request + JWT │ │ - ├─────────────────────────────────────────────→│ - │ │ │ - │ │ 4. Validate Token │ - │ │ (JWKS or Static Key) │ - │ │←──────────────────────┤ - │ │ │ - │ │ 5. Token Valid │ - │ ├──────────────────────→│ - │ │ │ - │ 6. API Response │ │ - │←─────────────────────────────────────────────┤ -``` - -## Quick Start +- static RSA public key validation +- JWKS-backed validation +- issuer and audience checks +- required claim and required scope checks +- non-enforcing and dev modes +- service-to-service middleware +- claim extraction from request context -### 1. Basic Configuration - -Create an authentication configuration: +### Quick Example ```go import "github.com/openchami/boot-service/pkg/auth" -// For development (authentication disabled) -config := auth.DevConfig() - -// For production config := auth.DefaultConfig() -config.JWKSURL = "https://your-auth-server/.well-known/jwks.json" -config.JWTIssuer = "your-issuer" +config.JWKSURL = "https://auth.example.com/.well-known/jwks.json" +config.JWTIssuer = "https://auth.example.com" config.JWTAudience = "boot-service" -``` - -### 2. Create Middleware +config.RequiredScopes = []string{"boot:read"} -```go authMiddleware := config.CreateMiddleware(logger) ``` -### 3. Apply to Routes +Apply it to routes in the usual Chi pattern: ```go -import "github.com/go-chi/chi/v5" - r := chi.NewRouter() - -// Public routes -r.Get("/health", healthHandler) - -// Protected routes -r.Group(func(r chi.Router) { - r.Use(authMiddleware) - r.Get("/boot/{nodeId}", bootHandler) -}) +r.Use(authMiddleware) ``` -## Configuration Options - -### Core Settings - -- **`enabled`**: Enable/disable authentication entirely -- **`jwksUrl`**: URL to fetch JSON Web Key Set (recommended) -- **`jwtPublicKey`**: Static RSA public key in PEM format -- **`jwksRefreshInterval`**: How often to refresh JWKS cache - -### Validation Options +### Other Package Helpers -- **`validateExpiration`**: Check token expiration (recommended: true) -- **`validateIssuer`**: Validate token issuer (recommended: true in production) -- **`validateAudience`**: Validate token audience (recommended: true in production) -- **`requiredClaims`**: List of required claims (e.g., ["sub", "iss", "aud"]) -- **`requiredScopes`**: List of required scopes (e.g., ["boot:read"]) +The package also exposes: -### Development Options +- `auth.DevConfig()` +- `auth.NonEnforcingConfig()` +- `auth.CreateScopeMiddleware(...)` +- `auth.CreateServiceTokenMiddleware(...)` +- `auth.GetClaimsFromRequest(r)` -- **`allowEmptyToken`**: Allow requests without tokens (dev only) -- **`nonEnforcing`**: Log errors but don't block requests (testing only) +## Current Server Runtime Behavior -## Configuration Examples +The standalone server binary does **not** currently attach `pkg/auth` +middleware inside `cmd/server/main.go`. -> **📝 Note**: For comprehensive configuration examples with detailed documentation, see `config.example.yaml` in the project root. Copy this file to `config.yaml` and customize for your environment. +As of the current branch, the auth-related runtime behavior is: -### Development Environment +- `enable_auth: true` requires top-level `tokensmith_url` +- `enable_auth` is used for TokenSmith-dependent startup behavior and HSM service-token exchange +- if `enable_auth: true`, `hsm_url` is set, and `tokensmith_url` is set, then a bootstrap token is required +- the generated AuthZ classifier exists in `cmd/server/authz_classifier.go`, but the server entrypoint does not currently wire request auth middleware to the route tree -```yaml -auth: - enabled: false # Authentication disabled -``` - -### Production with JWKS +That means you should not assume the HTTP resource APIs are JWT-protected just +because `enable_auth` is true. -```yaml -auth: - enabled: true - jwks_url: "https://auth.openchami.org/.well-known/jwks.json" - jwks_refresh_interval: "1h" - jwt_issuer: "https://auth.openchami.org" - jwt_audience: "boot-service" - validate_expiration: true - validate_issuer: true - validate_audience: true - required_claims: ["sub", "iss", "aud"] - required_scopes: ["boot:read"] -``` +## Runtime Configuration Inputs -### OpenCHAMI Cluster +For the current server binary, the live top-level auth-related inputs are: ```yaml -auth: - enabled: true - jwks_url: "http://tokensmith:8080/.well-known/jwks.json" - jwt_issuer: "openchami-tokensmith" - jwt_audience: "openchami-cluster" - required_claims: ["sub", "cluster_id", "openchami_id"] - required_scopes: ["boot:read"] -``` - -## Usage Patterns - -### Basic Authentication - -```go -// All routes require valid JWT -r.Use(authMiddleware) -``` - -### Scope-based Authorization - -```go -import "github.com/openchami/boot-service/pkg/auth" - -// Require specific scope -readScope := auth.CreateScopeMiddleware("boot:read") -writeScope := auth.CreateScopeMiddleware("boot:write") - -r.Group(func(r chi.Router) { - r.Use(authMiddleware) - r.Use(readScope) - r.Get("/boot/list", listBootConfigsHandler) -}) - -r.Group(func(r chi.Router) { - r.Use(authMiddleware) - r.Use(writeScope) - r.Post("/boot", createBootConfigHandler) -}) -``` - -### Service-to-Service Authentication - -```go -// Require service token for internal APIs -serviceAuth := auth.CreateServiceTokenMiddleware("boot-service") - -r.Group(func(r chi.Router) { - r.Use(authMiddleware) - r.Use(serviceAuth) - r.Get("/internal/stats", internalStatsHandler) -}) -``` - -### Accessing Claims in Handlers - -```go -func bootHandler(w http.ResponseWriter, r *http.Request) { - // Get claims from request context - claims, err := auth.GetClaimsFromRequest(r) - if err != nil { - http.Error(w, "Failed to get claims", http.StatusInternalServerError) - return - } - - // Access user information - userID := claims.Subject - email := claims.Email - scopes := claims.Scope - clusterID := claims.ClusterID - - // Use claims for business logic - log.Printf("Boot request from user %s (%s) with scopes %v", - userID, email, scopes) -} +enable_auth: false +tokensmith_url: "" +tokensmith_target_service: "hsm" +tokensmith_bootstrap_policy_scopes_hint: "" +tokensmith_refresh_skew_sec: 120 +# tokensmith_bootstrap_token: "" ``` -## Token Structure +Standardized environment variables: -The TokenSmith middleware expects tokens with the following claims structure: +- `TOKENSMITH_URL` +- `TOKENSMITH_BOOTSTRAP_TOKEN` +- `TOKENSMITH_TARGET_SERVICE` +- `TOKENSMITH_BOOTSTRAP_POLICY_SCOPES_HINT` +- `TOKENSMITH_REFRESH_SKEW_SEC` -### Standard JWT Claims (RFC 7519) +Deprecated compatibility environment variable: -- **`iss`** (issuer): Token issuer -- **`sub`** (subject): User or service identifier -- **`aud`** (audience): Intended recipients -- **`exp`** (expiration): Token expiration time -- **`iat`** (issued at): Token issuance time -- **`nbf`** (not before): Token validity start time +- `TOKENSMITH_SCOPES` -### OpenCHAMI Claims +## JWKS and Static-Key Notes -- **`cluster_id`**: OpenCHAMI cluster identifier -- **`openchami_id`**: OpenCHAMI entity identifier -- **`scope`**: Array of granted scopes -- **`groups`**: User group memberships +JWKS and static RSA key support are implemented in `pkg/auth`, but they are not +currently part of the server's documented top-level runtime config path. -### NIST SP 800-63B Claims (for high-assurance environments) +If you are embedding the auth package in another service or extending this one, +use the package config directly rather than copying old nested `auth:` YAML +examples into the current server config. -- **`auth_level`**: Authentication assurance level (IAL1, IAL2, IAL3) -- **`auth_factors`**: Number of authentication factors used -- **`auth_methods`**: Authentication methods (e.g., ["password", "mfa"]) -- **`session_id`**: Session identifier -- **`session_exp`**: Session expiration time +## HSM Service-Token Exchange -## Security Considerations +When all of the following are true: -### Production Deployment +- `enable_auth: true` +- `hsm_url` is set +- `tokensmith_url` is set -1. **Always use HTTPS** in production environments -2. **Use JWKS** for automatic key rotation -3. **Validate issuer and audience** claims -4. **Require appropriate scopes** for sensitive operations -5. **Set reasonable token lifetimes** (e.g., 1 hour for user tokens, 5 minutes for service tokens) +the server exchanges a bootstrap token for short-lived HSM service tokens and +uses those for HSM API calls. -### Token Scopes +If `enable_auth: false`, the server logs that the TokenSmith URL is ignored for +HSM integration. -Common scopes for boot service operations: +## Testing and Examples -- **`boot:read`**: Read boot configurations and node information -- **`boot:write`**: Create/update boot configurations -- **`boot:admin`**: Administrative operations -- **`node:read`**: Read node information -- **`node:write`**: Update node state +The auth package has integration tests for: -### Error Handling +- non-enforcing mode +- static-key token validation +- scope middleware +- service-to-service token checks +- expired and invalid token handling -The middleware returns appropriate HTTP status codes: - -- **`401 Unauthorized`**: Missing or invalid token -- **`403 Forbidden`**: Insufficient scopes or permissions -- **`500 Internal Server Error`**: Server-side errors (e.g., JWKS fetch failures) - -## Integration with OpenCHAMI Ecosystem - -### TokenSmith Service - -The boot service integrates with the OpenCHAMI TokenSmith service for: - -- **Token Validation**: Verify JWT signatures and claims -- **JWKS Endpoints**: Automatic key rotation -- **Service Tokens**: Internal service communication - -### Fabrica Policy Engine - -For advanced authorization scenarios, the boot service can integrate with Fabrica for: - -- **Policy-based Access Control**: Complex authorization rules -- **Role-based Access**: User role management -- **Resource-specific Permissions**: Fine-grained access control - -### BSS Integration - -When deployed with BSS (Boot Script Service): - -- **Shared Authentication**: Common JWT validation -- **Consistent Authorization**: Uniform scope requirements -- **Service Discovery**: Automatic service token generation - -## Monitoring and Observability - -### Logging - -Authentication events are logged at appropriate levels: - -```go -2025-01-08T10:30:15Z INFO Authentication successful user=user123 scopes=[boot:read,boot:write] -2025-01-08T10:30:16Z WARN Token validation failed error="token expired" -2025-01-08T10:30:17Z ERROR JWKS refresh failed error="connection timeout" -``` - -### Metrics - -Key metrics to monitor: - -- **Authentication success/failure rates** -- **Token validation latency** -- **JWKS refresh frequency and errors** -- **Scope authorization failures** - -### Health Checks - -The authentication system provides health indicators: - -- **JWKS connectivity** (when using JWKS URLs) -- **Token validation performance** -- **Configuration validation** +See `docs/AUTHENTICATION_TESTING.md` for the current test surface. ## Troubleshooting -### Common Issues - -1. **"missing authorization header"** - - Ensure clients send `Authorization: Bearer ` header - -2. **"invalid token"** - - Check token expiration and signature - - Verify JWKS URL is accessible - - Confirm issuer/audience claims - -3. **"insufficient scope"** - - Verify required scopes are granted to the user/service - - Check scope configuration in auth settings - -4. **"JWKS fetch failed"** - - Verify JWKS URL is accessible from boot service - - Check network connectivity and DNS resolution - - Confirm JWKS endpoint returns valid JSON - -### Debug Configuration - -For debugging authentication issues: - -```yaml -auth: - enabled: true - nonEnforcing: true # Log errors but don't block - # ... other settings -``` - -This logs authentication failures without blocking requests, useful for troubleshooting in staging environments. - -## Migration Guide - -### From Custom Auth to TokenSmith - -1. **Update Dependencies**: - ```bash - go get github.com/openchami/tokensmith/middleware - ``` - -2. **Replace Custom Middleware**: - ```go - // Old - customAuth := myapp.CreateAuthMiddleware(config) - - // New - authConfig := auth.DefaultConfig() - tokensmithAuth := authConfig.CreateMiddleware(logger) - ``` - -3. **Update Configuration**: - - Convert custom auth settings to TokenSmith format - - Add JWKS URL for automatic key rotation - - Define required scopes and claims +### The server asks for `tokensmith_url` -4. **Test Integration**: - - Verify token validation works correctly - - Test scope-based authorization - - Confirm error handling behavior +That is expected when `enable_auth: true`. It is a startup validation rule in +the current server config path. -### From No Auth to TokenSmith +### A token is accepted in package tests but not by your app -1. **Start with Development Config**: - ```go - config := auth.DevConfig() // Authentication disabled - ``` +Check whether you are using the standalone server binary or wiring `pkg/auth` +middleware in your own router. They are not the same thing today. -2. **Gradually Enable Features**: - - Enable authentication with permissive settings - - Add required claims validation - - Implement scope-based authorization +### You expected JWKS config in `config.yaml` to protect routes -3. **Production Hardening**: - - Enable all validation options - - Define strict scope requirements - - Configure proper JWKS integration +The current server entrypoint does not wire `pkg/auth.CreateMiddleware` into the +route tree. Package-level support exists, but runtime enforcement in the server +binary is not documented as active. ## Additional Resources +- [AUTHENTICATION_TESTING.md](AUTHENTICATION_TESTING.md) for auth test coverage +- [CONFIGURATION.md](CONFIGURATION.md) for current server runtime keys - [OpenCHAMI TokenSmith Documentation](https://github.com/OpenCHAMI/tokensmith) -- [JWT Best Practices (RFC 8725)](https://datatracker.ietf.org/doc/html/rfc8725) -- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html) -- [OAuth 2.0 JWT Bearer Token Profiles](https://datatracker.ietf.org/doc/html/rfc7523) diff --git a/docs/AUTHENTICATION_TESTING.md b/docs/AUTHENTICATION_TESTING.md index 66e38fe..4f54bb1 100644 --- a/docs/AUTHENTICATION_TESTING.md +++ b/docs/AUTHENTICATION_TESTING.md @@ -1,165 +1,102 @@ # Authentication Testing Framework -This document describes the comprehensive authentication testing framework built for the OpenCHAMI boot service using TokenSmith middleware. +This document describes the auth test utilities and integration tests in +`pkg/auth`. -## 🎉 Successfully Implemented +It covers the package-level auth helpers, not route protection in the current +server entrypoint. -### ✅ TokenSmith Integration -- **Complete middleware integration** with OpenCHAMI TokenSmith -- **RSA key parsing** from PEM format for static key validation -- **NIST-compliant JWT tokens** with all required claims (auth_level, auth_factors, etc.) -- **Multiple authentication modes**: disabled, non-enforcing, enforcing -- **Scope-based authorization** with granular permissions +## What Is Covered Today -### ✅ Testing Framework -- **Local JWT generation** with properly formatted RSA key pairs -- **Test utilities** for creating tokens with various scopes and claims -- **Integration tests** covering all authentication scenarios -- **Example server** demonstrating practical usage patterns +The current integration tests cover: -## Testing Capabilities +- non-enforcing mode +- static-key JWT validation +- scope middleware behavior +- service-to-service token checks +- expired token handling +- invalid token handling -### 1. **Non-Enforcing Mode** ✅ -```go -config := NonEnforcingConfig() -// - Allows requests without tokens -// - Logs authentication errors but doesn't block -// - Perfect for development and debugging -``` +The current tests use locally generated RSA keys and static-key validation. -### 2. **Static Key Validation** ✅ -```go -keyPair, _ := GenerateTestKeyPair() -config := CreateStaticKeyConfig(keyPair.PublicKeyPEM) -// - Uses locally generated RSA keys -// - Validates JWT signatures properly -// - Supports all TokenSmith claim requirements -``` +## Key Test Helpers -### 3. **Scope-Based Authorization** ✅ -```go -// Create tokens with specific scopes -readToken := CreateTestTokenWithScopes(keyPair, []string{"read"}) -writeToken := CreateTestTokenWithScopes(keyPair, []string{"write"}) +From `pkg/auth/testing.go`: -// Protect routes with scope requirements -middleware := CreateScopeMiddleware("read", "write") -``` +- `TestingConfig(publicKeyPEM string)` +- `NonEnforcingConfig()` +- `GenerateTestKeyPair()` +- `CreateTestToken(...)` +- `CreateTestTokenWithScopes(...)` +- `CreateServiceToken(...)` +- `CreateStaticKeyConfig(...)` -### 4. **Service-to-Service Authentication** ✅ -```go -// Service tokens with proper NIST compliance -serviceToken := CreateServiceToken(keyPair, "client-service", "boot-service", []string{"service:boot"}) -``` +## Example Test Flows -## Test Results +### Non-Enforcing Mode -All tests passing: -``` -=== RUN TestAuthenticationIntegration -=== RUN TestAuthenticationIntegration/NonEnforcingMode ✅ PASS -=== RUN TestAuthenticationIntegration/ValidTokenWithStaticKey ✅ PASS -=== RUN TestAuthenticationIntegration/ScopeBasedAuthorization ✅ PASS -=== RUN TestAuthenticationIntegration/ServiceToServiceAuthentication ✅ PASS -=== RUN TestAuthenticationIntegration/ExpiredTokenHandling ✅ PASS -=== RUN TestAuthenticationIntegration/InvalidTokenHandling ✅ PASS ---- PASS: TestAuthenticationIntegration (0.72s) +```go +config := NonEnforcingConfig() +middleware := config.CreateMiddleware(nil) ``` -## Key Issues Resolved +### Static-Key Validation -### 🔧 **RSA Key Parsing** -**Problem**: TokenSmith middleware expected `*rsa.PublicKey` but was getting string -**Solution**: Added proper PEM parsing in `config.go`: ```go -keyPem, _ := pem.Decode([]byte(c.JWTPublicKey)) -pubKey, _ := x509.ParsePKIXPublicKey(keyPem.Bytes) -rsaKey := pubKey.(*rsa.PublicKey) +keyPair, _ := GenerateTestKeyPair() +config := CreateStaticKeyConfig(keyPair.PublicKeyPEM) +middleware := config.CreateMiddleware(nil) ``` -### 🔧 **Non-Enforcing Mode** -**Problem**: Middleware was still requiring tokens even in non-enforcing mode -**Solution**: Used `AllowEmptyToken: true` option: -```go -config.AllowEmptyToken = true // Allow requests without tokens -config.NonEnforcing = true // Log errors but don't fail -``` +### Scope Checks -### 🔧 **NIST Claims Compliance** -**Problem**: TokenSmith requires specific claims for NIST SP 800-63B compliance -**Solution**: Added all required claims to test tokens: ```go -claims := &token.TSClaims{ - AuthLevel: "IAL2", - AuthFactors: 2, - AuthMethods: []string{"password", "mfa"}, - SessionID: "test-session-123", - SessionExp: now.Add(1 * time.Hour).Unix(), - AuthEvents: []string{"login"}, - // ... other standard JWT claims -} +tokenWithReadScope, _ := CreateTestTokenWithScopes(keyPair, []string{"read"}) +scopeMiddleware := CreateScopeMiddleware("read", "write") ``` -### 🔧 **Scope Consistency** -**Problem**: Mismatched scope names between tokens and middleware expectations -**Solution**: Standardized on simple scope names (`read`, `write`, `service:boot`) +### Service-to-Service Checks -## Usage Examples - -### Development Mode (No Auth) ```go -config := auth.DevConfig() // Disabled authentication -middleware := config.CreateMiddleware(logger) +serviceToken, _ := CreateServiceToken(keyPair, "test-service", "boot-service", []string{"service:boot"}) +serviceMiddleware := CreateServiceTokenMiddleware("boot-service") ``` -### Non-Enforcing Mode (Logs Only) -```go -config := auth.NonEnforcingConfig() // Allows empty tokens -middleware := config.CreateMiddleware(logger) -``` +## What Is Not Covered Here -### Production Mode (Full Validation) -```go -config := auth.DefaultConfig() -config.JWTPublicKey = publicKeyPEM -middleware := config.CreateMiddleware(logger) -``` +- attaching auth middleware to the standalone server's generated routes +- policy engine wiring in `cmd/server` +- HSM bootstrap-token exchange in `cmd/server/main.go` -### Scope Protection -```go -// Protect routes requiring specific scopes -readOnlyMiddleware := auth.CreateScopeMiddleware("read") -writeMiddleware := auth.CreateScopeMiddleware("read", "write") -``` +Those are separate concerns from the package tests. + +## JWKS Status -## Manual Testing +JWKS support already exists in `pkg/auth/config.go`. -The example server (`examples/auth-testing/main.go`) provides: -- **Generated test tokens** for immediate use -- **Multiple auth configurations** (dev, non-enforcing, enforcing) -- **Sample curl commands** for manual testing -- **Different route protections** demonstrating scope requirements +Current auth integration tests focus on static-key validation because it is a +cheap and deterministic test surface. A lack of JWKS-specific tests does not +mean JWKS support is absent. -Run with: `go run examples/auth-testing/main.go` +## Example Server -## Files Created/Modified +The example server at `examples/auth-testing/main.go` remains useful for manual +exploration of the auth package behavior. -- `pkg/auth/config.go` - RSA key parsing fixes -- `pkg/auth/testing.go` - Test utilities and token generation -- `pkg/auth/integration_test.go` - Comprehensive integration tests -- `examples/auth-testing/main.go` - Practical demonstration server +Run it with: -## Next Steps +```bash +go run examples/auth-testing/main.go +``` -1. **JWKS Support**: Add integration with JWKS URLs for dynamic key rotation -2. **Policy Integration**: Connect with OpenCHAMI policy engines for dynamic authorization -3. **Metrics**: Add authentication metrics and monitoring -4. **Documentation**: Complete API documentation with authentication examples +## Summary -The authentication framework is now **production-ready** with comprehensive testing capabilities for both local development and integration with OpenCHAMI TokenSmith in deployed environments. +The auth package is tested and reusable. The most important distinction is that +these package tests do not imply that the standalone server binary is currently +wiring request auth middleware onto its route tree. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index ae95656..2eb6a17 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -1,311 +1,186 @@ # Configuration Guide -This document explains how to configure the OpenCHAMI boot service. +This document describes the configuration keys the current server binary reads +from `cmd/server/main.go`. + +If a key is not listed here, assume it is not currently consumed by the server +startup path. ## Quick Start -1. **Copy the example configuration**: +1. Copy the example configuration: + ```bash cp config.example.yaml config.yaml ``` -2. **Edit for your environment**: - ```bash - # Edit the configuration file - nano config.yaml # or your preferred editor - ``` +2. Start the service: -3. **Start the service**: ```bash - ./bin/boot-service serve + ./bin/server serve ``` -## Configuration Methods - -The boot service supports multiple configuration methods in order of precedence: +3. Override settings with flags or environment variables when needed. -1. **Command-line flags** (highest priority) -2. **Environment variables** (e.g., `BOOT_SERVICE_PORT=8082`) -3. **Configuration file** (`config.yaml`) -4. **Default values** (lowest priority) +## Configuration Precedence -## Boot Profiles +The server resolves configuration in this order: -**⚠️ This section is an overview. See [PROFILES.md](PROFILES.md) for comprehensive documentation.** +1. Command-line flags +2. Environment variables +3. `config.yaml` +4. Built-in defaults -Boot profiles allow you to organize boot configurations by operational scenario or node type: +The standard server environment variable prefix is `BOOT_SERVICE_`. TokenSmith +bootstrap settings for HSM auth also support standardized `TOKENSMITH_*` +environment variables. -```bash -# Standard compute profile -curl "http://boot-service:8080/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=compute" - -# Debug profile -curl "http://boot-service:8080/boot/v1/bootscript?host=x0c0s0b0n0&profile=debug" - -# Default profile (omit profile parameter) -curl "http://boot-service:8080/boot/v1/bootscript?nid=42" -``` +## Supported Runtime Keys -Boot configurations specify a profile in their spec: +### Server and Storage ```yaml -apiVersion: boot.openchami.io/v1 -kind: BootConfiguration -metadata: - name: compute-standard -spec: - profile: "compute" # Optional profile identifier - kernel: "http://files.openchami.org/vmlinuz-compute" - initrd: "http://files.openchami.org/initramfs-compute.img" - params: "console=ttyS0,115200" -``` - -Key behaviors: - -- Configurations without a profile are treated as the **default** profile -- When requesting a profile not found, the service falls back to the default -- Higher priority scores and exact identifier matches determine selection within a profile - -→ **Read [PROFILES.md](PROFILES.md)** for detailed examples, selection logic, best practices, and migration guides. - -### Configuration File - -The recommended approach for most deployments is to use a configuration file: - -- **Location**: `config.yaml` in the working directory -- **Format**: YAML -- **Example**: See `config.example.yaml` for comprehensive documentation -- **Git**: The actual `config.yaml` is gitignored to prevent accidental commits of sensitive data - -### Environment Variables - -Useful for containerized deployments: - -```bash -export BOOT_SERVICE_PORT=8082 -export BOOT_SERVICE_ENABLE_AUTH=true -export BOOT_SERVICE_HSM_URL=http://smd:27779 -./bin/boot-service serve -``` - -### Command-line Flags - -Useful for quick testing and overrides: - -```bash -./bin/boot-service serve --port 8082 --enable-auth --hsm-url http://localhost:27779 -``` - -## Configuration Sections - -### Core Server Settings - -```yaml -port: 8082 # HTTP server port -host: "0.0.0.0" # Bind interface -read_timeout: 30 # Request read timeout (seconds) -write_timeout: 30 # Response write timeout (seconds) -idle_timeout: 120 # Connection idle timeout (seconds) -``` - -### Storage Configuration - -```yaml -data_dir: "./data" # Directory for file storage -storage_type: "file" # Storage backend (file, database) -``` - -### Feature Toggles - -```yaml -enable_auth: false # Enable JWT authentication -enable_metrics: true # Enable Prometheus metrics -enable_legacy_api: true # Enable BSS-compatible API -metrics_port: 9092 # Metrics endpoint port -``` - -### Authentication (when enable_auth: true) +port: 8080 +host: "0.0.0.0" +read_timeout: 30 +write_timeout: 30 +idle_timeout: 120 -```yaml -auth: - enabled: true - jwks_url: "https://auth.example.com/.well-known/jwks.json" - jwt_issuer: "https://auth.example.com" - jwt_audience: "boot-service" - required_scopes: ["boot:read"] +data_dir: "./data" +storage_type: "file" ``` -### External Services +### Feature Flags ```yaml -hsm_url: "http://localhost:27779" # Hardware State Manager - -tokensmith: - url: "http://localhost:8080" # TokenSmith auth service - timeout: 30 +enable_auth: false +enable_metrics: false +enable_legacy_api: true +metrics_port: 9090 ``` -### HSM Service-Token Auth (TokenSmith) +`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. -When both `hsm_url` and `tokensmith_url` are set, boot-service performs a -bootstrap-token exchange against TokenSmith (`POST /oauth/token`) and uses the -short-lived service token for HSM API calls. - -If `enable_auth: false`, `tokensmith_url` is ignored for HSM integration and no -bootstrap token is required. +### TokenSmith and HSM ```yaml -tokensmith_url: "http://localhost:8080" +tokensmith_url: "" tokensmith_target_service: "hsm" -tokensmith_bootstrap_policy_scopes_hint: "hsm:read" +tokensmith_bootstrap_policy_scopes_hint: "" tokensmith_refresh_skew_sec: 120 -# tokensmith_bootstrap_token: "" # Prefer env var -``` -`tokensmith_bootstrap_token` may be provided via config, or by environment -variable `TOKENSMITH_BOOTSTRAP_TOKEN`. - -## Environment-Specific Examples +hsm_url: "" +hsm_sync_enabled: true +hsm_sync_interval: 5 +``` -### Development +Optional bootstrap token input: ```yaml -# Minimal development configuration -enable_auth: false -enable_metrics: true -logging: - level: "debug" -development: - enabled: true +# tokensmith_bootstrap_token: "" ``` -### Production +Environment fallback: -```yaml -# Production configuration with full security -enable_auth: true -auth: - enabled: true - jwks_url: "https://auth.openchami.org/.well-known/jwks.json" - jwt_issuer: "https://auth.openchami.org" - jwt_audience: "boot-service" - required_scopes: ["boot:read"] -logging: - level: "info" - format: "json" +```bash +export TOKENSMITH_BOOTSTRAP_TOKEN="" ``` -### Kubernetes/Container +Deprecated compatibility input still accepted: ```yaml -# Container-friendly configuration -port: 8080 -host: "0.0.0.0" -data_dir: "/data" -auth: - jwks_url: "http://tokensmith:8080/.well-known/jwks.json" - jwt_issuer: "openchami-tokensmith" -hsm_url: "http://smd:27779" -logging: - format: "json" - output: "stdout" +# tokensmith_scopes: "hsm:read" ``` -The runtime image is distroless, so it does not include `curl` or `wget` for an -in-container Docker `HEALTHCHECK`. Probe `/health` from Kubernetes, your -container runtime, or an external monitor instead. +## Current Auth Behavior -## Validation +`enable_auth` does **not** currently attach the `pkg/auth` request middleware to +the server routes in `cmd/server/main.go`. -The service validates configuration at startup and will exit with an error if: +Today, `enable_auth` affects the server in these ways: -- Required authentication settings are missing when auth is enabled -- Invalid URLs are provided for external services -- Conflicting settings are detected +- startup validation requires `tokensmith_url` when `enable_auth: true` +- HSM service-token exchange is enabled only when `enable_auth: true` +- if `hsm_url` and `tokensmith_url` are both set while auth is enabled, a bootstrap token is required -Check the startup logs for configuration validation results: +If `enable_auth: false`, `tokensmith_url` is ignored for HSM integration. -``` -2025/10/09 11:00:27 Starting boot service with configuration: -2025/10/09 11:00:27 Server: 0.0.0.0:8082 -2025/10/09 11:00:27 Storage: file (./data) -2025/10/09 11:00:27 Features: auth=false, hsm=true, metrics=true, legacy-api=true -``` +For package-level JWT and JWKS middleware usage, see `docs/AUTHENTICATION.md`. + +## Metrics Behavior -## Security Considerations +Metrics are disabled by default. When enabled, the server: -### Configuration File Security +- serves `/metrics` on the main router +- starts a separate metrics listener on `host:metrics_port` -- **Never commit** `config.yaml` to version control (it's gitignored) -- **Use restrictive permissions**: `chmod 600 config.yaml` -- **Store secrets securely**: Consider using environment variables for sensitive data +## Boot Profiles and HTTP Behavior -### Production Checklist +Boot profiles are stored on `BootConfiguration.spec.profile`, but the legacy +HTTP bootscript endpoint currently ignores the `profile` query parameter and +auto-selects the best configuration across profiles. -- ✅ Enable authentication (`enable_auth: true`) -- ✅ Use JWKS for key rotation (`jwks_url`) -- ✅ Validate issuer and audience (`validate_issuer: true`, `validate_audience: true`) -- ✅ Require appropriate scopes (`required_scopes`) -- ✅ Use HTTPS for external service URLs -- ✅ Set reasonable timeouts -- ✅ Enable structured logging (`format: "json"`) +See `docs/PROFILES.md` for the exact behavior split between controller logic and +the legacy HTTP endpoint. -## Troubleshooting +## Unsupported Older Examples -### Common Issues +Older docs and examples may still mention nested sections such as: -1. **Service won't start**: - - Check configuration file syntax: `yamllint config.yaml` - - Verify file permissions and existence - - Check logs for specific validation errors +- `auth:` +- `tokensmith:` +- `logging:` +- `health:` +- `limits:` +- `development:` +- `bss:` -2. **Authentication not working**: - - Verify `enable_auth` matches `auth.enabled` - - Check JWKS URL accessibility - - Validate issuer/audience claims in tokens +Those sections are not currently unmarshaled by the server config struct in +`cmd/server/main.go`. -3. **External services unreachable**: - - Verify URLs are accessible from the service - - Check network connectivity and DNS resolution - - Review timeout settings +## Example Environment Overrides -### Debug Configuration +```bash +export BOOT_SERVICE_PORT=8082 +export BOOT_SERVICE_ENABLE_METRICS=true +export BOOT_SERVICE_HSM_URL=http://localhost:27779 +./bin/server serve +``` -For troubleshooting configuration issues: +For HSM service-token exchange: -```yaml -logging: - level: "debug" # Verbose logging -auth: - non_enforcing: true # Log auth errors but don't block -development: - enabled: true # Additional debug features +```bash +export BOOT_SERVICE_ENABLE_AUTH=true +export TOKENSMITH_URL=http://localhost:8080 +export TOKENSMITH_BOOTSTRAP_TOKEN="" +./bin/server serve --hsm-url http://localhost:27779 ``` -## Migration +## Validation and Troubleshooting -### From Command-line Flags +The current startup validation fails when: -1. Create `config.yaml` based on your current flags -2. Test that the service starts correctly -3. Remove flags from your startup scripts +- `port` is outside the valid TCP range +- `enable_auth: true` but `tokensmith_url` is empty +- `tokensmith_refresh_skew_sec` is negative +- `enable_auth: true`, `hsm_url` is set, `tokensmith_url` is set, and no bootstrap token is available -### Adding Authentication +Common checks: -1. Start with `enable_auth: false` -2. Configure auth section with permissive settings -3. Test with valid tokens -4. Gradually tighten validation requirements -5. Enable enforcement: `enable_auth: true` +1. If the service will not start, run `./bin/server serve` directly and inspect the startup error. +2. If metrics do not appear, confirm `enable_metrics: true` and check port `9090` unless you changed `metrics_port`. +3. If HSM integration fails while auth is enabled, confirm `TOKENSMITH_BOOTSTRAP_TOKEN` is set. ## See Also -- [Authentication Documentation](AUTHENTICATION.md) - Detailed auth configuration -- [API Documentation](API.md) - REST API reference -- `config.example.yaml` - Comprehensive configuration example with comments +- [API.md](API.md) for the current HTTP surface +- [AUTHENTICATION.md](AUTHENTICATION.md) for package-level auth behavior and current server auth notes +- [PROFILES.md](PROFILES.md) for boot profile behavior +- `config.example.yaml` for a sample config that matches the current runtime keys diff --git a/docs/PROFILES.md b/docs/PROFILES.md index 89e4174..d169685 100644 --- a/docs/PROFILES.md +++ b/docs/PROFILES.md @@ -1,51 +1,31 @@ # Boot Profiles Guide -This document describes how to use boot profiles to manage different boot configurations for different node types or operational scenarios. +Boot profiles are labels on `BootConfiguration.spec.profile`. They organize boot +configurations into logical groups such as `compute`, `login`, or `debug`. -## Table of Contents +## Current Behavior Split -1. [Overview](#overview) -2. [Core Concepts](#core-concepts) -3. [Quick Start](#quick-start) -4. [Creating Boot Configurations](#creating-boot-configurations) -5. [Requesting Boot Scripts](#requesting-boot-scripts) -6. [Profile Selection Algorithm](#profile-selection-algorithm) -7. [API Reference](#api-reference) -8. [Best Practices](#best-practices) -9. [Troubleshooting](#troubleshooting) -10. [Migration Guide](#migration-guide) +There are two profile-related behaviors in this repository and they are not the +same: -## Overview +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. -Boot profiles allow you to organize boot configurations into logical groups, enabling: +That distinction matters more than anything else in this document. -- **Different boot environments**: Compute nodes, storage nodes, login nodes, management nodes -- **Operational scenarios**: Standard production boot, maintenance/debug boot, specialized workload boot -- **Configuration management**: Keep related boot parameters organized and maintainable -- **Dynamic selection**: Choose the profile at runtime based on operational needs +## Profile Model -## Core Concepts +- `spec.profile == ""` or omitted means the configuration belongs to the default profile. +- Any non-empty `spec.profile` groups that configuration under a named profile. +- Profiles do not assign nodes by themselves; matching still depends on `hosts`, `macs`, `nids`, and `groups`. -### What is a Profile? - -A **profile** is a label you assign to boot configurations to group them logically. It's purely organizational—profiles don't exist independently; they're just values in the `spec.profile` field of BootConfiguration resources. - -### The Default Profile - -**The "default" profile is special and you must create it.** Here's what it means: - -- **Definition**: Any BootConfiguration with an empty or omitted `profile` field belongs to the "default" profile -- **Not auto-created**: The service does NOT create a default profile at startup—you must create it -- **Safety net**: When a requested profile doesn't exist or doesn't match the node, the system falls back to default profile configurations -- **Required**: For the service to work reliably, at least one default configuration should always exist - -**Example of a default configuration** (note the missing `profile` field): +Example default profile configuration: ```yaml apiVersion: boot.openchami.io/v1 @@ -53,14 +33,13 @@ kind: BootConfiguration metadata: name: default-boot spec: - # No 'profile' field here - this makes it the DEFAULT kernel: "http://files.openchami.org/vmlinuz-generic" initrd: "http://files.openchami.org/initramfs-generic.img" params: "console=ttyS0,115200 root=/dev/ram0" - priority: 1 # Low priority ensures specific profiles take precedence + priority: 1 ``` -**Example of a named profile** (compare to above): +Example named profile configuration: ```yaml apiVersion: boot.openchami.io/v1 @@ -68,7 +47,7 @@ kind: BootConfiguration metadata: name: compute-standard spec: - profile: "compute" # This makes it a NAMED profile, not default + profile: "compute" hosts: - "x0c0s*" kernel: "http://files.openchami.org/vmlinuz-compute" @@ -77,66 +56,15 @@ spec: priority: 50 ``` -### Node Targeting vs. Profile Assignment - -**Important distinction**: Profiles don't "assign" configurations to nodes. Instead: - -1. **Boot configurations define matching criteria** (`hosts`, `macs`, `nids`, `groups`) that select which nodes they apply to -2. **Profiles group related configurations** for organizational purposes -3. **Both factors determine selection**: When you request a boot script, the system: - - Finds all configurations matching your requested profile - - Among those, scores each based on how well the targeting criteria match your node - - Returns the highest-scoring configuration - -**Example flow**: - -``` -Node boots with MAC aa:bb:cc:dd:ee:ff -↓ -Node requests: "Give me boot script for profile=compute" -↓ -System searches: - - Configurations with profile="compute" (only these) - - That also match MAC aa:bb:cc:dd:ee:ff (or other targeting criteria) - - Scores each match (MAC match = 100 points, pattern match = 50 points, etc.) -↓ -Returns highest-scoring config for compute profile -Or falls back to default profile if no compute configs match -``` - -## Quick Start - -### Scenario: Boot Different Node Types - -You have compute nodes and login nodes. You want each to boot with different kernels. +## Creating Profiled Configurations -**Step 1: Create a default configuration** (required fallback) +Create configurations through the modern API: ```bash curl -X POST http://boot-service:8080/bootconfigurations \ -H "Content-Type: application/json" \ -d '{ - "metadata": { - "name": "default-boot" - }, - "spec": { - "kernel": "http://files.openchami.org/vmlinuz-generic", - "initrd": "http://files.openchami.org/initramfs-generic.img", - "params": "console=ttyS0,115200 root=/dev/ram0", - "priority": 1 - } - }' -``` - -**Step 2: Create a compute profile** - -```bash -curl -X POST http://boot-service:8080/bootconfigurations \ - -H "Content-Type: application/json" \ - -d '{ - "metadata": { - "name": "compute-standard" - }, + "metadata": {"name": "compute-standard"}, "spec": { "profile": "compute", "hosts": ["x0c0s*"], @@ -148,768 +76,108 @@ curl -X POST http://boot-service:8080/bootconfigurations \ }' ``` -**Step 3: Create a login profile** - -```bash -curl -X POST http://boot-service:8080/bootconfigurations \ - -H "Content-Type: application/json" \ - -d '{ - "metadata": { - "name": "login-standard" - }, - "spec": { - "profile": "login", - "hosts": ["login[0-9]"], - "kernel": "http://files.openchami.org/vmlinuz-login", - "initrd": "http://files.openchami.org/initramfs-login.img", - "params": "console=ttyS0,115200 root=/dev/nvme0n1p1", - "priority": 50 - } - }' -``` - -**Step 4: Request boot scripts** +Useful fields on `BootConfiguration.spec`: -```bash -# Compute node requests boot script for compute profile -curl "http://boot-service:8080/boot/v1/bootscript?host=x0c0s0b0n0&profile=compute" +- `profile` +- `hosts` +- `macs` +- `nids` +- `groups` +- `kernel` +- `initrd` +- `params` +- `priority` -# Login node requests boot script for login profile -curl "http://boot-service:8080/boot/v1/bootscript?host=login0&profile=login" +## Controller Behavior -# Unknown node (or no profile specified) gets best overall match by score/priority -# and falls back to default only if nothing else matches -curl "http://boot-service:8080/boot/v1/bootscript?host=unknown-node" -``` +The controller method `GenerateBootScript(ctx, identifier, profile)` honors the +requested profile. -## Creating Boot Configurations +Current controller rules are: -### Overview +- when `profile` is empty, search across all profiles and choose the best match by score then priority +- when `profile` is non-empty, search only that profile first +- when a requested profile has no match, retry against the default profile -Boot configurations are created as BootConfiguration resources using the modern REST API. Each configuration specifies: +This behavior is covered by tests in `pkg/controllers/bootscript/controller_profile_test.go`. -- **Targeting criteria** (`hosts`, `macs`, `nids`, `groups`): How to identify which nodes use this config -- **Profile** (optional): Name for logical grouping (leave empty for default profile) -- **Boot parameters** (`kernel`, `initrd`, `params`): What to boot and with which arguments -- **Priority** (optional): Tiebreaker when multiple configs match a node +## Legacy HTTP Endpoint Behavior -### Creating via Modern API (Recommended) +The legacy endpoint is: -Use the `POST /bootconfigurations` endpoint with a JSON body: +- `GET /boot/v1/bootscript` -```bash -curl -X POST http://boot-service:8080/bootconfigurations \ - -H "Content-Type: application/json" \ - -d '{ - "metadata": { - "name": "my-config-name" - }, - "spec": { - "profile": "compute", - "hosts": ["x0c0s*"], - "kernel": "http://files.openchami.org/vmlinuz", - "initrd": "http://files.openchami.org/initramfs.img", - "params": "console=ttyS0,115200 root=/dev/ram0", - "priority": 50 - } - }' -``` +It accepts node identifiers through: -**Response** (successful creation): -```json -{ - "apiVersion": "boot.openchami.io/v1", - "kind": "BootConfiguration", - "metadata": { - "name": "my-config-name", - "uid": "boo-a1b2c3d4" - }, - "spec": { - "profile": "compute", - "hosts": ["x0c0s*"], - "kernel": "http://files.openchami.org/vmlinuz", - "initrd": "http://files.openchami.org/initramfs.img", - "params": "console=ttyS0,115200 root=/dev/ram0", - "priority": 50 - } -} -``` +- `?mac=` +- `?host=` +- `?nid=` -### BootConfiguration Spec Reference - -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `profile` | string | No | Profile name for grouping configs. Empty/omitted = default profile. | -| `hosts` | []string | No | XName patterns (e.g., `"x0c0s*"`, `"login[0-9]"`) | -| `macs` | []string | No | MAC addresses (e.g., `"aa:bb:cc:dd:ee:ff"`) | -| `nids` | []int | No | Numeric node IDs (e.g., `42`, `100`) | -| `groups` | []string | No | Inventory group memberships | -| `kernel` | string | **Yes** | URL or path to kernel image | -| `initrd` | string | No | URL or path to initrd/initramfs image | -| `params` | string | No | Kernel parameters (console, root, etc.) | -| `priority` | int | No | Priority for tiebreaking (0-100). Higher = takes precedence. | - -**Note**: At least one targeting criterion (`hosts`, `macs`, `nids`, or `groups`) should be specified. If none are specified, the config acts as a catch-all default. - -### Examples - -#### Example 1: Default Configuration (Fallback) - -This config has NO profile, so it's the "default" profile. - -**JSON (for REST API)**: -```json -{ - "metadata": {"name": "default-boot"}, - "spec": { - "kernel": "http://files.openchami.org/vmlinuz-generic", - "initrd": "http://files.openchami.org/initramfs-generic.img", - "params": "console=ttyS0,115200 root=/dev/ram0", - "priority": 1 - } -} -``` +Current limitation: the handler ignores any `profile` query parameter and always +calls the controller with an empty profile. -**YAML (for documentation)**: -```yaml -apiVersion: boot.openchami.io/v1 -kind: BootConfiguration -metadata: - name: default-boot -spec: - # No 'profile' field - this is DEFAULT - kernel: "http://files.openchami.org/vmlinuz-generic" - initrd: "http://files.openchami.org/initramfs-generic.img" - params: "console=ttyS0,115200 root=/dev/ram0" - priority: 1 -``` - -When used: Returned as fallback when requested profile doesn't exist or doesn't match the node. - -#### Example 2: Compute Profile - -Matches compute nodes by XName pattern. - -**JSON**: -```json -{ - "metadata": {"name": "compute-standard"}, - "spec": { - "profile": "compute", - "hosts": ["x0c0s*"], - "kernel": "http://files.openchami.org/vmlinuz-compute", - "initrd": "http://files.openchami.org/initramfs-compute.img", - "params": "console=ttyS0,115200 root=/dev/ram0 cgroup_memory=1", - "priority": 50 - } -} -``` - -**YAML**: -```yaml -apiVersion: boot.openchami.io/v1 -kind: BootConfiguration -metadata: - name: compute-standard -spec: - profile: "compute" - hosts: - - "x0c0s*" - kernel: "http://files.openchami.org/vmlinuz-compute" - initrd: "http://files.openchami.org/initramfs-compute.img" - params: "console=ttyS0,115200 root=/dev/ram0 cgroup_memory=1" - priority: 50 -``` - -When used: When a compute node requests `?profile=compute`. - -#### Example 3: Login Node Profile - -Matches login nodes by pattern. - -**JSON**: -```json -{ - "metadata": {"name": "login-standard"}, - "spec": { - "profile": "login", - "hosts": ["login[0-9]"], - "kernel": "http://files.openchami.org/vmlinuz-login", - "initrd": "http://files.openchami.org/initramfs-login.img", - "params": "console=ttyS0,115200 root=/dev/nvme0n1p1", - "priority": 50 - } -} -``` - -**YAML**: -```yaml -apiVersion: boot.openchami.io/v1 -kind: BootConfiguration -metadata: - name: login-standard -spec: - profile: "login" - hosts: - - "login[0-9]" - kernel: "http://files.openchami.org/vmlinuz-login" - initrd: "http://files.openchami.org/initramfs-login.img" - params: "console=ttyS0,115200 root=/dev/nvme0n1p1" - priority: 50 -``` - -When used: When a login node requests `?profile=login`. - -#### Example 4: Debug Profile (MAC-Specific) - -Matches a specific node by MAC address for debugging. - -**JSON**: -```json -{ - "metadata": {"name": "debug-mode"}, - "spec": { - "profile": "debug", - "macs": ["aa:bb:cc:dd:ee:ff"], - "kernel": "http://files.openchami.org/vmlinuz-debug", - "initrd": "http://files.openchami.org/initramfs-debug.img", - "params": "console=ttyS0,115200 root=/dev/ram0 rd.break=pre-mount systemd.log_level=debug", - "priority": 100 - } -} -``` - -**YAML**: -```yaml -apiVersion: boot.openchami.io/v1 -kind: BootConfiguration -metadata: - name: debug-mode -spec: - profile: "debug" - macs: - - "aa:bb:cc:dd:ee:ff" - kernel: "http://files.openchami.org/vmlinuz-debug" - initrd: "http://files.openchami.org/initramfs-debug.img" - params: "console=ttyS0,115200 root=/dev/ram0 rd.break=pre-mount systemd.log_level=debug" - priority: 100 -``` - -When used: When the node with MAC `aa:bb:cc:dd:ee:ff` requests `?profile=debug`. The high priority ensures this config is chosen. - -#### Example 5: Profile with Group Membership - -Matches nodes that belong to specific inventory groups. - -**JSON**: -```json -{ - "metadata": {"name": "compute-gpu"}, - "spec": { - "profile": "compute", - "groups": ["gpu-enabled", "hpc-cluster"], - "kernel": "http://files.openchami.org/vmlinuz-compute-gpu", - "initrd": "http://files.openchami.org/initramfs-compute-gpu.img", - "params": "console=ttyS0,115200 root=/dev/ram0 nvidia_drm.modeset=1", - "priority": 75 - } -} -``` - -**YAML**: -```yaml -apiVersion: boot.openchami.io/v1 -kind: BootConfiguration -metadata: - name: compute-gpu -spec: - profile: "compute" - groups: - - "gpu-enabled" - - "hpc-cluster" - kernel: "http://files.openchami.org/vmlinuz-compute-gpu" - initrd: "http://files.openchami.org/initramfs-compute-gpu.img" - params: "console=ttyS0,115200 root=/dev/ram0 nvidia_drm.modeset=1" - priority: 75 -``` - -When used: When a node that's a member of both `gpu-enabled` and `hpc-cluster` groups requests `?profile=compute`. - -## Requesting Boot Scripts - -### Legacy API Endpoint (`/boot/v1/bootscript`) - -The legacy BSS-compatible endpoint returns an iPXE boot script. Use the `profile` query parameter to request a specific profile: +That means all of these requests behave the same as far as profile selection is +concerned: ```bash -# Request compute profile with MAC address -curl "http://boot-service:8080/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=compute" - -# Request debug profile with XName -curl "http://boot-service:8080/boot/v1/bootscript?host=x0c0s0b0n0&profile=debug" - -# Request login profile with NID -curl "http://boot-service:8080/boot/v1/bootscript?nid=42&profile=login" - -# Omit profile parameter to auto-resolve best config across all profiles -# (score first, then priority). Use &profile=default to force default-only behavior. 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" ``` -**Response**: An iPXE script that can be executed by the boot firmware: - -```ipxe -#!ipxe -echo Starting boot sequence for x0c0s0b0n0 -set base-url http://files.openchami.org -kernel ${base-url}/vmlinuz-compute root=/dev/ram0 console=ttyS0,115200 cgroup_memory=1 -initrd ${base-url}/initramfs-compute.img -boot -``` - -### Node Identification Methods - -You can identify the requesting node in three ways. Provide exactly one: - -| Method | Parameter | Format | Example | -|--------|-----------|--------|----------| -| **MAC Address** | `?mac=` | Standard MAC format | `?mac=aa:bb:cc:dd:ee:ff` | -| **XName** (Cray) | `?host=` | Cray hardware naming | `?host=x0c0s0b0n0` | -| **NID** (Numeric ID) | `?nid=` | Integer node ID | `?nid=42` | - -### Using with PXE - -In your DHCP/PXE configuration, reference this endpoint with dynamic node identifiers: - -```ipxe -# Standard boot with node's MAC address -chain http://boot-service:8080/boot/v1/bootscript?mac=${net0/mac}&profile=compute - -# Debug boot (if special boot flag is set) -chain http://boot-service:8080/boot/v1/bootscript?mac=${net0/mac}&profile=debug - -# No profile specified - service auto-selects best config across all profiles -# (score first, priority second) -chain http://boot-service:8080/boot/v1/bootscript?mac=${net0/mac} -``` - -### Modern API Endpoint (`GET /bootconfigurations`) - -The modern REST API also supports the profile concept: - -```bash -# List ALL boot configurations -curl "http://boot-service:8080/bootconfigurations" - -# List configurations for a specific profile (requires filtering) -# Note: The API doesn't have a built-in profile filter; use client-side filtering -curl "http://boot-service:8080/bootconfigurations" | jq '.[] | select(.spec.profile == "compute")' - -# Get a specific configuration by UID -curl "http://boot-service:8080/bootconfigurations/boo-a1b2c3d4" -``` - -## Profile Selection Algorithm - -When you request a boot script, the boot service follows this algorithm: - -**Step 1: Filter by Profile** -- If no profile requested: evaluate candidates across ALL profiles -- If profile requested and not `default`: evaluate only that profile -- If profile requested as `default`: evaluate only default profile (`spec.profile == ""`) - -**Step 2: Score Each Candidate** - -For each configuration matching the profile, calculate a score based on how well the targeting criteria match your node: - -| Matching Criterion | Points | Notes | -|--------------------|--------|-------| -| Exact MAC match | **100** | Highest priority | -| NID match | **75** | Direct numeric ID match | -| Host/XName pattern match | **50** | Regex or glob pattern | -| Group membership (per group) | **25** | Cumulative for multiple groups | -| Catch-all (no criteria) | **1** | Lowest; used as universal fallback | - -**Step 3: Select Best Match** -- Sort candidates by: Score (descending) → Priority (descending) -- Return the highest-ranked configuration - -**Step 4: Fallback** -- If no profile requested: - - Return best overall candidate from all profiles - - If no specific candidate matches, catch-all/default configs (score=1) win - - If nothing matches at all: Return error -- If a non-default profile was requested: - - Return best match from requested profile when available - - Otherwise retry with default profile - - If no default exists: Return error - -**Example with Data**: - -**Scenario 1: Exact MAC Match** - -Given: -- Config A: profile="compute", macs=["aa:bb:cc:dd:ee:ff"], priority=50 -- Config B: profile="compute", hosts=["x0c0s*"], priority=50 -- Config C: profile="" (default), priority=1 - -Request: `?mac=aa:bb:cc:dd:ee:ff&profile=compute` - -Step 1: Filter profile → Only A and B (profile="compute") -Step 2: Score: - - Config A: 100 (MAC exact match) - - Config B: 50 (XName pattern doesn't match aa:bb:cc:dd:ee:ff) -Step 3: Select → **Config A** (100 > 50) - -**Scenario 2: Pattern Match** - -Same configs as above. - -Request: `?host=x0c0s0b0n0&profile=compute` - -Step 1: Filter profile → Only A and B (profile="compute") -Step 2: Score: - - Config A: 0 (MAC "aa:bb:cc:dd:ee:ff" doesn't match node x0c0s0b0n0) - - Config B: 50 (pattern "x0c0s*" matches x0c0s0b0n0) -Step 3: Select → **Config B** (50 > 0) - -**Scenario 3: Fallback to Default** - -Same configs as above. - -Request: `?mac=aa:bb:cc:dd:ee:ff&profile=storage` - -Step 1: Filter profile → No configs with profile="storage" -Step 2: No candidates for storage -Step 4: Fallback to default profile - - Config C: profile="" (default), score=1 -Step 3: Select → **Config C** (default fallback) - -**Scenario 4: No Default Profile Error** - -Given: -- Config A: profile="compute", hosts=["x0c0s*"], priority=50 -- NO default config exists - -Request: `?host=login0&profile=compute` - -Step 1: Filter profile → Only A (profile="compute") -Step 2: Score: - - Config A: 0 (pattern "x0c0s*" doesn't match node login0) -Step 3: No match -Step 4: Fallback to default → No default config exists -Step 4: **Error** - "no matching configurations found" - -## API Reference - -### Creating Boot Configurations - -**Endpoint**: `POST /bootconfigurations` - -**Request Body**: -```json -{ - "metadata": { - "name": "config-name" - }, - "spec": { - "profile": "compute", - "hosts": ["x0c0s*"], - "macs": [], - "nids": [], - "groups": [], - "kernel": "http://files.openchami.org/vmlinuz", - "initrd": "http://files.openchami.org/initramfs.img", - "params": "console=ttyS0,115200 root=/dev/ram0", - "priority": 50 - } -} -``` - -**Example**: -```bash -curl -X POST http://boot-service:8080/bootconfigurations \ - -H "Content-Type: application/json" \ - -d '{"metadata":{"name":"my-config"},"spec":{"profile":"compute","hosts":["x0c0s*"],"kernel":"http://example.com/vmlinuz"}}' -``` - -**Response**: `201 Created` with full resource representation. - -### Listing Boot Configurations - -**Endpoint**: `GET /bootconfigurations` - -**Query Parameters**: None (filtering must be done client-side) - -**Example**: -```bash -curl "http://boot-service:8080/bootconfigurations" -``` - -**Response**: `200 OK` with array of all BootConfiguration resources. - -### Getting a Specific Configuration +In every case, the server auto-resolves the best configuration across profiles. -**Endpoint**: `GET /bootconfigurations/{uid}` - -**Example**: -```bash -curl "http://boot-service:8080/bootconfigurations/boo-a1b2c3d4" -``` +## Selection Algorithm -**Response**: `200 OK` with the specific BootConfiguration resource, or `404 Not Found`. +The current score model is: -### Updating Boot Configurations +- exact MAC match: `100` +- NID match: `75` +- host/XName pattern match: `50` +- group membership: `25` per matched group +- catch-all/default config: `1` -**Endpoint**: `PATCH /bootconfigurations/{uid}` +Candidates are ordered by: -**Request Body** (partial update - include only fields to change): -```json -{ - "spec": { - "priority": 75 - } -} -``` +1. score descending +2. `priority` descending -**Example** (change profile priority): -```bash -curl -X PATCH http://boot-service:8080/bootconfigurations/boo-a1b2c3d4 \ - -H "Content-Type: application/json" \ - -d '{"spec":{"priority":75}}' -``` +## Operational Guidance -**Example** (change profile name): -```bash -curl -X PATCH http://boot-service:8080/bootconfigurations/boo-a1b2c3d4 \ - -H "Content-Type: application/json" \ - -d '{"spec":{"profile":"login"}}' -``` +Use profiles today for: -**Response**: `200 OK` with updated resource. +- organizing `BootConfiguration` resources +- controller-level integrations that call `GenerateBootScript(..., profile)` directly +- preparing for a future HTTP surface that may expose explicit profile selection -### Deleting Boot Configurations - -**Endpoint**: `DELETE /bootconfigurations/{uid}` - -**Example**: -```bash -curl -X DELETE "http://boot-service:8080/bootconfigurations/boo-a1b2c3d4" -``` - -**Response**: `204 No Content` on success, or `404 Not Found` if already deleted. - -## Best Practices - -### Profile Naming - -Choose clear, descriptive profile names: - -- ✅ `compute`, `login`, `storage`, `management` -- ✅ `debug`, `maintenance`, `production` -- ❌ `v1`, `type-a`, `special` - -### Configuration Organization - -1. **Always create a default profile**: Define at least one BootConfiguration with an empty `profile` field as your fallback -2. **Use consistent profile naming**: Choose clear names like `compute`, `login`, `storage`, `debug` (not `v1`, `type-a`, etc.) -3. **Name configurations descriptively**: Use metadata names like `compute-standard`, `login-production` to indicate purpose -4. **Version boot images**: Include version info in kernel/initrd URLs (e.g., `vmlinuz-5.10.0-v2`) -5. **Set reasonable priorities**: Default=1, standard profiles=50, specialized/debug=100 -6. **Document profile usage**: Comment in configs or keep external docs of what each profile is for - -### Priority Field Guidelines - -The `priority` field breaks ties when multiple configs have the same score. Suggested values: - -| Profile Type | Priority | Rationale | -|---|---|---| -| Default/Fallback | 1 | Lowest; only used when nothing else matches | -| Standard production | 50 | Normal operation | -| Specialized (GPU, storage) | 75 | More specific than standard | -| Debug/Maintenance | 100 | Highest; forces selection when you need it | - -**Note**: The targeting criteria score (MAC=100, NID=75, pattern=50, group=25) usually dominates priority. Priority mainly matters when scores are identical. - -### Group Usage with Profiles - -Combine profiles with groups for powerful organization: - -```yaml -apiVersion: boot.openchami.io/v1 -kind: BootConfiguration -metadata: - name: compute-hpc-optimized -spec: - profile: "compute" - groups: - - "hpc-cluster" # Apply only to HPC cluster nodes - - "gpu-enabled" # Further filter to GPU nodes - kernel: "http://files.openchami.org/vmlinuz-hpc" - initrd: "http://files.openchami.org/initramfs-hpc.img" - params: "console=ttyS0,115200 root=/dev/ram0 numa=on acpi=on" - priority: 75 -``` +Do not assume profile-specific HTTP behavior from `/boot/v1/bootscript` yet. ## Troubleshooting -### "No matching configurations found" - -**Error**: You request a boot script but get this error. - -**Cause**: Either: -1. Requested profile has no configurations that match the node's targeting criteria -2. No default profile exists to fall back to - -**Solution**: -1. Check that a default configuration exists (profile field empty or omitted) - ```bash - curl http://boot-service:8080/bootconfigurations | jq '.[] | select(.spec.profile == "")' - ``` -2. Verify configurations exist for the requested profile: - ```bash - curl http://boot-service:8080/bootconfigurations | jq '.[] | select(.spec.profile == "compute")' - ``` -3. Check that your node matches the targeting criteria: - - For MAC: compare node's MAC with `spec.macs` - - For XName: compare node's XName with `spec.hosts` patterns - - For NID: compare node's NID with `spec.nids` - - For groups: check node membership in `spec.groups` - -### Wrong Profile Being Used - -**Symptom**: You request `?profile=compute` but get a different profile's boot script. - -**Diagnosis** - Check in this order: - -1. **Verify request parameters**: - ```bash - # Did you include the profile parameter? - curl -v "http://boot-service:8080/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=compute" 2>&1 | grep -i profile - ``` +### A profile query parameter does nothing -2. **Check what configurations exist**: - ```bash - curl "http://boot-service:8080/bootconfigurations" | jq '.[] | {name: .metadata.name, profile: .spec.profile}' - ``` +That is the current expected behavior of the legacy HTTP route. The handler +accepts the parameter but ignores it, and matching is still based on best score +and priority across profiles. -3. **Verify targeting criteria match**: - ```bash - # List compute profile configs - curl "http://boot-service:8080/bootconfigurations" | jq '.[] | select(.spec.profile == "compute") | {name, hosts: .spec.hosts, macs: .spec.macs}' +### The wrong profile appears to win - # Does your node's MAC/XName match the criteria? - ``` - -4. **Check scoring** - If multiple configs match, which scores highest? - ```bash - # Look at both profile and priority - curl "http://boot-service:8080/bootconfigurations" | jq '.[] | {name: .metadata.name, profile: .spec.profile, priority: .spec.priority}' - ``` - -**Common issues**: -- Profile name typo (e.g., `compue` vs `compute`) -- Node doesn't match targeting criteria (wrong MAC, XName pattern doesn't apply) -- Multiple configs with same score; the one with higher priority was selected -- Requesting non-existent profile without a default profile to fall back to - -### Profile Doesn't Exist - Falling Back to Default - -**Symptom**: You request `?profile=special` but get the default profile instead. - -**This is expected behavior**. Here's what happens: - -``` -Request: ?profile=special -↓ -System looks for configs with profile="special" -↓ -None found -↓ -Fallback to profile="" (default) -↓ -Return default profile config -``` - -**Why this is good**: Provides resilience. If a profile doesn't exist, nodes don't fail—they get a reasonable default. - -**If you need to enforce specific profiles**: -1. Set up each required profile with at least one configuration -2. Make sure no default profile exists (or set its priority very low, e.g., 0) -3. Alternative: Use a catch-all config with very specific priority/criteria to catch unmatched nodes - -### Verifying Which Configuration Is Being Used - -Boot scripts include comments identifying the node and config: +Inspect the stored boot configurations and compare their targeting fields and +priority values: ```bash -# Request the boot script -curl "http://boot-service:8080/boot/v1/bootscript?mac=aa:bb:cc:dd:ee:ff&profile=compute" - -# Output will include iPXE script with kernel/initrd URLs showing which config was selected +curl "http://boot-service:8080/bootconfigurations" | jq '.[] | {name: .metadata.name, profile: .spec.profile, priority: .spec.priority, hosts: .spec.hosts, macs: .spec.macs, nids: .spec.nids, groups: .spec.groups}' ``` -**Alternative**: Query the configurations directly and review which matches: +### Default fallback is missing -```bash -# Get all configs, filter to your profile -curl "http://boot-service:8080/bootconfigurations" | jq '.[] | select(.spec.profile == "compute")' - -# Look at kernel URLs in returned configs to identify which you should be using -``` - -### Default Profile Creation Checklist - -When setting up the boot service, ensure you have at least one default configuration: - -- [ ] Created at least one BootConfiguration with empty `profile` field (or no profile field) -- [ ] Default config has reasonable `kernel` and `params` values -- [ ] Default config priority is low (recommend 1-10) so specific profiles take precedence -- [ ] Verified default config exists: - ```bash - curl http://boot-service:8080/bootconfigurations | jq '.[] | select(.spec.profile == "")' - ``` -- [ ] If result is empty: Default profile doesn't exist! Create one immediately. - -## Migration Guide - -### From Non-Profiled Setup - -If you have existing boot configurations without profiles: - -**Step 1**: Current state -- Existing configs have no `profile` field -- These are automatically treated as "default" profile -- Everything continues to work as-is - -**Step 2**: Add profiles gradually -- New configurations can include explicit `profile` fields -- Existing configs without profiles remain as default -- No breaking changes - -**Step 3**: Update existing configs (optional) - -Add a profile to organize better: +Make sure at least one configuration has an empty or omitted `profile` field. ```bash -# First, get the UID of the config to update -curl http://boot-service:8080/bootconfigurations | jq '.[] | select(.metadata.name == "old-config") | .metadata.uid' - -# Then PATCH to add profile -curl -X PATCH http://boot-service:8080/bootconfigurations/{uid} \ - -H "Content-Type: application/json" \ - -d '{"spec": {"profile": "compute"}}' +curl "http://boot-service:8080/bootconfigurations" | jq '.[] | select((.spec.profile // "") == "")' ``` -### Backward Compatibility - -- Existing clients requesting `?profile=` on old configs: Works (matched against empty profile = default) -- Existing clients requesting no profile: Works (auto-selects best config across all profiles) -- No API changes needed - ## See Also -- [CONFIGURATION.md](CONFIGURATION.md) - Service configuration options -- [AUTHENTICATION.md](AUTHENTICATION.md) - JWT and authentication setup -- `examples/` directory - Sample YAML configurations and curl commands +- [API.md](API.md) for the current HTTP endpoint surface +- [CONFIGURATION.md](CONFIGURATION.md) for server configuration behavior diff --git a/go.mod b/go.mod index 1d5d8b3..72ca43c 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/getkin/kin-openapi v0.133.0 github.com/go-chi/chi/v5 v5.2.3 github.com/golang-jwt/jwt/v5 v5.3.0 - github.com/openchami/fabrica v0.4.4 + github.com/openchami/fabrica v0.4.5 github.com/openchami/tokensmith v0.4.0 github.com/prometheus/client_golang v1.23.2 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index fa0e668..0a9f6df 100644 --- a/go.sum +++ b/go.sum @@ -99,8 +99,8 @@ github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletI github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/openchami/chi-middleware/log v0.0.0-20240812224658-b16b83c70700 h1:Gzt5f6RK39CHvY3SJudzBb/RK4tVh/S3CpJ0eQlbNdg= github.com/openchami/chi-middleware/log v0.0.0-20240812224658-b16b83c70700/go.mod h1:UuXvr2loD4MtvZeKr57W0WpBs+gm0KM1kdtcXrE8M6s= -github.com/openchami/fabrica v0.4.4 h1:M7VncVIIywfdWpim5lPjvyKxxxe0LLiVK/ZOw1/4BhU= -github.com/openchami/fabrica v0.4.4/go.mod h1:Kmii+YJz6fYfIU2ZH+LkfriLhXB8AVf8vUDxfG9Acnk= +github.com/openchami/fabrica v0.4.5 h1:ZokuHjWGXYHz3jq3mNStIopIBAhLp+NQdUxjUHwxKYM= +github.com/openchami/fabrica v0.4.5/go.mod h1:h/0CX1tDwqdmBxk4lm8EtxcMgI6ebaxbz65ic4uozfg= github.com/openchami/tokensmith v0.4.0 h1:+HBzW0ilH3/P5RNlGJfgVtSbc9r5ClzPHij9CnySVxI= github.com/openchami/tokensmith v0.4.0/go.mod h1:L4ZCMX/vPGwXUUn9otw+UdfFTbarv+ZVO/FjhZmoOAE= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= From 14ad8a28147c3082db2715b834ed4b769e2eb59b Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Wed, 13 May 2026 06:32:10 -0400 Subject: [PATCH 09/10] feat: update configuration documentation and examples, enhance AUTHENTICATION and PROFILES guides Signed-off-by: Alex Lovell-Troy --- .github/copilot-instructions.md | 2 ++ CHANGELOG.md | 12 +++++--- Makefile | 3 +- config.example.yaml | 15 +++++++++ docs/AUTHENTICATION.md | 10 +++--- docs/CONFIGURATION.md | 54 ++++++++++++++++----------------- docs/PROFILES.md | 20 ++++++------ 7 files changed, 67 insertions(+), 49 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index cc0f1f2..f99edca 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -205,6 +205,8 @@ port: 8080 enable_auth: false enable_metrics: false enable_legacy_api: true +# metrics_port is configured separately because it becomes active as soon as +# metrics are enabled, even though metrics default to off. metrics_port: 9090 hsm_url: "http://localhost:27779" tokensmith_url: "http://localhost:8080" diff --git a/CHANGELOG.md b/CHANGELOG.md index e3bdf38..c51c71b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.1.5] - Unreleased +Changes remain under `Unreleased` until they ship in the next tagged release. + +## [Unreleased] ### Added @@ -22,11 +24,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Regenerated server, client, storage, and OpenAPI surfaces against Fabrica `v0.4.4`. +- Regenerated server, client, storage, and OpenAPI surfaces against Fabrica `v0.4.5`. - Updated generated file headers to include Fabrica version metadata. - Updated the Docker release build to pass dynamic build arguments into image builds. - Tightened code generation drift checks around the current Fabrica workflow. -- Refreshed the OpenAPI health schema and example payload to match the current `/health` response. +- Documented the generated service endpoints added in this release, including `/health`, `/openapi.json`, and `/docs`. ## [0.1.4] - 2026-05-06 @@ -76,8 +78,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - File-backed `BMC`, `BootConfiguration`, and `Node` resource APIs. - Legacy BSS-compatible boot endpoints and generated Go client support. -[0.1.5]: https://github.com/OpenCHAMI/boot-service/compare/v0.1.4...HEAD +[Unreleased]: https://github.com/OpenCHAMI/boot-service/compare/v0.1.4...HEAD [0.1.4]: https://github.com/OpenCHAMI/boot-service/compare/v0.1.3...v0.1.4 [0.1.3]: https://github.com/OpenCHAMI/boot-service/compare/v0.1.2...v0.1.3 [0.1.2]: https://github.com/OpenCHAMI/boot-service/compare/v0.1.1...v0.1.2 -[0.1.1]: https://github.com/OpenCHAMI/boot-service/compare/v0.1.0...v0.1.1 \ No newline at end of file +[0.1.1]: https://github.com/OpenCHAMI/boot-service/compare/v0.1.0...v0.1.1 diff --git a/Makefile b/Makefile index 0edd23d..5605ba0 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,9 @@ VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") DOCKER_GO_VERSION ?= $(shell awk '/^go / {print $$2; exit}' go.mod) +FABRICA_VERSION ?= $(shell awk '/github.com\/openchami\/fabrica[[:space:]]+v/ {print $$2; exit}' go.mod) LDFLAGS=-ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)" -FABRICA_CMD ?= go run github.com/openchami/fabrica/cmd/fabrica@latest +FABRICA_CMD ?= go run github.com/openchami/fabrica/cmd/fabrica@$(FABRICA_VERSION) FABRICA_SOURCE_ARG ?= FABRICA_FORCE_FLAG ?= FABRICA_ENV ?= diff --git a/config.example.yaml b/config.example.yaml index 25dc4c1..246a377 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -19,26 +19,37 @@ # SERVER # ============================================================================= +# Main HTTP listen port for the API server. port: 8080 +# Interface to bind for the main HTTP listener. host: "0.0.0.0" +# Read deadline in seconds for inbound HTTP requests. read_timeout: 30 +# Write deadline in seconds for outbound HTTP responses. write_timeout: 30 +# Keep-alive timeout in seconds for idle connections. idle_timeout: 120 # ============================================================================= # STORAGE # ============================================================================= +# Directory used by the file-backed storage implementation. data_dir: "./data" +# Storage backend type. The current server supports "file". storage_type: "file" # ============================================================================= # FEATURE FLAGS # ============================================================================= +# Enables TokenSmith-dependent startup validation and HSM token exchange. enable_auth: false +# Enables Prometheus metrics endpoints. enable_metrics: false +# Controls legacy BSS-compatible endpoints other than /boot/v1/bootscript. enable_legacy_api: true +# Metrics listener port used when enable_metrics is true. metrics_port: 9090 # ============================================================================= @@ -53,6 +64,7 @@ tokensmith_url: "" # Prefer environment variable TOKENSMITH_BOOTSTRAP_TOKEN for real deployments. # tokensmith_bootstrap_token: "" +# Target service name requested during TokenSmith service-token exchange. tokensmith_target_service: "hsm" # Optional comma-separated scope hint used for diagnostics when exchanging HSM @@ -62,11 +74,14 @@ tokensmith_bootstrap_policy_scopes_hint: "" # Deprecated legacy alias still accepted by current code. # tokensmith_scopes: "hsm:read" +# Refresh skew in seconds applied before service tokens are considered stale. tokensmith_refresh_skew_sec: 120 # HSM-backed node resolution is enabled when this URL is provided. hsm_url: "" +# Enables background synchronization against the HSM provider. hsm_sync_enabled: true +# Interval in minutes between HSM background sync runs. hsm_sync_interval: 5 # ============================================================================= diff --git a/docs/AUTHENTICATION.md b/docs/AUTHENTICATION.md index 9805c5d..d3414e3 100644 --- a/docs/AUTHENTICATION.md +++ b/docs/AUTHENTICATION.md @@ -86,11 +86,11 @@ tokensmith_refresh_skew_sec: 120 Standardized environment variables: -- `TOKENSMITH_URL` -- `TOKENSMITH_BOOTSTRAP_TOKEN` -- `TOKENSMITH_TARGET_SERVICE` -- `TOKENSMITH_BOOTSTRAP_POLICY_SCOPES_HINT` -- `TOKENSMITH_REFRESH_SKEW_SEC` +- `TOKENSMITH_URL`: base TokenSmith URL used when `enable_auth` triggers startup validation or HSM service-token exchange, for example `http://localhost:8080` +- `TOKENSMITH_BOOTSTRAP_TOKEN`: bootstrap JWT exchanged for short-lived HSM service tokens +- `TOKENSMITH_TARGET_SERVICE`: target service name requested from TokenSmith, typically `hsm` +- `TOKENSMITH_BOOTSTRAP_POLICY_SCOPES_HINT`: optional scope hint used in diagnostics, for example `hsm:read` +- `TOKENSMITH_REFRESH_SKEW_SEC`: refresh skew in seconds before a cached service token is treated as stale Deprecated compatibility environment variable: diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 2eb6a17..dd3f18c 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -28,9 +28,9 @@ startup path. 3. Override settings with flags or environment variables when needed. -## Configuration Precedence +## Configuration Precedence Order -The server resolves configuration in this order: +The server applies configuration in this precedence order: 1. Command-line flags 2. Environment variables @@ -45,41 +45,39 @@ environment variables. ### Server and Storage -```yaml -port: 8080 -host: "0.0.0.0" -read_timeout: 30 -write_timeout: 30 -idle_timeout: 120 - -data_dir: "./data" -storage_type: "file" -``` +| Key | Example | Description | +| --- | --- | --- | +| `port` | `8080` | Main HTTP listen port for the API router. | +| `host` | `"0.0.0.0"` | Interface address bound by the main HTTP listener. | +| `read_timeout` | `30` | Request read timeout in seconds. | +| `write_timeout` | `30` | Response write timeout in seconds. | +| `idle_timeout` | `120` | Keep-alive timeout in seconds for idle connections. | +| `data_dir` | `"./data"` | Filesystem path used by the file-backed storage implementation. | +| `storage_type` | `"file"` | Storage backend selector. The current server supports `file`. | ### Feature Flags -```yaml -enable_auth: false -enable_metrics: false -enable_legacy_api: true -metrics_port: 9090 -``` +| Key | Example | Description | +| --- | --- | --- | +| `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`. | +| `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. ### TokenSmith and HSM -```yaml -tokensmith_url: "" -tokensmith_target_service: "hsm" -tokensmith_bootstrap_policy_scopes_hint: "" -tokensmith_refresh_skew_sec: 120 - -hsm_url: "" -hsm_sync_enabled: true -hsm_sync_interval: 5 -``` +| Key | Example | Description | +| --- | --- | --- | +| `tokensmith_url` | `"http://localhost:8080"` | Base URL for TokenSmith when startup validation or HSM token exchange is enabled. | +| `tokensmith_target_service` | `"hsm"` | Service name requested during TokenSmith service-token exchange. | +| `tokensmith_bootstrap_policy_scopes_hint` | `"hsm:read"` | Optional comma-separated scope hint used for diagnostics during bootstrap exchange. | +| `tokensmith_refresh_skew_sec` | `120` | Number of seconds before expiry that cached service tokens should be treated as stale. | +| `hsm_url` | `"http://localhost:27779"` | Enables HSM-backed node resolution when set. | +| `hsm_sync_enabled` | `true` | Turns the optional background HSM sync loop on or off. | +| `hsm_sync_interval` | `5` | Background HSM sync interval in minutes. | Optional bootstrap token input: diff --git a/docs/PROFILES.md b/docs/PROFILES.md index d169685..a9e2c45 100644 --- a/docs/PROFILES.md +++ b/docs/PROFILES.md @@ -17,7 +17,7 @@ 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. -That distinction matters more than anything else in this document. +**That distinction matters more than anything else in this document.** ## Profile Model @@ -78,15 +78,15 @@ curl -X POST http://boot-service:8080/bootconfigurations \ Useful fields on `BootConfiguration.spec`: -- `profile` -- `hosts` -- `macs` -- `nids` -- `groups` -- `kernel` -- `initrd` -- `params` -- `priority` +- `profile`: logical profile label such as `compute` or `debug` +- `hosts`: XName or hostname glob patterns used for node matching +- `macs`: exact boot MAC addresses with the highest match score +- `nids`: numeric node identifiers used for explicit node targeting +- `groups`: group labels matched against node group membership +- `kernel`: kernel image URL served to iPXE +- `initrd`: initramfs image URL served to iPXE +- `params`: kernel command-line arguments appended to the boot entry +- `priority`: tie-breaker used after the match score is computed ## Controller Behavior From 10a0a0c2345a17e771525555bd2617a4b38dfc71 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Wed, 13 May 2026 06:46:16 -0400 Subject: [PATCH 10/10] Regenerated all files with fabrica 0.4.5 Signed-off-by: Alex Lovell-Troy --- cmd/server/authz_classifier_generated.go | 2 +- cmd/server/bmc_handlers_generated.go | 4 ++-- cmd/server/bootconfiguration_handlers_generated.go | 4 ++-- cmd/server/models_generated.go | 2 +- cmd/server/node_handlers_generated.go | 4 ++-- cmd/server/openapi_generated.go | 2 +- cmd/server/routes_generated.go | 2 +- pkg/apiversion/registry_generated.go | 4 ++-- pkg/client/client_generated.go | 2 +- pkg/resources/register_generated.go | 4 ++-- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/cmd/server/authz_classifier_generated.go b/cmd/server/authz_classifier_generated.go index a99890a..22f3116 100644 --- a/cmd/server/authz_classifier_generated.go +++ b/cmd/server/authz_classifier_generated.go @@ -1,4 +1,4 @@ -// Code generated by Fabrica. DO NOT EDIT. +// Code generated by Fabrica 0.4.5 (commit: 0a2be92b1375e82f25979a964bdce30b8f6a8825). DO NOT EDIT. // Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // // SPDX-License-Identifier: MIT diff --git a/cmd/server/bmc_handlers_generated.go b/cmd/server/bmc_handlers_generated.go index cca6c4d..5f9b24c 100644 --- a/cmd/server/bmc_handlers_generated.go +++ b/cmd/server/bmc_handlers_generated.go @@ -1,6 +1,6 @@ -// Code generated by Fabrica v0.4.4-9-g4df0460 (commit: 4df0460). DO NOT EDIT. +// Code generated by Fabrica 0.4.5 (commit: 0a2be92b1375e82f25979a964bdce30b8f6a8825). DO NOT EDIT. // Template: server/handlers.go.tmpl -// Generated: 2026-05-11T16:34:21Z +// Generated: 2026-05-13T10:45:16Z // // # Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // diff --git a/cmd/server/bootconfiguration_handlers_generated.go b/cmd/server/bootconfiguration_handlers_generated.go index 75d82f4..3e872fa 100644 --- a/cmd/server/bootconfiguration_handlers_generated.go +++ b/cmd/server/bootconfiguration_handlers_generated.go @@ -1,6 +1,6 @@ -// Code generated by Fabrica v0.4.4-9-g4df0460 (commit: 4df0460). DO NOT EDIT. +// Code generated by Fabrica 0.4.5 (commit: 0a2be92b1375e82f25979a964bdce30b8f6a8825). DO NOT EDIT. // Template: server/handlers.go.tmpl -// Generated: 2026-05-11T16:34:21Z +// Generated: 2026-05-13T10:45:16Z // // # Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // diff --git a/cmd/server/models_generated.go b/cmd/server/models_generated.go index 86cdc59..8bd3f7a 100644 --- a/cmd/server/models_generated.go +++ b/cmd/server/models_generated.go @@ -1,4 +1,4 @@ -// Code generated by Fabrica dev. DO NOT EDIT. +// Code generated by Fabrica 0.4.5 (commit: 0a2be92b1375e82f25979a964bdce30b8f6a8825). DO NOT EDIT. // Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // // SPDX-License-Identifier: MIT diff --git a/cmd/server/node_handlers_generated.go b/cmd/server/node_handlers_generated.go index 2e6c21c..cd4379d 100644 --- a/cmd/server/node_handlers_generated.go +++ b/cmd/server/node_handlers_generated.go @@ -1,6 +1,6 @@ -// Code generated by Fabrica v0.4.4-9-g4df0460 (commit: 4df0460). DO NOT EDIT. +// Code generated by Fabrica 0.4.5 (commit: 0a2be92b1375e82f25979a964bdce30b8f6a8825). DO NOT EDIT. // Template: server/handlers.go.tmpl -// Generated: 2026-05-11T16:34:21Z +// Generated: 2026-05-13T10:45:16Z // // # Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // diff --git a/cmd/server/openapi_generated.go b/cmd/server/openapi_generated.go index 18f8653..3bb8147 100644 --- a/cmd/server/openapi_generated.go +++ b/cmd/server/openapi_generated.go @@ -1,4 +1,4 @@ -// Code generated by Fabrica. DO NOT EDIT. +// Code generated by Fabrica 0.4.5 (commit: 0a2be92b1375e82f25979a964bdce30b8f6a8825). DO NOT EDIT. // Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // // SPDX-License-Identifier: MIT diff --git a/cmd/server/routes_generated.go b/cmd/server/routes_generated.go index da130b7..b6935fe 100644 --- a/cmd/server/routes_generated.go +++ b/cmd/server/routes_generated.go @@ -1,4 +1,4 @@ -// Code generated by Fabrica. DO NOT EDIT. +// Code generated by Fabrica 0.4.5 (commit: 0a2be92b1375e82f25979a964bdce30b8f6a8825). DO NOT EDIT. // Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // // SPDX-License-Identifier: MIT diff --git a/pkg/apiversion/registry_generated.go b/pkg/apiversion/registry_generated.go index cd7c272..6304d32 100644 --- a/pkg/apiversion/registry_generated.go +++ b/pkg/apiversion/registry_generated.go @@ -1,6 +1,6 @@ -// Code generated by Fabrica dev. DO NOT EDIT. +// Code generated by Fabrica 0.4.5 (commit: 0a2be92b1375e82f25979a964bdce30b8f6a8825). DO NOT EDIT. // Template: apiversion/register.gotmpl -// Generated at: 2026-05-05T09:18:40Z +// Generated at: 2026-05-13T10:45:16Z // SPDX-FileCopyrightText: Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // SPDX-License-Identifier: MIT // diff --git a/pkg/client/client_generated.go b/pkg/client/client_generated.go index 4aca78a..e61a5eb 100644 --- a/pkg/client/client_generated.go +++ b/pkg/client/client_generated.go @@ -1,4 +1,4 @@ -// Code generated by Fabrica v0.4.4-9-g4df0460-dirty (commit: 4df0460). DO NOT EDIT. +// Code generated by Fabrica 0.4.5 (commit: 0a2be92b1375e82f25979a964bdce30b8f6a8825). DO NOT EDIT. // Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // // SPDX-License-Identifier: MIT diff --git a/pkg/resources/register_generated.go b/pkg/resources/register_generated.go index a274575..13ffec5 100644 --- a/pkg/resources/register_generated.go +++ b/pkg/resources/register_generated.go @@ -1,6 +1,6 @@ -// Code generated by Fabrica v0.4.2-1-gd8e9816-dirty. DO NOT EDIT. +// Code generated by Fabrica 0.4.5. DO NOT EDIT. // Template: cmd/fabrica/generate.go:generateVersionedRegistrationCode -// Generated: 2026-05-05T10:59:46Z +// Generated: 2026-05-13T10:45:16Z // // Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC //