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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions charts/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ env: []
# - name: GLOG_V
# value: "10"

# Adapter requirements (JSON array format)
# - name: HYPERFLEET_CLUSTER_ADAPTERS
# value: '["validation","dns","pullsecret","hypershift"]'
# - name: HYPERFLEET_NODEPOOL_ADAPTERS
# value: '["validation","hypershift"]'

# Volume mounts for additional configs
extraVolumeMounts: []

Expand Down
17 changes: 17 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ export OPENAPI_SCHEMA_PATH=/path/to/custom-schema.yaml
- `LOG_LEVEL` - Logging level: `debug`, `info`, `warn`, `error` (default: `info`)
- `LOG_FORMAT` - Log format: `json`, `text` (default: `json`)

**Adapter Requirements:**
- `HYPERFLEET_CLUSTER_ADAPTERS` - Required adapters for cluster "Ready" state, JSON array (default: `["validation","dns","pullsecret","hypershift"]`)
- `HYPERFLEET_NODEPOOL_ADAPTERS` - Required adapters for nodepool "Ready" state, JSON array (default: `["validation","hypershift"]`)

Example:
```bash
export HYPERFLEET_CLUSTER_ADAPTERS='["validation","dns"]'
export HYPERFLEET_NODEPOOL_ADAPTERS='["validation"]'
```

## Kubernetes Deployment

### Using Helm Chart
Expand Down Expand Up @@ -202,6 +212,13 @@ database:
enabled: true
secretName: hyperfleet-db-external

# Optional: customize adapter requirements
env:
- name: HYPERFLEET_CLUSTER_ADAPTERS
value: '["validation","dns"]'
- name: HYPERFLEET_NODEPOOL_ADAPTERS
value: '["validation"]'

replicaCount: 3

resources:
Expand Down
66 changes: 66 additions & 0 deletions pkg/config/adapter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package config

import (
"encoding/json"
"log"
"os"
)

// AdapterRequirementsConfig configures which adapters must be ready for resources
type AdapterRequirementsConfig struct {
RequiredClusterAdapters []string
RequiredNodePoolAdapters []string
}

// NewAdapterRequirementsConfig creates config from environment variables or defaults
func NewAdapterRequirementsConfig() *AdapterRequirementsConfig {
config := &AdapterRequirementsConfig{
RequiredClusterAdapters: getDefaultClusterAdapters(),
RequiredNodePoolAdapters: getDefaultNodePoolAdapters(),
}

config.LoadFromEnv()
return config
}

// LoadFromEnv loads adapter lists from HYPERFLEET_CLUSTER_ADAPTERS and
// HYPERFLEET_NODEPOOL_ADAPTERS (JSON array format: '["adapter1","adapter2"]')
func (c *AdapterRequirementsConfig) LoadFromEnv() {
if clusterAdaptersStr := os.Getenv("HYPERFLEET_CLUSTER_ADAPTERS"); clusterAdaptersStr != "" {
var adapters []string
if err := json.Unmarshal([]byte(clusterAdaptersStr), &adapters); err == nil {
c.RequiredClusterAdapters = adapters
log.Printf("Loaded HYPERFLEET_CLUSTER_ADAPTERS from env: %v", adapters)
} else {
log.Printf("WARNING: Failed to parse HYPERFLEET_CLUSTER_ADAPTERS, using defaults: %v", err)
}
}

if nodepoolAdaptersStr := os.Getenv("HYPERFLEET_NODEPOOL_ADAPTERS"); nodepoolAdaptersStr != "" {
var adapters []string
if err := json.Unmarshal([]byte(nodepoolAdaptersStr), &adapters); err == nil {
c.RequiredNodePoolAdapters = adapters
log.Printf("Loaded HYPERFLEET_NODEPOOL_ADAPTERS from env: %v", adapters)
} else {
log.Printf("WARNING: Failed to parse HYPERFLEET_NODEPOOL_ADAPTERS, using defaults: %v", err)
}
}
}

// Default cluster adapters: validation, dns, pullsecret, hypershift
func getDefaultClusterAdapters() []string {
return []string{
"validation",
"dns",
"pullsecret",
"hypershift",
}
}

// Default nodepool adapters: validation, hypershift
func getDefaultNodePoolAdapters() []string {
return []string{
"validation",
"hypershift",
}
}
161 changes: 161 additions & 0 deletions pkg/config/adapter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package config

import (
"testing"

. "github.com/onsi/gomega"
)

