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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ WORKDIR /app
# ubi9-micro doesn't include CA certificates; copy from builder for TLS (e.g. Google Pub/Sub)
COPY --from=builder /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem
COPY --from=builder /build/bin/hyperfleet-api /app/hyperfleet-api
COPY --from=builder /build/openapi/openapi.yaml /app/openapi/openapi.yaml
COPY --from=builder /build/LICENSE /licenses/LICENSE

USER 65532:65532
Expand Down
69 changes: 69 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,75 @@ test-helm: ## Test Helm charts (lint, template, validate)
--set-json 'adapters.nodepool=["validation","hypershift"]' > /dev/null
@echo "Full adapter config template OK"
@echo ""
@echo "Testing template with validation schema enabled..."
@OUTPUT=$$(helm template test-release charts/ \
--set image.registry=quay.io \
--set image.repository=openshift-hyperfleet/hyperfleet-api \
--set image.tag=test \
--set 'adapters.cluster=["validation"]' \
--set 'adapters.nodepool=["validation"]' \
--set validationSchema.enabled=true \
--set-string 'validationSchema.content=openapi: 3.0.0'); \
echo "$$OUTPUT" | grep -q 'app.kubernetes.io/component: validation-schema' || { echo "FAIL: validation-schema ConfigMap not found"; exit 1; }; \
echo "$$OUTPUT" | grep -q '/etc/hyperfleet/validation-schema' || { echo "FAIL: validation schema volume mount not found"; exit 1; }
@echo "Validation schema enabled config template OK"
@echo ""
@echo "Testing template with validation schema disabled (default)..."
@OUTPUT=$$(helm template test-release charts/ \
--set image.registry=quay.io \
--set image.repository=openshift-hyperfleet/hyperfleet-api \
--set image.tag=test \
--set 'adapters.cluster=["validation"]' \
--set 'adapters.nodepool=["validation"]'); \
if echo "$$OUTPUT" | grep -q 'validation-schema'; then echo "FAIL: validation-schema should not appear when disabled"; exit 1; fi
@echo "Validation schema disabled config template OK"
@echo ""
@echo "Testing template with validation schema existingConfigMap..."
@OUTPUT=$$(helm template test-release charts/ \
--set image.registry=quay.io \
--set image.repository=openshift-hyperfleet/hyperfleet-api \
--set image.tag=test \
--set 'adapters.cluster=["validation"]' \
--set 'adapters.nodepool=["validation"]' \
--set validationSchema.enabled=true \
--set validationSchema.existingConfigMap=my-validation-schema); \
echo "$$OUTPUT" | grep -q 'my-validation-schema' || { echo "FAIL: existingConfigMap name not found"; exit 1; }; \
if echo "$$OUTPUT" | grep -q 'app.kubernetes.io/component: validation-schema'; then echo "FAIL: generated ConfigMap should not appear with existingConfigMap"; exit 1; fi
@echo "Validation schema existingConfigMap config template OK"
@echo ""
@echo "Testing validation schema fails without content or existingConfigMap..."
@OUTPUT=$$(helm template test-release charts/ \
--set image.registry=quay.io \
--set image.repository=openshift-hyperfleet/hyperfleet-api \
--set image.tag=test \
--set 'adapters.cluster=["validation"]' \
--set 'adapters.nodepool=["validation"]' \
--set validationSchema.enabled=true 2>&1); \
if [ $$? -eq 0 ]; then \
echo "FAIL: should fail when validationSchema.enabled=true without content or existingConfigMap"; exit 1; \
fi; \
echo "$$OUTPUT" | grep -q 'validationSchema.content is required' || { \
echo "FAIL: expected validationSchema validation error message"; echo "$$OUTPUT"; exit 1; \
}
@echo "Validation schema validation (no content) OK"
@echo ""
@echo "Testing validation schema fails with whitespace-only content..."
@OUTPUT=$$(helm template test-release charts/ \
--set image.registry=quay.io \
--set image.repository=openshift-hyperfleet/hyperfleet-api \
--set image.tag=test \
--set 'adapters.cluster=["validation"]' \
--set 'adapters.nodepool=["validation"]' \
--set validationSchema.enabled=true \
--set-string 'validationSchema.content= ' 2>&1); \
if [ $$? -eq 0 ]; then \
echo "FAIL: should fail when validationSchema.content is whitespace-only"; exit 1; \
fi; \
echo "$$OUTPUT" | grep -q 'validationSchema.content is required' || { \
echo "FAIL: expected validationSchema validation error message"; echo "$$OUTPUT"; exit 1; \
}
@echo "Validation schema validation (whitespace-only content) OK"
@echo ""
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo "All Helm chart tests passed!"
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ This project uses [pre-commit](https://pre-commit.io/) for code quality checks.
- **[Deployment](docs/deployment.md)** - Container images, Kubernetes deployment, and configuration
- **[Authentication](docs/authentication.md)** - Development and production auth
- **[Logging](docs/logging.md)** - Structured logging, OpenTelemetry integration, and data masking
- **[Partner Schema Validation](openapi/README.md#partner-schema-validation)** - How to supply a partner-specific OpenAPI schema for runtime `spec` field validation
- **[Validation Schema](openapi/README.md#validation-schema)** - How to supply a custom OpenAPI schema for runtime `spec` field validation

### Additional Resources

Expand Down
21 changes: 21 additions & 0 deletions charts/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
HyperFleet API has been deployed.

Check deployment status:
kubectl get pods -l app.kubernetes.io/instance={{ .Release.Name }}

Access the API:
kubectl port-forward svc/{{ include "hyperfleet-api.fullname" . }} {{ .Values.ports.api | default 8000 }}:{{ .Values.ports.api | default 8000 }}

{{- if .Values.validationSchema.enabled }}

Validation schema validation is ENABLED.
{{- if .Values.validationSchema.existingConfigMap }}
Schema source: ConfigMap "{{ .Values.validationSchema.existingConfigMap }}"
{{- else }}
Schema source: inline content (generated ConfigMap)
{{- end }}
The API will fail to start if the schema is missing or invalid.
{{- end }}

Documentation:
https://github.com/openshift-hyperfleet/hyperfleet-api/blob/main/docs/deployment.md
1 change: 1 addition & 0 deletions charts/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ data:
hostname: {{ .Values.config.server.hostname | quote }}
host: {{ .Values.config.server.host | default "0.0.0.0" | quote }}
port: {{ .Values.config.server.port | default 8000 }}
openapi_schema_path: {{ ternary "/etc/hyperfleet/validation-schema/openapi.yaml" "openapi/openapi.yaml" .Values.validationSchema.enabled }}

timeouts:
read: {{ .Values.config.server.timeouts.read }}
Expand Down
23 changes: 23 additions & 0 deletions charts/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ spec:
# Checksum of generated ConfigMap
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- end }}
{{- if .Values.validationSchema.enabled }}
{{- if .Values.validationSchema.existingConfigMap }}
checksum/validation-schema: {{ (lookup "v1" "ConfigMap" .Release.Namespace .Values.validationSchema.existingConfigMap).data | toYaml | sha256sum }}
{{- else }}
checksum/validation-schema: {{ include (print $.Template.BasePath "/validation-schema-configmap.yaml") . | sha256sum }}
{{- end }}
{{- end }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
Expand Down Expand Up @@ -150,6 +157,12 @@ spec:
mountPath: /etc/hyperfleet
readOnly: true

{{- if .Values.validationSchema.enabled }}
- name: validation-schema
mountPath: /etc/hyperfleet/validation-schema
readOnly: true
{{- end }}

# Temp directory for writable filesystem
- name: tmp
mountPath: /tmp
Expand All @@ -173,6 +186,16 @@ spec:
- name: tmp
emptyDir: {}

{{- if .Values.validationSchema.enabled }}
- name: validation-schema
configMap:
{{- if .Values.validationSchema.existingConfigMap }}
name: {{ .Values.validationSchema.existingConfigMap }}
{{- else }}
name: {{ include "hyperfleet-api.fullname" . }}-validation-schema
{{- end }}
{{- end }}

{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 6 }}
{{- end }}
Expand Down
16 changes: 16 additions & 0 deletions charts/templates/validation-schema-configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{{- if and .Values.validationSchema.enabled (not .Values.validationSchema.existingConfigMap) }}
{{- if not (trim (default "" .Values.validationSchema.content)) }}
{{- fail "validationSchema.content is required when validationSchema.enabled is true and validationSchema.existingConfigMap is not set" }}
{{- end }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "hyperfleet-api.fullname" . }}-validation-schema
labels:
{{- include "hyperfleet-api.labels" . | nindent 4 }}
app.kubernetes.io/component: validation-schema
data:
openapi.yaml: |
{{ .Values.validationSchema.content | indent 4 }}
{{- end }}
39 changes: 39 additions & 0 deletions charts/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,45 @@ sidecars: []
# cpu: 50m
# memory: 64Mi

