Skip to content

Commit 52e3a10

Browse files
ciaranRocheclaude
andcommitted
POC improvements: CRD fixes, FIPS 140-3 migration, schema hardening
- Add "Unknown" to condition status enum in all CRDs per K8s metav1.ConditionStatus - Add ClusterRoleBinding for admin ClusterRole - Migrate Makefile from BoringCrypto/GOEXPERIMENT to Go 1.25 native FIPS 140-3 (GOFIPS140/GODEBUG) - Add BearerAuth security requirement to buildCreateStatusOperation - Fix silent constraint drops in CRD-to-OpenAPI schema conversion (handle []interface{}, float64, additionalProperties as object) - Bump generation in Replace when spec changes - Remove redundant value-type presenter registrations - Fix getOwnerPlural fallback to use lowercase Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3bc3234 commit 52e3a10

9 files changed

Lines changed: 106 additions & 37 deletions

File tree

Makefile

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
# Include bingo-managed tool variables
44
include .bingo/Variables.mk
55

6-
# CGO_ENABLED=0 is not FIPS compliant. large commercial vendors and FedRAMP require FIPS compliant crypto
7-
# Use ?= to allow Dockerfile to override (CGO_ENABLED=0 for Alpine-based dev images)
8-
CGO_ENABLED ?= 1
6+
# Go 1.25+ uses native FIPS 140-3 support (GOFIPS140) instead of BoringCrypto/GOEXPERIMENT.
7+
# CGO is no longer required for FIPS compliance. Set GOFIPS140=latest at build time and
8+
# GODEBUG=fips140=on at runtime to enable FIPS 140-3 mode.
9+
# Use ?= to allow Dockerfile to override
10+
CGO_ENABLED ?= 0
11+
GOFIPS140 ?= latest
912

1013
# Enable users to override the golang used to accomodate custom installations
1114
GO ?= go
@@ -69,7 +72,7 @@ help:
6972
.PHONY: help
7073

7174
# Encourage consistent tool versions
72-
GO_VERSION:=go1.24.
75+
GO_VERSION:=go1.25.
7376

7477
### Constants:
7578
version:=$(shell date +%s)
@@ -135,12 +138,12 @@ lint: $(GOLANGCI_LINT)
135138
build: check-gopath generate-mocks
136139
@mkdir -p bin
137140
echo "Building version: ${build_version}"
138-
CGO_ENABLED=$(CGO_ENABLED) GOEXPERIMENT=boringcrypto ${GO} build -ldflags="$(ldflags)" -o bin/hyperfleet-api ./cmd/hyperfleet-api
141+
CGO_ENABLED=$(CGO_ENABLED) GOFIPS140=$(GOFIPS140) ${GO} build -ldflags="$(ldflags)" -o bin/hyperfleet-api ./cmd/hyperfleet-api
139142
.PHONY: build
140143

