diff --git a/staging/src/k8s.io/apiserver/pkg/util/configmetrics/info_collector.go b/staging/src/k8s.io/apiserver/pkg/util/configmetrics/info_collector.go new file mode 100644 index 00000000000..58e7a1b8a10 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/util/configmetrics/info_collector.go @@ -0,0 +1,92 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package configmetrics + +import ( + "slices" + "sync/atomic" + + "k8s.io/component-base/metrics" + "k8s.io/utils/ptr" +) + +// HashProvider is an interface for getting the current config hash values +type HashProvider interface { + GetCurrentHashes() []string + SetHashes(hashes ...string) +} + +// AtomicHashProvider implements HashProvider using a single atomic pointer to a slice +type AtomicHashProvider struct { + hashes *atomic.Pointer[[]string] +} + +// NewAtomicHashProvider creates a new atomic hash provider +func NewAtomicHashProvider() *AtomicHashProvider { + p := &AtomicHashProvider{ + hashes: &atomic.Pointer[[]string]{}, + } + // Initialize with empty slice + p.hashes.Store(ptr.To([]string{})) + return p +} + +func (h *AtomicHashProvider) GetCurrentHashes() []string { + hashesPtr := h.hashes.Load() + if hashesPtr == nil { + // should never happen, but just in case + return []string{} + } + // Return a copy to prevent external modification + hashes := *hashesPtr + return slices.Clone(hashes) +} + +func (h *AtomicHashProvider) SetHashes(hashes ...string) { + hashCopy := slices.Clone(hashes) + h.hashes.Store(&hashCopy) +} + +// NewConfigInfoCustomCollector creates a custom collector for config hash info metrics. +// This eliminates the need for state management and locks by collecting metrics on demand. +func NewConfigInfoCustomCollector(desc *metrics.Desc, hashProvider HashProvider) metrics.StableCollector { + return &configInfoCustomCollector{ + desc: desc, + hashProvider: hashProvider, + } +} + +type configInfoCustomCollector struct { + metrics.BaseStableCollector + desc *metrics.Desc + hashProvider HashProvider +} + +var _ metrics.StableCollector = &configInfoCustomCollector{} + +func (c *configInfoCustomCollector) DescribeWithStability(ch chan<- *metrics.Desc) { + ch <- c.desc +} + +func (c *configInfoCustomCollector) CollectWithStability(ch chan<- metrics.Metric) { + hashes := c.hashProvider.GetCurrentHashes() + if len(hashes) == 0 { + return + } + + ch <- metrics.NewLazyConstMetric(c.desc, metrics.GaugeValue, 1, hashes...) +} diff --git a/staging/src/k8s.io/apiserver/pkg/util/configmetrics/info_collector_test.go b/staging/src/k8s.io/apiserver/pkg/util/configmetrics/info_collector_test.go new file mode 100644 index 00000000000..87900e4290e --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/util/configmetrics/info_collector_test.go @@ -0,0 +1,62 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package configmetrics + +import ( + "testing" +) + +// assertHashesEqual is a test helper that compares expected and actual hash slices +func assertHashesEqual(t *testing.T, expected, actual []string) { + t.Helper() + if len(actual) != len(expected) { + t.Errorf("expected %d hashes, got %d", len(expected), len(actual)) + return + } + for i, hash := range actual { + if hash != expected[i] { + t.Errorf("expected hash %q at index %d, got %q", expected[i], i, hash) + } + } +} + +func TestAtomicHashProvider(t *testing.T) { + provider := NewAtomicHashProvider() + + apiServerIDHash := "sha256:abc123" + configHash := "sha256:hash1" + + // Test initial state + hashes := provider.GetCurrentHashes() + assertHashesEqual(t, []string{}, hashes) + + // Test setting hashes + provider.SetHashes(apiServerIDHash, configHash) + hashes = provider.GetCurrentHashes() + assertHashesEqual(t, []string{apiServerIDHash, configHash}, hashes) + + // Test updating hashes + newConfigHash := "sha256:hash2" + provider.SetHashes(apiServerIDHash, newConfigHash) + hashes = provider.GetCurrentHashes() + assertHashesEqual(t, []string{apiServerIDHash, newConfigHash}, hashes) + + // Test empty hashes + provider.SetHashes() + hashes = provider.GetCurrentHashes() + assertHashesEqual(t, []string{}, hashes) +}