# ============================================================
# Validation Schema Configuration
# ============================================================
# Supply a custom OpenAPI schema for cluster/nodepool spec
# validation. When enabled, the schema content is mounted into
# the container and the API validates specs against it on every
# create/update request. The API will fail to start if the
# schema is invalid.
validationSchema:
enabled: false
# Use an existing ConfigMap instead of generating one from content.
# The ConfigMap must contain an "openapi.yaml" key with the schema.
# If set, validationSchema.content is ignored.
existingConfigMap: ""
# Inline OpenAPI 3.0 schema content. Must define ClusterSpec and
# NodePoolSpec under components.schemas.
# Example:
# content: |
# openapi: 3.0.0
# info:
# title: My Validation Schema
# version: 1.0.0
# paths: {}
# components:
# schemas:
# ClusterSpec:
# type: object
# required: [region]
# properties:
# region:
# type: string
# NodePoolSpec:
# type: object
# required: [machine_type]
# properties:
# machine_type:
# type: string
content: ""

Comment thread
coderabbitai[bot] marked this conversation as resolved.
# ============================================================
# Advanced Overrides (Escape Hatch)
# ============================================================
Expand Down
27 changes: 9 additions & 18 deletions cmd/hyperfleet-api/server/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,45 +96,36 @@ func (s *apiServer) routes(tracingEnabled bool) *mux.Router {
apiV1Router.HandleFunc("/openapi.html", openapiHandler.GetOpenAPIUI).Methods(http.MethodGet)
apiV1Router.HandleFunc("/openapi", openapiHandler.GetOpenAPI).Methods(http.MethodGet)

registerAPIMiddleware(apiV1Router)
err = registerAPIMiddleware(apiV1Router)
check(err, "Failed to initialize API middleware")

// Auto-discovered routes (no manual editing needed)
LoadDiscoveredRoutes(apiV1Router, services, authMiddleware)

return mainRouter
}

