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
3 changes: 3 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ func main() {
viper.RegisterAlias("tokensmith_bootstrap_policy_scopes_hint", "tokensmith-bootstrap-policy-scopes-hint")
viper.RegisterAlias("tokensmith_scopes", "tokensmith-scopes")
viper.RegisterAlias("tokensmith_refresh_skew_sec", "tokensmith-refresh-skew-sec")
viper.RegisterAlias("enable_auth", "enable-auth")
viper.RegisterAlias("enable_legacy_api", "enable-legacy-api")
viper.RegisterAlias("enable_metrics", "enable-metrics")

// Standardized TokenSmith env vars for cross-service UX consistency.
viper.BindEnv("tokensmith_url", "TOKENSMITH_URL") //nolint:errcheck
Expand Down
1 change: 1 addition & 0 deletions cmd/server/server_extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func registerCustomServerIntegrations(r chi.Router, config Config, hsmClient *hs
providerConfig := bootscript.ProviderConfig{
Type: "hsm",
HSMConfig: &hsmIntegrationConfig,
HSMClient: hsmClient,
}

controllerLogger := log.New(os.Stdout, "bootscript: ", log.LstdFlags)
Expand Down
69 changes: 69 additions & 0 deletions pkg/clients/hsm/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ type HSMEthernetInterface struct { //nolint:revive
LastUpdate string `json:"LastUpdate,omitempty"`
}

// HSMMembership represents group membership information from HSM
type HSMMembership struct {
ID string `json:"id"`
GroupLabels []string `json:"groupLabels"`
PartitionName string `json:"partitionName"`
}

// HSMEthernetResponse represents the response from HSM ethernet interfaces endpoint
type HSMEthernetResponse struct { //nolint:revive
EthernetInterfaces []HSMEthernetInterface `json:"EthernetInterfaces"`
Expand Down Expand Up @@ -90,6 +97,7 @@ type HSMClient struct { //nolint:revive
type HSMCache struct { //nolint:revive
components map[string]*CacheEntry
ethernetInterfaces map[string]*CacheEntry
membership map[string]*CacheEntry
mu sync.RWMutex
expiry time.Duration
}
Expand All @@ -105,6 +113,7 @@ func NewHSMCache(expiry time.Duration) *HSMCache {
return &HSMCache{
components: make(map[string]*CacheEntry),
ethernetInterfaces: make(map[string]*CacheEntry),
membership: make(map[string]*CacheEntry),
expiry: expiry,
}
}
Expand Down Expand Up @@ -135,6 +144,17 @@ func (c *HSMCache) GetEthernet(key string) (interface{}, bool) {
return entry.Data, true
}

func (c *HSMCache) GetMembership(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()

entry, exists := c.membership[key]
if !exists || time.Now().After(entry.ExpiresAt) {
return nil, false
}
return entry.Data, true
}

// SetComponent stores a component in cache with expiration
func (c *HSMCache) SetComponent(key string, data interface{}) {
c.mu.Lock()
Expand All @@ -157,6 +177,16 @@ func (c *HSMCache) SetEthernet(key string, data interface{}) {
}
}

func (c *HSMCache) SetMembership(key string, data interface{}) {
c.mu.Lock()
defer c.mu.Unlock()

c.membership[key] = &CacheEntry{
Data: data,
ExpiresAt: time.Now().Add(c.expiry),
}
}

// NewHSMClient creates a new HSM client.
func NewHSMClient(config HSMConfig, logger *log.Logger) (*HSMClient, error) {
if strings.TrimSpace(config.BaseURL) == "" {
Expand Down Expand Up @@ -314,6 +344,44 @@ func (c *HSMClient) GetEthernetInterfaces(ctx context.Context) ([]HSMEthernetInt
return interfaces, nil
}

func (c *HSMClient) GetMembership(ctx context.Context, componentID string) (*HSMMembership, error) {
//check cache
cacheKey := fmt.Sprintf("membership_%s", componentID)
if data, found := c.cache.GetMembership(cacheKey); found {
c.logger.Printf("HSM membership cache hit for %s", componentID)
return data.(*HSMMembership), nil
}

url := fmt.Sprintf("%s/hsm/v2/memberships/%s", c.config.BaseURL, componentID)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create HSM membership request: %w", err)
}

if err := c.addAuthHeader(ctx, req); err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to call HSM membership endpoint: %w", err)
}
defer resp.Body.Close() //nolint:errcheck //nolint:errcheck

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HSM returned status %d", resp.StatusCode)
}
var membership HSMMembership
if err := json.NewDecoder(resp.Body).Decode(&membership); err != nil {
return nil, fmt.Errorf("failed to decode HSM response: %w", err)
}

c.cache.SetMembership(cacheKey, &membership)

c.logger.Printf("Retrieved %d group memberships from HSM for component %s", len(membership.GroupLabels), componentID)

return &membership, nil
}

// GetComponentByMAC finds a component by its MAC address
func (c *HSMClient) GetComponentByMAC(ctx context.Context, macAddress string) (*HSMComponent, error) {
// Get ethernet interfaces to find the component ID
Expand Down Expand Up @@ -372,6 +440,7 @@ func (c *HSMClient) ClearCache() {

c.cache.components = make(map[string]*CacheEntry)
c.cache.ethernetInterfaces = make(map[string]*CacheEntry)
c.cache.membership = make(map[string]*CacheEntry)

c.logger.Printf("HSM cache cleared")
}
Expand Down
60 changes: 54 additions & 6 deletions pkg/clients/hsm/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,24 @@ func NewIntegrationService(config IntegrationConfig, bootClient client.Client, l
}, nil
}

func NewIntegrationServiceWithClient(hsmClient *HSMClient, config IntegrationConfig, bootClient client.Client, logger *log.Logger) (*IntegrationService, error) {
if logger == nil {
logger = log.New(log.Writer(), "hsm-integration: ", log.LstdFlags)
}

if hsmClient == nil {
return nil, fmt.Errorf("hsm client is required")
}

return &IntegrationService{
hsmClient: hsmClient,
bootClient: bootClient,
logger: logger,
syncEnabled: config.SyncEnabled,
syncInterval: config.SyncInterval,
}, nil
}

// SyncNodesFromHSM synchronizes node data from HSM to the boot service
func (s *IntegrationService) SyncNodesFromHSM(ctx context.Context) error {
s.logger.Printf("Starting HSM node synchronization")
Expand Down Expand Up @@ -109,15 +127,23 @@ func (s *IntegrationService) SyncNodesFromHSM(ctx context.Context) error {
// Sync each compute node
var created, updated, skipped int
for _, comp := range computeNodes {
err := s.syncNode(ctx, comp, macMap, existingMap)
membership, err := s.hsmClient.GetMembership(ctx, comp.ID)
if err != nil {
s.logger.Printf("Warning: failed to get membership for %s: %v", comp.ID, err)
}
groups := []string{}
if membership != nil {
groups = membership.GroupLabels
}
err = s.syncNode(ctx, comp, macMap, groups, existingMap)
if err != nil {
s.logger.Printf("Warning: Failed to sync node %s: %v", comp.ID, err)
continue
}

// Track what we did
if existing, exists := existingMap[comp.ID]; exists {
if s.needsUpdate(comp, macMap, existing) {
if s.needsUpdate(comp, macMap, groups, existing) {
updated++
} else {
skipped++
Expand All @@ -132,7 +158,7 @@ func (s *IntegrationService) SyncNodesFromHSM(ctx context.Context) error {
}

// syncNode synchronizes a single node from HSM
func (s *IntegrationService) syncNode(ctx context.Context, comp HSMComponent, macMap map[string]string, existingMap map[string]*v1.Node) error {
func (s *IntegrationService) syncNode(ctx context.Context, comp HSMComponent, macMap map[string]string, groups []string, existingMap map[string]*v1.Node) error {
// Check if node already exists
existing, exists := existingMap[comp.ID]

Expand All @@ -146,12 +172,12 @@ func (s *IntegrationService) syncNode(ctx context.Context, comp HSMComponent, ma
BootMAC: bootMAC,
Role: comp.Role,
SubRole: comp.SubRole,
Groups: []string{}, // Will be populated from inventory service later
Groups: groups,
}

if exists {
// Update existing node if needed
if s.needsUpdate(comp, macMap, existing) {
if s.needsUpdate(comp, macMap, groups, existing) {
updateReq := client.UpdateNodeRequest{
Spec: nodeSpec,
}
Expand Down Expand Up @@ -182,7 +208,7 @@ func (s *IntegrationService) syncNode(ctx context.Context, comp HSMComponent, ma
}

// needsUpdate checks if a node needs to be updated based on HSM data
func (s *IntegrationService) needsUpdate(comp HSMComponent, macMap map[string]string, existing *v1.Node) bool {
func (s *IntegrationService) needsUpdate(comp HSMComponent, macMap map[string]string, groups []string, existing *v1.Node) bool {
// Check if NID changed
if comp.NID != existing.Spec.NID {
return true
Expand All @@ -198,6 +224,11 @@ func (s *IntegrationService) needsUpdate(comp HSMComponent, macMap map[string]st
return true
}

// Check if membership has changed
if !stringSlicesEqual(groups, existing.Spec.Groups) {
return true
}

// Check if MAC address changed
bootMAC := macMap[comp.ID]
return bootMAC != existing.Spec.BootMAC
Expand Down Expand Up @@ -382,3 +413,20 @@ func (s *IntegrationService) GetStats(ctx context.Context) map[string]interface{

return stats
}

func stringSlicesEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
seen := make(map[string]int, len(a))
for _, v := range a {
seen[v]++
}
for _, v := range b {
seen[v]--
if seen[v] < 0 {
return false
}
}
return true
}
18 changes: 14 additions & 4 deletions pkg/controllers/bootscript/flexible_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ type ProviderConfig struct {
Type string `yaml:"type"` // "hsm" or "yaml"
HSMConfig *hsm.IntegrationConfig `yaml:"hsm_config,omitempty"`
YAMLConfig *local.IntegrationConfig `yaml:"yaml_config,omitempty"`

HSMClient *hsm.HSMClient `yaml:"-"`
}

// NewFlexibleBootScriptController creates a controller with the specified provider
Expand All @@ -62,10 +64,18 @@ func NewFlexibleBootScriptController(bootClient client.Client, config ProviderCo
defaultConfig := hsm.DefaultIntegrationConfig()
config.HSMConfig = &defaultConfig
}

hsmIntegration, err := hsm.NewIntegrationService(*config.HSMConfig, bootClient, logger)
if err != nil {
return nil, err
var hsmIntegration *hsm.IntegrationService
var err error
if config.HSMClient == nil {
hsmIntegration, err = hsm.NewIntegrationService(*config.HSMConfig, bootClient, logger)
if err != nil {
return nil, err
}
} else {
hsmIntegration, err = hsm.NewIntegrationServiceWithClient(config.HSMClient, *config.HSMConfig, bootClient, logger)
if err != nil {
return nil, err
}
}
controller.nodeProvider = hsmIntegration
controller.syncProvider = hsmIntegration
Expand Down
Loading