141144
# Install
142145
install: check-gopath generate-mocks
143-
CGO_ENABLED=$(CGO_ENABLED) GOEXPERIMENT=boringcrypto ${GO} install -ldflags="$(ldflags)" ./cmd/hyperfleet-api
146+
CGO_ENABLED=$(CGO_ENABLED) GOFIPS140=$(GOFIPS140) ${GO} install -ldflags="$(ldflags)" ./cmd/hyperfleet-api
144147
@ ${GO} version | grep -q "$(GO_VERSION)" || \
145148
( \
146149
printf '\033[41m\033[97m\n'; \
@@ -173,7 +176,7 @@ secrets:
173176
# Examples:
174177
# make test TESTFLAGS="-run TestSomething"
175178
test: install secrets $(GOTESTSUM)
176-
OCM_ENV=unit_testing $(GOTESTSUM) --format $(TEST_SUMMARY_FORMAT) -- -p 1 -v $(TESTFLAGS) \
179+
GODEBUG=fips140=on OCM_ENV=unit_testing $(GOTESTSUM) --format $(TEST_SUMMARY_FORMAT) -- -p 1 -v $(TESTFLAGS) \
177180
./pkg/... \
178181
./cmd/...
179182
.PHONY: test
@@ -186,7 +189,7 @@ test: install secrets $(GOTESTSUM)
186189
# Examples:
187190
# make test-unit-json TESTFLAGS="-run TestSomething"
188191
ci-test-unit: install secrets $(GOTESTSUM)
189-
OCM_ENV=unit_testing $(GOTESTSUM) --jsonfile-timing-events=$(unit_test_json_output) --format $(TEST_SUMMARY_FORMAT) -- -p 1 -v $(TESTFLAGS) \
192+
GODEBUG=fips140=on OCM_ENV=unit_testing $(GOTESTSUM) --jsonfile-timing-events=$(unit_test_json_output) --format $(TEST_SUMMARY_FORMAT) -- -p 1 -v $(TESTFLAGS) \
190193
./pkg/... \
191194
./cmd/...
192195
.PHONY: ci-test-unit
@@ -202,7 +205,7 @@ ci-test-unit: install secrets $(GOTESTSUM)
202205
# make test-integration TESTFLAGS="-run TestAccountsGet" runs TestAccountsGet
203206
# make test-integration TESTFLAGS="-short" skips long-run tests
204207
ci-test-integration: install secrets $(GOTESTSUM)
205-
TESTCONTAINERS_RYUK_DISABLED=true OCM_ENV=integration_testing $(GOTESTSUM) --jsonfile-timing-events=$(integration_test_json_output) --format $(TEST_SUMMARY_FORMAT) -- -p 1 -ldflags -s -v -timeout 1h $(TESTFLAGS) \
208+
GODEBUG=fips140=on TESTCONTAINERS_RYUK_DISABLED=true OCM_ENV=integration_testing $(GOTESTSUM) --jsonfile-timing-events=$(integration_test_json_output) --format $(TEST_SUMMARY_FORMAT) -- -p 1 -ldflags -s -v -timeout 1h $(TESTFLAGS) \
206209
./test/integration
207210
.PHONY: ci-test-integration
208211

@@ -217,7 +220,7 @@ ci-test-integration: install secrets $(GOTESTSUM)
217220
# make test-integration TESTFLAGS="-run TestAccountsGet" runs TestAccountsGet
218221
# make test-integration TESTFLAGS="-short" skips long-run tests
219222
test-integration: install secrets $(GOTESTSUM)
220-
TESTCONTAINERS_RYUK_DISABLED=true OCM_ENV=integration_testing $(GOTESTSUM) --format $(TEST_SUMMARY_FORMAT) -- -p 1 -ldflags -s -v -timeout 1h $(TESTFLAGS) \
223+
GODEBUG=fips140=on TESTCONTAINERS_RYUK_DISABLED=true OCM_ENV=integration_testing $(GOTESTSUM) --format $(TEST_SUMMARY_FORMAT) -- -p 1 -ldflags -s -v -timeout 1h $(TESTFLAGS) \
221224
./test/integration
222225
.PHONY: test-integration
223226

@@ -233,13 +236,13 @@ generate-all: generate-mocks
233236
.PHONY: generate-all
234237

235238
run: build
236-
./bin/hyperfleet-api migrate
237-
CRD_PATH=$(PWD)/charts/crds ./bin/hyperfleet-api serve
239+
GODEBUG=fips140=on ./bin/hyperfleet-api migrate
240+
GODEBUG=fips140=on CRD_PATH=$(PWD)/charts/crds ./bin/hyperfleet-api serve
238241
.PHONY: run
239242

240243
run-no-auth: build
241-
./bin/hyperfleet-api migrate
242-
CRD_PATH=$(PWD)/charts/crds ./bin/hyperfleet-api serve --enable-authz=false --enable-jwt=false
244+
GODEBUG=fips140=on ./bin/hyperfleet-api migrate
245+
GODEBUG=fips140=on CRD_PATH=$(PWD)/charts/crds ./bin/hyperfleet-api serve --enable-authz=false --enable-jwt=false
243246
.PHONY: run-no-auth
244247

245248
# Run Swagger and host the api docs
@@ -261,7 +264,7 @@ clean:
261264
cmds:
262265
@mkdir -p bin
263266
for cmd in $$(ls cmd); do \
264-
CGO_ENABLED=$(CGO_ENABLED) ${GO} build \
267+
CGO_ENABLED=$(CGO_ENABLED) GOFIPS140=$(GOFIPS140) ${GO} build \
265268
-ldflags="$(ldflags)" \
266269
-o "bin/$${cmd}" \
267270
"./cmd/$${cmd}" \

charts/crds/cluster-crd.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ spec:
6262
enum:
6363
- "True"
6464
- "False"
65+
- "Unknown"
6566
reason:
6667
type: string
6768
description: Machine-readable reason for the condition's last transition

charts/crds/idp-crd.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ spec:
7878
enum:
7979
- "True"
8080
- "False"
81+
- "Unknown"
8182
reason:
8283
type: string
8384
description: Machine-readable reason for the condition's last transition

charts/crds/nodepool-crd.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ spec:
6868
enum:
6969
- "True"
7070
- "False"
71+
- "Unknown"
7172
reason:
7273
type: string
7374
description: Machine-readable reason for the condition's last transition
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{{- if .Values.rbac.create -}}
2+
apiVersion: rbac.authorization.k8s.io/v1
3+
kind: ClusterRoleBinding
4+
metadata:
5+
name: {{ include "hyperfleet-api.fullname" . }}-admin
6+
labels:
7+
{{- include "hyperfleet-api.labels" . | nindent 4 }}
8+
roleRef:
9+
apiGroup: rbac.authorization.k8s.io
10+
kind: ClusterRole
11+
name: {{ include "hyperfleet-api.fullname" . }}-admin
12+
subjects:
13+
- kind: ServiceAccount
14+
name: {{ include "hyperfleet-api.serviceAccountName" . }}
15+
namespace: {{ .Release.Namespace }}
16+
{{- end }}

pkg/openapi/paths.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ func buildCreateStatusOperation(def *api.ResourceDefinition, ownerDef *api.Resou
337337
},
338338
},
339339
Responses: buildStatusCreateResponses(),
340+
Security: &openapi3.SecurityRequirements{{"BearerAuth": {}}},
340341
}
341342
}
342343