func registerAPIMiddleware(router *mux.Router) {
func registerAPIMiddleware(router *mux.Router) error {
router.Use(MetricsMiddleware)

// Schema validation middleware (validates cluster/nodepool spec fields)
// Load schema path from config (follows Flag > Env > Config File > Default priority)
schemaPath := env().Config.Server.OpenAPISchemaPath

// Initialize schema validator (non-blocking - will warn if schema not found)
// Use background context for initialization logging
ctx := context.Background()

schemaValidator, err := validators.NewSchemaValidator(schemaPath)
if err != nil {
// Log warning but don't fail - schema validation is optional
logger.With(ctx, logger.FieldSchemaPath, schemaPath).WithError(err).Warn("Failed to load schema validator")
logger.Warn(ctx, "Schema validation is disabled. Spec fields will not be validated.")
logger.Info(ctx, "To enable schema validation:")
logger.Info(ctx, " - Local: Run from repo root, or use --server-openapi-schema-path=openapi/openapi.yaml")
logger.Info(ctx, " - Config file: server.openapi_schema_path")
logger.Info(ctx, " - Environment: HYPERFLEET_SERVER_OPENAPI_SCHEMA_PATH")
} else {
// Apply schema validation middleware
logger.With(ctx, logger.FieldSchemaPath, schemaPath).Info("Schema validation enabled")
router.Use(middleware.SchemaValidationMiddleware(schemaValidator))
return fmt.Errorf("schema validation required but failed to load from %s: %w", schemaPath, err)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

logger.With(ctx, logger.FieldSchemaPath, schemaPath).Info("Schema validation enabled")
router.Use(middleware.SchemaValidationMiddleware(schemaValidator))

router.Use(
func(next http.Handler) http.Handler {
return db.TransactionMiddleware(next, env().Database.SessionFactory, env().Config.Database.Pool.RequestTimeout)
},
)

router.Use(gorillahandlers.CompressHandler)

return nil
}
3 changes: 3 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ HTTP server settings for the API endpoint.
| `server.hostname` | string | `""` | Public hostname for logging (optional) |
| `server.host` | string | `localhost` | Server bind host (`0.0.0.0` for Kubernetes) |
| `server.port` | int | `8000` | Server bind port |
| `server.openapi_schema_path` | string | `openapi/openapi.yaml` | Path to OpenAPI schema for spec validation. API fails to start if missing or invalid. |
| `server.timeouts.read` | duration | `5s` | HTTP read timeout |
| `server.timeouts.write` | duration | `30s` | HTTP write timeout |
| `server.tls.enabled` | bool | `false` | Enable HTTPS/TLS |
Expand Down Expand Up @@ -341,6 +342,7 @@ Complete table of all configuration properties, their environment variables, and
| `server.hostname` | `HYPERFLEET_SERVER_HOSTNAME` | string | `""` |
| `server.host` | `HYPERFLEET_SERVER_HOST` | string | `localhost` |
| `server.port` | `HYPERFLEET_SERVER_PORT` | int | `8000` |
| `server.openapi_schema_path` | `HYPERFLEET_SERVER_OPENAPI_SCHEMA_PATH` | string | `openapi/openapi.yaml` |
| `server.timeouts.read` | `HYPERFLEET_SERVER_TIMEOUTS_READ` | duration | `5s` |
| `server.timeouts.write` | `HYPERFLEET_SERVER_TIMEOUTS_WRITE` | duration | `30s` |
| `server.tls.enabled` | `HYPERFLEET_SERVER_TLS_ENABLED` | bool | `false` |
Expand Down Expand Up @@ -402,6 +404,7 @@ All CLI flags and their corresponding configuration paths.
| `--server-hostname` | `server.hostname` | string |
| `--server-host` | `server.host` | string |
| `--server-port` | `server.port` | int |
| `--server-openapi-schema-path` | `server.openapi_schema_path` | string |
| `--server-read-timeout` | `server.timeouts.read` | duration |
| `--server-write-timeout` | `server.timeouts.write` | duration |
| `--server-https-enabled` | `server.tls.enabled` | bool |
Expand Down
41 changes: 40 additions & 1 deletion docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,46 @@ HyperFleet API is configured via environment variables and configuration files.

The API validates cluster and nodepool `spec` fields against an OpenAPI schema. This allows different providers (GCP, AWS, Azure) to have different spec structures.

Schema validation is optional and file-path based. Set `--server-openapi-schema-path` (or `HYPERFLEET_SERVER_OPENAPI_SCHEMA_PATH`) to a provider-specific OpenAPI schema file to enable it. If the path is missing or the file is unreadable, the API logs a warning and starts without validation — startup is non-blocking.
The schema path is configured via `--server-openapi-schema-path` (or `HYPERFLEET_SERVER_OPENAPI_SCHEMA_PATH`). The default is `openapi/openapi.yaml`. The API **will fail to start** if the configured schema file is missing, unreadable, or invalid — this ensures misconfigured deployments are caught immediately rather than silently accepting invalid data.

#### Validation Schema via Helm

Partners can supply a custom OpenAPI schema using the Helm chart:

```yaml
validationSchema:
enabled: true
content: |
openapi: 3.0.0
info:
title: My Validation Schema
version: 1.0.0
paths: {}
components:
schemas:
ClusterSpec:
type: object
required: [region]
properties:
region:
type: string
NodePoolSpec:
type: object
required: [machine_type]
properties:
machine_type:
type: string
```

When `validationSchema.enabled` is `true`, the chart creates a ConfigMap with the schema content, mounts it into the container, and sets `server.openapi_schema_path` in the generated config file to point to it.

Alternatively, reference an existing ConfigMap (must contain an `openapi.yaml` key):

```yaml
validationSchema:
enabled: true
existingConfigMap: my-validation-schema
```

See [Configuration Guide](config.md) for all configuration options.

Expand Down
12 changes: 6 additions & 6 deletions openapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ OpenAPI schemas are **not authored here**. They are defined in the [`hyperfleet-

**Never edit `openapi.yaml` or `openapi.gen.go` directly.** Both are overwritten by `make generate`.

## Partner Schema Validation
## Validation Schema

### Why this exists

HyperFleet API is intentionally schema-agnostic at its core: it stores clusters and nodepools as long as the `spec` field is present and non-null, without caring what is inside it. This is by design — the API serves multiple partners with different provider-specific payloads.
HyperFleet API is intentionally schema-agnostic at its core: it stores clusters and nodepools as long as the `spec` field is present and non-null, without caring what is inside it. This is by design — the API serves multiple deployments with different provider-specific payloads.

Partners, however, **do** care. A GCP partner might require a `region` field inside `spec`; an AWS partner might require an `instanceType`. Without validation, invalid or incomplete specs silently end up in the database and only fail later when a downstream component tries to use them.
Deployers, however, **do** care. A GCP deployment might require a `region` field inside `spec`; an AWS deployment might require an `instanceType`. Without validation, invalid or incomplete specs silently end up in the database and only fail later when a downstream component tries to use them.

The `--server-openapi-schema-path` flag solves this: at deploy time, the operator points the API at a partner-specific OpenAPI schema file. The API then validates every `POST`/`PATCH` request's `spec` payload against that schema in HTTP middleware — before any service or database code runs.
The `--server-openapi-schema-path` flag solves this: at deploy time, the operator points the API at a deployment-specific OpenAPI schema file. The API then validates every `POST`/`PATCH` request's `spec` payload against that schema in HTTP middleware — before any service or database code runs.

### What the schema file must contain

Expand All @@ -48,12 +48,12 @@ The schema file must be a valid OpenAPI 3.0 document. The API looks up two speci
| `cluster` | `components.schemas.ClusterSpec` |
| `nodepool` | `components.schemas.NodePoolSpec` |

A minimal example for a GCP partner:
A minimal example for a GCP deployment:

```yaml
openapi: 3.0.0
info:
title: HyperFleet GCP Partner Schema
title: HyperFleet GCP Validation Schema
version: 1.0.0
paths: {}
components:
Expand Down
Loading