From 4fc03410315998ee38689ff69ea056f3595b53b6 Mon Sep 17 00:00:00 2001 From: Travis Cotton Date: Tue, 5 May 2026 08:39:19 -0600 Subject: [PATCH 1/4] adding group membership info HSM lookups and caching Signed-off-by: Travis Cotton --- pkg/clients/hsm/client.go | 69 ++++++++++++++++++++++++++++++++++ pkg/clients/hsm/integration.go | 42 ++++++++++++++++++--- 2 files changed, 105 insertions(+), 6 deletions(-) diff --git a/pkg/clients/hsm/client.go b/pkg/clients/hsm/client.go index 90318e1..2c0067f 100644 --- a/pkg/clients/hsm/client.go +++ b/pkg/clients/hsm/client.go @@ -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"` @@ -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 } @@ -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, } } @@ -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() @@ -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) == "" { @@ -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() + + 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 @@ -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") } diff --git a/pkg/clients/hsm/integration.go b/pkg/clients/hsm/integration.go index 3af1eca..faf11cf 100644 --- a/pkg/clients/hsm/integration.go +++ b/pkg/clients/hsm/integration.go @@ -109,7 +109,15 @@ 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 @@ -117,7 +125,7 @@ func (s *IntegrationService) SyncNodesFromHSM(ctx context.Context) error { // 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++ @@ -132,7 +140,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] @@ -146,12 +154,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, } @@ -182,7 +190,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 @@ -198,6 +206,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 @@ -382,3 +395,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 +} From 1a04fd8cc4ea4d5fcad764d40b40e1802cad616f Mon Sep 17 00:00:00 2001 From: Travis Cotton Date: Tue, 5 May 2026 09:42:37 -0600 Subject: [PATCH 2/4] add missing aliases Signed-off-by: Travis Cotton --- cmd/server/main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/server/main.go b/cmd/server/main.go index cf34241..f7a6119 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -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 From fbc8dd70e738601fbb8c89b5638aa6e0c266ef7f Mon Sep 17 00:00:00 2001 From: Travis Cotton Date: Tue, 5 May 2026 11:40:21 -0600 Subject: [PATCH 3/4] attempting to fix ignored hsmClient Signed-off-by: Travis Cotton --- cmd/server/server_extensions.go | 1 + pkg/clients/hsm/integration.go | 18 ++++++++++++++++++ .../bootscript/flexible_controller.go | 18 ++++++++++++++---- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/cmd/server/server_extensions.go b/cmd/server/server_extensions.go index 9c65512..6a2ae9d 100644 --- a/cmd/server/server_extensions.go +++ b/cmd/server/server_extensions.go @@ -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) diff --git a/pkg/clients/hsm/integration.go b/pkg/clients/hsm/integration.go index faf11cf..75a8ff3 100644 --- a/pkg/clients/hsm/integration.go +++ b/pkg/clients/hsm/integration.go @@ -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") diff --git a/pkg/controllers/bootscript/flexible_controller.go b/pkg/controllers/bootscript/flexible_controller.go index 6837518..622f234 100644 --- a/pkg/controllers/bootscript/flexible_controller.go +++ b/pkg/controllers/bootscript/flexible_controller.go @@ -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 @@ -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 From ee368240c64dfb711f5f5d24a99e39f9fd2ee403 Mon Sep 17 00:00:00 2001 From: Travis Cotton Date: Tue, 5 May 2026 11:43:31 -0600 Subject: [PATCH 4/4] don't check linting on defer resp.Body.Close() Signed-off-by: Travis Cotton --- pkg/clients/hsm/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/clients/hsm/client.go b/pkg/clients/hsm/client.go index 2c0067f..8d05dc3 100644 --- a/pkg/clients/hsm/client.go +++ b/pkg/clients/hsm/client.go @@ -365,7 +365,7 @@ func (c *HSMClient) GetMembership(ctx context.Context, componentID string) (*HSM if err != nil { return nil, fmt.Errorf("failed to call HSM membership endpoint: %w", err) } - defer resp.Body.Close() + defer resp.Body.Close() //nolint:errcheck //nolint:errcheck if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("HSM returned status %d", resp.StatusCode)