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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- JWT authentication handler using `golang-jwt/jwt/v5` and `MicahParks/keyfunc/v3` with RS256 validation, configurable issuer and audience, and JWKS key rotation support ([#120](https://github.com/openshift-hyperfleet/hyperfleet-api/pull/120))
- Hard deletion for Clusters and NodePools: resources and their adapter statuses are permanently removed from the database once all required adapters report `Finalized=True` and no child resources remain ([#119](https://github.com/openshift-hyperfleet/hyperfleet-api/pull/119))
- `Finalized` condition aggregation with `WaitingForChildResources` intermediate state when all adapters are finalized but child node pools still exist ([#119](https://github.com/openshift-hyperfleet/hyperfleet-api/pull/119))
- Soft deletion for Clusters and NodePools with `deleted_time` and `deleted_by` fields for tracking deletion requests ([#106](https://github.com/openshift-hyperfleet/hyperfleet-api/pull/106))
Expand All @@ -28,6 +29,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Replaced OCM SDK authentication handler with standalone JWT middleware, removing `ocm-sdk-go` dependency and its transitive dependencies (`glog`, `bluemonday`, `json-iterator`) ([#120](https://github.com/openshift-hyperfleet/hyperfleet-api/pull/120))
- Upgraded JWT library from `golang-jwt/jwt/v4` to `golang-jwt/jwt/v5` ([#120](https://github.com/openshift-hyperfleet/hyperfleet-api/pull/120))
- Refactored `AdapterStatusDao.Upsert()` to accept a pre-fetched existing record, moving lookup and `LastTransitionTime` preservation logic to the service layer ([#119](https://github.com/openshift-hyperfleet/hyperfleet-api/pull/119))
- Refactored DAO methods to remove Unscoped calls for fetching Clusters and NodePools ([#106](https://github.com/openshift-hyperfleet/hyperfleet-api/pull/106))
- Bumped oapi-codegen version to fix missing `omitempty` on generated response objects ([#106](https://github.com/openshift-hyperfleet/hyperfleet-api/pull/106))
Expand All @@ -37,6 +40,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Streamlined configuration system with Viper, removed getters and _FILE suffix pattern ([#75](https://github.com/openshift-hyperfleet/hyperfleet-api/pull/75))
- Used CHANGE_ME placeholder for image registry ([#83](https://github.com/openshift-hyperfleet/hyperfleet-api/pull/83))

### Removed

- OCM SDK dependency (`ocm-sdk-go`), OCM client (`pkg/client/ocm/`), OCM configuration (`pkg/config/ocm.go`), OCM logger bridge (`pkg/logger/ocm_bridge.go`), and OCM authorization mocks ([#120](https://github.com/openshift-hyperfleet/hyperfleet-api/pull/120))

### Fixed

- Validated adapter status conditions in handler layer ([#88](https://github.com/openshift-hyperfleet/hyperfleet-api/pull/88))
Expand Down
7 changes: 1 addition & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,6 @@ ifndef TEST_SUMMARY_FORMAT
TEST_SUMMARY_FORMAT = short-verbose
endif

ifndef OCM_BASE_URL
OCM_BASE_URL := "https://api.integration.openshift.com"
endif

.PHONY: help
help: ## Display this help
Expand Down Expand Up @@ -191,9 +188,7 @@ secrets: ## Initialize secrets directory with default values
@printf "$(db_password)" > secrets/db.password
@printf "$(db_port)" > secrets/db.port
@printf "$(db_user)" > secrets/db.user
@printf "ocm-hyperfleet-testing" > secrets/ocm-service.clientId
@printf "your-client-secret-here" > secrets/ocm-service.clientSecret
@printf "your-token-here" > secrets/ocm-service.token

@echo "Secrets directory initialized with default values"

##@ Testing
Expand Down
12 changes: 0 additions & 12 deletions PREREQUISITES.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,6 @@ PostgreSQL client tools provide the `psql` command-line interface for database i
- **Installation**: Follow the instructions on the [jq official website](https://jqlang.github.io/jq/)
- **Verification**: Run `jq --version`

## ocm CLI (Optional)

`ocm` stands for OpenShift Cluster Manager CLI and is used for authentication in production mode.

- **Purpose**: CLI tool for authenticating with OCM and making authenticated API requests
- **Installation**: Refer to the [OCM CLI documentation](https://github.com/openshift-online/ocm-cli)
- **Note**: Only required when running with authentication enabled (production mode)
- **Development**: For local development, use `make run-no-auth` which bypasses authentication

## Quick Verification

Run these commands to verify all prerequisites are installed:
Expand All @@ -57,9 +48,6 @@ go version # Should show 1.24 or higher
podman --version
psql --version # PostgreSQL client
jq --version # JSON processor

# Optional tools
ocm version # OCM CLI (production auth only)
```

## Getting Started
Expand Down
3 changes: 0 additions & 3 deletions cmd/hyperfleet-api/environments/e_development.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ func (e *devEnvImpl) OverrideConfig(c *config.ApplicationConfig) error {
c.Database.SSL.Mode = SSLModeDisable
}

// Enable OCM mocks for development (no real OCM connection needed)
c.OCM.Mock.Enabled = true

return nil
}

Expand Down
3 changes: 0 additions & 3 deletions cmd/hyperfleet-api/environments/e_integration_testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ func (e *integrationTestingEnvImpl) OverrideConfig(c *config.ApplicationConfig)
c.Database.SSL.Mode = SSLModeDisable
}

// Enable OCM mocks for integration testing (no real OCM connection needed)
c.OCM.Mock.Enabled = true

return nil
}

Expand Down
4 changes: 1 addition & 3 deletions cmd/hyperfleet-api/environments/e_production.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ func (e *productionEnvImpl) OverrideClients(c *Clients) error {

func (e *productionEnvImpl) EnvironmentDefaults() map[string]string {
return map[string]string{
"v": "1",
"ocm-debug": "false",
"enable-ocm-mock": "false",
"v": "1",
}
}
4 changes: 0 additions & 4 deletions cmd/hyperfleet-api/environments/e_unit_testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ func (e *unitTestingEnvImpl) OverrideConfig(c *config.ApplicationConfig) error {
if c.Database.SSL.Mode == "" {
c.Database.SSL.Mode = SSLModeDisable
}

// Enable OCM mocks for unit testing (no real OCM connection needed)
c.OCM.Mock.Enabled = true

// Unit tests use a mock DB and don't need real credentials
return nil
}
Expand Down
48 changes: 2 additions & 46 deletions cmd/hyperfleet-api/environments/framework.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"github.com/spf13/pflag"

"github.com/openshift-hyperfleet/hyperfleet-api/cmd/hyperfleet-api/environments/registry"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/client/ocm"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/config"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/errors"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/logger"
Expand All @@ -30,7 +29,7 @@ func init() {
})
}

// EnvironmentImpl defines a set of behaviors for an OCM environment.
// EnvironmentImpl defines a set of behaviors for a runtime environment.
// Each environment provides a set of flags for basic set/override of the environment
// and configuration functions for each component type.
type EnvironmentImpl interface {
Expand Down Expand Up @@ -58,8 +57,6 @@ func Environment() *Env {
// This is used by the new config system to apply environment-specific settings
// (e.g., development environment disables JWT and TLS)
func ApplyEnvironmentOverrides(cfg *config.ApplicationConfig) error {
// Read current environment from env var instead of using cached environment.Name
// to ensure we use the most up-to-date value
envName := GetEnvironmentStrFromEnv()
envImpl, found := environments[envName]
if !found {
Expand All @@ -69,13 +66,12 @@ func ApplyEnvironmentOverrides(cfg *config.ApplicationConfig) error {
}

// SetEnvironmentDefaults sets environment-specific flag defaults
// This is used for environment-specific behavior flags (e.g., verbose mode, OCM debug)
func (e *Env) SetEnvironmentDefaults(flags *pflag.FlagSet) error {
return setFlagDefaults(flags, environments[e.Name].EnvironmentDefaults())
}

// Initialize loads the environment's resources
// This should be called after the e.Config has been set appropriately though AddFlags and pasing, done elsewhere
// This should be called after the e.Config has been set appropriately though AddFlags and parsing, done elsewhere
// The environment does NOT handle flag parsing
func (e *Env) Initialize() error {
ctx := context.Background()
Expand Down Expand Up @@ -103,10 +99,6 @@ func (e *Env) Initialize() error {
os.Exit(1)
}

err := e.LoadClients()
if err != nil {
return err
}
if err := envImpl.OverrideClients(&e.Clients); err != nil {
logger.WithError(ctx, err).Error("Failed to configure Clients")
os.Exit(1)
Expand Down Expand Up @@ -136,53 +128,17 @@ func (e *Env) Seed() *errors.ServiceError {
}

func (e *Env) LoadServices() {
// Initialize the service registry map
e.Services.serviceRegistry = make(map[string]interface{})

// Auto-discovered services (no manual editing needed)
registry.LoadDiscoveredServices(&e.Services, e)
}

func (e *Env) LoadClients() error {
ctx := context.Background()
var err error

ocmConfig := ocm.Config{
BaseURL: e.Config.OCM.BaseURL,
ClientID: e.Config.OCM.ClientID,
ClientSecret: e.Config.OCM.ClientSecret,
SelfToken: e.Config.OCM.SelfToken,
TokenURL: e.Config.OCM.TokenURL,
Debug: e.Config.OCM.Debug,
}

// Create OCM Authz client
if e.Config.OCM.Mock.Enabled {
logger.Info(ctx, "Using Mock OCM Authz Client")
e.Clients.OCM, err = ocm.NewClientMock(ocmConfig)
} else {
e.Clients.OCM, err = ocm.NewClient(ocmConfig)
}
if err != nil {
logger.WithError(ctx, err).Error("Unable to create OCM Authz client")
return err
}

return nil
}

func (e *Env) Teardown() {
ctx := context.Background()
if e.Database.SessionFactory != nil {
if err := e.Database.SessionFactory.Close(); err != nil {
logger.WithError(ctx, err).Error("Error closing database session factory")
}
}
if e.Clients.OCM != nil {
if err := e.Clients.OCM.Close(); err != nil {
logger.WithError(ctx, err).Error("Error closing OCM client")
}
}
}

func setFlagDefaults(flags *pflag.FlagSet, defaults map[string]string) error {
Expand Down
15 changes: 0 additions & 15 deletions cmd/hyperfleet-api/environments/framework_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package environments

import (
"os/exec"
"reflect"
"testing"

Expand All @@ -10,20 +9,6 @@ import (
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/config"
)

func BenchmarkGetDynos(b *testing.B) {
b.ReportAllocs()
fn := func(b *testing.B) {
cmd := exec.Command("ocm", "get", "/api/hyperfleet/v1/clusters", "params='size=2'")
_, err := cmd.CombinedOutput()
if err != nil {
b.Errorf("ERROR %+v", err)
}
}
for n := 0; n < b.N; n++ {
fn(b)
}
}

func TestLoadServices(t *testing.T) {
// Set environment to unit_testing to use mocks
t.Setenv("OCM_ENV", "unit_testing")
Expand Down
6 changes: 1 addition & 5 deletions cmd/hyperfleet-api/environments/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"sync"

"github.com/openshift-hyperfleet/hyperfleet-api/pkg/auth"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/client/ocm"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/config"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/db"
)
Expand Down Expand Up @@ -66,15 +65,12 @@ func (s *Services) SetService(name string, service interface{}) {
s.serviceRegistry[name] = service
}

type Clients struct {
OCM *ocm.Client
}
type Clients struct{}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

nit — non-blocking suggestion

Category: Architecture

After removing the OCM client, several abstractions are now vestigial:

  • Clients struct is empty — OverrideClients across all environment impls does nothing
  • AuthorizationMiddleware interface has no real implementation (only the passthrough mock). It's still threaded through RouteRegistrationFunc and plugin registrations as dead weight
  • ConfigDefaults struct appears unreferenced

Consider a follow-up PR to clean up this dead plumbing, or track it as tech debt.


type ConfigDefaults struct {
Server map[string]interface{}
Metrics map[string]interface{}
Database map[string]interface{}
OCM map[string]interface{}
Options map[string]interface{}
}

Expand Down
36 changes: 17 additions & 19 deletions cmd/hyperfleet-api/server/api_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import (
"time"

gorillahandlers "github.com/gorilla/handlers"
"github.com/openshift-online/ocm-sdk-go/authentication"

"github.com/openshift-hyperfleet/hyperfleet-api/cmd/hyperfleet-api/environments"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/auth"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/logger"
)

Expand All @@ -34,30 +34,28 @@ func NewAPIServer(tracingEnabled bool) Server {
var mainHandler http.Handler = mainRouter

if env().Config.Server.JWT.Enabled {
// Create the logger for the authentication handler using slog bridge
authnLogger := logger.NewOCMLoggerBridge()

// Create the handler that verifies that tokens are valid:
var err error
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Blocking

Category: Bug

NewJWTHandler receives context.Background() here, but keyfunc.NewDefaultCtx / keyfunc.New inside buildKeyfunc starts a background goroutine for JWKS key refresh. Since context.Background() never cancels, there's no way to stop that goroutine on shutdown.

The old OCM client had e.Clients.OCM.Close() in Teardown() — this PR removes it with no replacement for the new JWKS resource.

Suggested approach:

  1. Pass a cancellable context to NewJWTHandler (e.g., from a server-lifecycle context)
  2. Store the cancel func on jwtHandler and add a Close() method
  3. Call Close() during server shutdown (or from Teardown())
type jwtHandler struct {
    keyfunc  keyfunc.Keyfunc
    parser   *jwt.Parser
    publicPatterns []*regexp.Regexp
    next     http.Handler
    cancel   context.CancelFunc // add this
}

func (h *jwtHandler) Close() {
    if h.cancel != nil {
        h.cancel()
    }
}

And in api_server.go:

ctx, cancel := context.WithCancel(serverCtx) // not context.Background()
mainHandler, err := auth.NewJWTHandler(ctx, auth.JWTHandlerConfig{...})
// store cancel or handler for shutdown

mainHandler, err = authentication.NewHandler().
Logger(authnLogger).
KeysFile(env().Config.Server.JWK.CertFile).
KeysURL(env().Config.Server.JWK.CertURL).
ACLFile(env().Config.Server.ACL.File).
Public("^/api/hyperfleet/?$").
Public("^/api/hyperfleet/v1/?$").
Public("^/api/hyperfleet/v1/openapi/?$").
Public("^/api/hyperfleet/v1/openapi.html/?$").
Public("^/api/hyperfleet/v1/errors(/.*)?$").
Next(mainHandler).
Build()
check(err, "Unable to create authentication handler")
mainHandler, err = auth.NewJWTHandler(context.Background(), auth.JWTHandlerConfig{
KeysFile: env().Config.Server.JWK.CertFile,
KeysURL: env().Config.Server.JWK.CertURL,
IssuerURL: env().Config.Server.JWT.IssuerURL,
Audience: env().Config.Server.JWT.Audience,
PublicPaths: []string{
"^/api/hyperfleet/?$",
"^/api/hyperfleet/v1/?$",
"^/api/hyperfleet/v1/openapi/?$",
"^/api/hyperfleet/v1/openapi.html/?$",
"^/api/hyperfleet/v1/errors(/.*)?$",
},
Next: mainHandler,
})
check(err, "Unable to create JWT authentication handler")
}

// Configure CORS for Red Hat console and API access
mainHandler = gorillahandlers.CORS(
gorillahandlers.AllowedOrigins([]string{
// OCM UI local development URLs
// Console local development URLs
"https://qa.foo.redhat.com:1337",
"https://prod.foo.redhat.com:1337",
"https://ci.foo.redhat.com:1337",
Expand Down
14 changes: 0 additions & 14 deletions configs/config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,6 @@ logging:
- refresh_token
- client_secret

# OCM (OpenShift Cluster Manager) Configuration
ocm:
base_url: https://api.integration.openshift.com # OCM API base URL
client_id: "" # OCM client ID (use env var HYPERFLEET_OCM_CLIENT_ID instead)
client_secret: "" # OCM client secret (use env var HYPERFLEET_OCM_CLIENT_SECRET instead)
self_token: "" # OCM self token (use env var HYPERFLEET_OCM_SELF_TOKEN instead)
token_url: https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token
debug: false # Enable OCM debug mode

mock:
enabled: true # Enable mock OCM clients (for testing)

# Metrics Configuration
metrics:
Expand Down Expand Up @@ -171,9 +160,6 @@ adapters:
# - HYPERFLEET_DATABASE_USERNAME_FILE=/secrets/db-username
# - HYPERFLEET_DATABASE_PASSWORD_FILE=/secrets/db-password
# - HYPERFLEET_DATABASE_NAME_FILE=/secrets/db-name
# - HYPERFLEET_OCM_CLIENT_ID_FILE=/secrets/ocm-client-id
# - HYPERFLEET_OCM_CLIENT_SECRET_FILE=/secrets/ocm-client-secret
# - HYPERFLEET_OCM_SELF_TOKEN_FILE=/secrets/ocm-self-token
#
# OpenTelemetry Tracing Configuration:
# HyperFleet uses standard OpenTelemetry environment variables for tracing.
Expand Down
Loading