pkg/openapi/schemas.go

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -278,18 +278,13 @@ func applySchemaProperties(schema *openapi3.Schema, crdSchema map[string]interfa
278278
}
279279
}
280280

281-
if required, ok := crdSchema["required"].([]string); ok {
282-
schema.Required = required
283-
}
281+
schema.Required = toStringSlice(crdSchema["required"])
284282

285283
if desc, ok := crdSchema["description"].(string); ok {
286284
schema.Description = desc
287285
}
288286

289-
// Handle additionalProperties
290-
if addProps, ok := crdSchema["additionalProperties"].(bool); ok && addProps {
291-
schema.AdditionalProperties = openapi3.AdditionalProperties{Has: boolPtr(true)}
292-
}
287+
applyAdditionalProperties(schema, crdSchema)
293288
}
294289

295290
// convertCRDSchemaToOpenAPI converts a CRD schema map to an OpenAPI SchemaRef.
@@ -320,13 +315,12 @@ func convertCRDSchemaToOpenAPI(crdSchema map[string]interface{}) *openapi3.Schem
320315
schema.Max = &max
321316
}
322317

323-
if minLen, ok := crdSchema["minLength"].(int64); ok {
324-
schema.MinLength = uint64(minLen)
318+
if v, ok := toUint64(crdSchema["minLength"]); ok {
319+
schema.MinLength = v
325320
}
326321

327-
if maxLen, ok := crdSchema["maxLength"].(int64); ok {
328-
uval := uint64(maxLen)
329-
schema.MaxLength = &uval
322+
if v, ok := toUint64(crdSchema["maxLength"]); ok {
323+
schema.MaxLength = &v
330324
}
331325

332326
if pattern, ok := crdSchema["pattern"].(string); ok {
@@ -346,15 +340,58 @@ func convertCRDSchemaToOpenAPI(crdSchema map[string]interface{}) *openapi3.Schem
346340
schema.Items = convertCRDSchemaToOpenAPI(items)
347341
}
348342

349-
if required, ok := crdSchema["required"].([]string); ok {
350-
schema.Required = required
343+
schema.Required = toStringSlice(crdSchema["required"])
344+
345+
applyAdditionalProperties(schema, crdSchema)
346+
347+
return &openapi3.SchemaRef{Value: schema}
348+
}
349+
350+
// toStringSlice converts an interface{} (typically []interface{} from JSON/YAML unmarshal) to []string.
351+
// Returns nil if the value is nil or not convertible.
352+
func toStringSlice(v interface{}) []string {
353+
switch val := v.(type) {
354+
case []string:
355+
return val
356+
case []interface{}:
357+
result := make([]string, 0, len(val))
358+
for _, item := range val {
359+
if s, ok := item.(string); ok {
360+
result = append(result, s)
361+
}
362+
}
363+
return result
351364
}
365+
return nil
366+
}
352367

353-
if addProps, ok := crdSchema["additionalProperties"].(bool); ok && addProps {
354-
schema.AdditionalProperties = openapi3.AdditionalProperties{Has: boolPtr(true)}
368+
// toUint64 converts an interface{} (typically float64 or int from JSON/YAML unmarshal) to uint64.
369+
func toUint64(v interface{}) (uint64, bool) {
370+
switch val := v.(type) {
371+
case float64:
372+
return uint64(val), true
373+
case int:
374+
return uint64(val), true
375+
case int64:
376+
return uint64(val), true
355377
}
378+
return 0, false
379+
}
356380

357-
return &openapi3.SchemaRef{Value: schema}
381+
// applyAdditionalProperties handles the additionalProperties field which can be a bool or an object.
382+
func applyAdditionalProperties(schema *openapi3.Schema, crdSchema map[string]interface{}) {
383+
ap := crdSchema["additionalProperties"]
384+
if ap == nil {
385+
return
386+
}
387+
switch val := ap.(type) {
388+
case bool:
389+
schema.AdditionalProperties = openapi3.AdditionalProperties{Has: boolPtr(val)}
390+
case map[string]interface{}:
391+
schema.AdditionalProperties = openapi3.AdditionalProperties{
392+
Schema: convertCRDSchemaToOpenAPI(val),
393+
}
394+
}
358395
}
359396

360397
// uint64Ptr returns a pointer to a uint64 value.

pkg/services/resource.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package services
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/json"
67
stderrors "errors"
@@ -109,7 +110,16 @@ func (s *sqlResourceService) Create(ctx context.Context, resource *api.Resource,
109110
}
110111

111112
func (s *sqlResourceService) Replace(ctx context.Context, resource *api.Resource) (*api.Resource, *errors.ServiceError) {
112-
resource, err := s.resourceDao.Replace(ctx, resource)
113+
existing, err := s.resourceDao.GetByKindAndID(ctx, resource.Kind, resource.ID)
114+
if err != nil {
115+
return nil, handleGetError(resource.Kind, "id", resource.ID, err)
116+
}
117+
118+
if !bytes.Equal(existing.Spec, resource.Spec) {
119+
resource.Generation = existing.Generation + 1
120+
}
121+
122+
resource, err = s.resourceDao.Replace(ctx, resource)
113123
if err != nil {
114124
return nil, handleUpdateError(resource.Kind, err)
115125
}

plugins/resources/plugin.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"context"
77
"net/http"
88
"os"
9+
"strings"
910

1011
"github.com/gorilla/mux"
1112
"github.com/openshift-hyperfleet/hyperfleet-api/cmd/hyperfleet-api/environments"
@@ -99,10 +100,8 @@ func init() {
99100
}
100101
})
101102

102-
// Presenter registration for Resource type
103-
presenters.RegisterPath(api.Resource{}, "resources")
103+
// Presenter registration for Resource type (pointer only)
104104
presenters.RegisterPath(&api.Resource{}, "resources")
105-
presenters.RegisterKind(api.Resource{}, "Resource")
106105
presenters.RegisterKind(&api.Resource{}, "Resource")
107106
}
108107

@@ -161,5 +160,5 @@ func getOwnerPlural(kind string) string {
161160
return def.Plural
162161
}
163162
// Fallback: lowercase + "s"
164-
return kind + "s"
163+
return strings.ToLower(kind) + "s"
165164
}

0 commit comments

Comments
 (0)