func TestNewAdapterRequirementsConfig_Defaults(t *testing.T) {
RegisterTestingT(t)

// Ensure env vars are not set
t.Setenv("HYPERFLEET_CLUSTER_ADAPTERS", "")
t.Setenv("HYPERFLEET_NODEPOOL_ADAPTERS", "")

config := NewAdapterRequirementsConfig()

// Verify default cluster adapters
Expect(config.RequiredClusterAdapters).To(Equal([]string{
"validation",
"dns",
"pullsecret",
"hypershift",
}))

// Verify default nodepool adapters
Expect(config.RequiredNodePoolAdapters).To(Equal([]string{
"validation",
"hypershift",
}))
}

func TestLoadFromEnv_ClusterAdapters(t *testing.T) {
RegisterTestingT(t)

t.Setenv("HYPERFLEET_CLUSTER_ADAPTERS", `["validation","dns"]`)
t.Setenv("HYPERFLEET_NODEPOOL_ADAPTERS", "")

config := NewAdapterRequirementsConfig()

// Verify cluster adapters from env
Expect(config.RequiredClusterAdapters).To(Equal([]string{
"validation",
"dns",
}))

// Verify nodepool adapters use defaults
Expect(config.RequiredNodePoolAdapters).To(Equal([]string{
"validation",
"hypershift",
}))
}

func TestLoadFromEnv_NodePoolAdapters(t *testing.T) {
RegisterTestingT(t)

t.Setenv("HYPERFLEET_NODEPOOL_ADAPTERS", `["validation"]`)
t.Setenv("HYPERFLEET_CLUSTER_ADAPTERS", "")

config := NewAdapterRequirementsConfig()

// Verify cluster adapters use defaults
Expect(config.RequiredClusterAdapters).To(Equal([]string{
"validation",
"dns",
"pullsecret",
"hypershift",
}))

// Verify nodepool adapters from env
Expect(config.RequiredNodePoolAdapters).To(Equal([]string{
"validation",
}))
}

func TestLoadFromEnv_BothAdapters(t *testing.T) {
RegisterTestingT(t)

t.Setenv("HYPERFLEET_CLUSTER_ADAPTERS", `["validation","dns"]`)
t.Setenv("HYPERFLEET_NODEPOOL_ADAPTERS", `["validation"]`)

config := NewAdapterRequirementsConfig()

// Verify both are loaded from env
Expect(config.RequiredClusterAdapters).To(Equal([]string{
"validation",
"dns",
}))
Expect(config.RequiredNodePoolAdapters).To(Equal([]string{
"validation",
}))
}

func TestLoadFromEnv_EmptyArray(t *testing.T) {
RegisterTestingT(t)

t.Setenv("HYPERFLEET_CLUSTER_ADAPTERS", `[]`)

config := NewAdapterRequirementsConfig()

// Verify empty array is loaded
Expect(config.RequiredClusterAdapters).To(Equal([]string{}))
}

func TestLoadFromEnv_InvalidJSON_ClusterAdapters(t *testing.T) {
RegisterTestingT(t)

t.Setenv("HYPERFLEET_CLUSTER_ADAPTERS", `not-valid-json`)

config := NewAdapterRequirementsConfig()

// Verify defaults are used when JSON is invalid
Expect(config.RequiredClusterAdapters).To(Equal([]string{
"validation",
"dns",
"pullsecret",
"hypershift",
}))
}

func TestLoadFromEnv_InvalidJSON_NodePoolAdapters(t *testing.T) {
RegisterTestingT(t)

t.Setenv("HYPERFLEET_NODEPOOL_ADAPTERS", `{invalid}`)

config := NewAdapterRequirementsConfig()

// Verify defaults are used when JSON is invalid
Expect(config.RequiredNodePoolAdapters).To(Equal([]string{
"validation",
"hypershift",
}))
}

func TestLoadFromEnv_SingleAdapter(t *testing.T) {
RegisterTestingT(t)

t.Setenv("HYPERFLEET_CLUSTER_ADAPTERS", `["validation"]`)

config := NewAdapterRequirementsConfig()

// Verify single adapter is loaded
Expect(config.RequiredClusterAdapters).To(Equal([]string{
"validation",
}))
}

func TestLoadFromEnv_CustomAdapterNames(t *testing.T) {
RegisterTestingT(t)

t.Setenv("HYPERFLEET_CLUSTER_ADAPTERS", `["custom-adapter-1","custom-adapter-2","test"]`)

config := NewAdapterRequirementsConfig()

// Verify custom adapters are loaded
Expect(config.RequiredClusterAdapters).To(Equal([]string{
"custom-adapter-1",
"custom-adapter-2",
"test",
}))
}
9 changes: 6 additions & 3 deletions pkg/services/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"

"github.com/openshift-hyperfleet/hyperfleet-api/pkg/api"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/config"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/dao"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/errors"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/logger"
Expand All @@ -31,10 +32,11 @@ type ClusterService interface {
OnDelete(ctx context.Context, id string) error
}

func NewClusterService(clusterDao dao.ClusterDao, adapterStatusDao dao.AdapterStatusDao) ClusterService {
func NewClusterService(clusterDao dao.ClusterDao, adapterStatusDao dao.AdapterStatusDao, adapterConfig *config.AdapterRequirementsConfig) ClusterService {
return &sqlClusterService{
clusterDao: clusterDao,
adapterStatusDao: adapterStatusDao,
adapterConfig: adapterConfig,
}
}

Expand All @@ -43,6 +45,7 @@ var _ ClusterService = &sqlClusterService{}
type sqlClusterService struct {
clusterDao dao.ClusterDao
adapterStatusDao dao.AdapterStatusDao
adapterConfig *config.AdapterRequirementsConfig
}

func (s *sqlClusterService) Get(ctx context.Context, id string) (*api.Cluster, *errors.ServiceError) {
Expand Down Expand Up @@ -184,8 +187,8 @@ func (s *sqlClusterService) UpdateClusterStatusFromAdapters(ctx context.Context,
}
}

// Compute overall phase using required adapters
newPhase := ComputePhase(ctx, adapterStatuses, requiredClusterAdapters, cluster.Generation)
// Compute overall phase using required adapters from config
newPhase := ComputePhase(ctx, adapterStatuses, s.adapterConfig.RequiredClusterAdapters, cluster.Generation)

// Calculate min(adapters[].last_report_time) for cluster.status.last_updated_time
// This uses the OLDEST adapter timestamp to ensure Sentinel can detect stale adapters
Expand Down
9 changes: 6 additions & 3 deletions pkg/services/node_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"

"github.com/openshift-hyperfleet/hyperfleet-api/pkg/api"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/config"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/dao"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/errors"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/logger"
Expand All @@ -31,10 +32,11 @@ type NodePoolService interface {
OnDelete(ctx context.Context, id string) error
}

func NewNodePoolService(nodePoolDao dao.NodePoolDao, adapterStatusDao dao.AdapterStatusDao) NodePoolService {
func NewNodePoolService(nodePoolDao dao.NodePoolDao, adapterStatusDao dao.AdapterStatusDao, adapterConfig *config.AdapterRequirementsConfig) NodePoolService {
return &sqlNodePoolService{
nodePoolDao: nodePoolDao,
adapterStatusDao: adapterStatusDao,
adapterConfig: adapterConfig,
}
}

Expand All @@ -43,6 +45,7 @@ var _ NodePoolService = &sqlNodePoolService{}
type sqlNodePoolService struct {
nodePoolDao dao.NodePoolDao
adapterStatusDao dao.AdapterStatusDao
adapterConfig *config.AdapterRequirementsConfig
}

func (s *sqlNodePoolService) Get(ctx context.Context, id string) (*api.NodePool, *errors.ServiceError) {
Expand Down Expand Up @@ -182,8 +185,8 @@ func (s *sqlNodePoolService) UpdateNodePoolStatusFromAdapters(ctx context.Contex
}
}

// Compute overall phase using required adapters
newPhase := ComputePhase(ctx, adapterStatuses, requiredNodePoolAdapters, nodePool.Generation)
// Compute overall phase using required adapters from config
newPhase := ComputePhase(ctx, adapterStatuses, s.adapterConfig.RequiredNodePoolAdapters, nodePool.Generation)

// Calculate min(adapters[].last_report_time) for nodepool.status.last_updated_time
// This uses the OLDEST adapter timestamp to ensure Sentinel can detect stale adapters
Expand Down
27 changes: 1 addition & 26 deletions pkg/services/status_aggregation.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,7 @@ import (
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/logger"
)

// requiredClusterAdapters defines the list of adapters that must be ready for a cluster to be "Ready"
// All of these adapters must have:
// 1. available === "True"
// 2. observed_generation === cluster.generation
//
// Based on HyperFleet MVP scope (GCP cluster adapters):
// - validation: Check GCP prerequisites
// - dns: Create Cloud DNS records
// - pullsecret: Store credentials in Secret Manager
// - hypershift: Create HostedCluster CR
// Note: placement is NOT required (handled separately)
var requiredClusterAdapters = []string{
"validation",
"dns",
"pullsecret",
"hypershift",
}

// requiredNodePoolAdapters defines the list of adapters that must be ready for a nodepool to be "Ready"
// Based on HyperFleet MVP scope (GCP nodepool adapters):
// - validation: Check nodepool prerequisites
// - hypershift: Create NodePool CR
var requiredNodePoolAdapters = []string{
"validation",
"hypershift",
}
// Required adapter lists configured via pkg/config/adapter.go (see AdapterRequirementsConfig)

// adapterConditionSuffixMap allows overriding the default suffix for specific adapters
// Currently empty - all adapters use "Successful" by default
Expand Down
Loading