pkg/secrets/manager.go (184 lines of code) (raw):
// Copyright 2024 Google LLC
//
// 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
//
// https://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 secrets
import (
"bytes"
"context"
"errors"
"fmt"
"sync"
"github.com/prometheus/client_golang/prometheus"
"gopkg.in/yaml.v2"
)
var (
failedSecretConfigs = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "prometheus_kubernetes_failed_secret_configs",
Help: "Current number of secret configurations that failed to load.",
},
)
secretsTotal = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "prometheus_kubernetes_secrets_configured",
Help: "Current number of secrets.",
},
)
)
func yamlSerialize(obj any) ([]byte, error) {
if obj == nil {
return []byte{}, nil
}
var buf bytes.Buffer
encoder := yaml.NewEncoder(&buf)
if err := encoder.Encode(obj); err != nil {
return nil, err
}
if err := encoder.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func yamlEqual(x, y any) (bool, error) {
yamlX, err := yamlSerialize(x)
if err != nil {
return false, err
}
yamlY, err := yamlSerialize(y)
if err != nil {
return false, err
}
return bytes.Equal(yamlX, yamlY), nil
}
// SecretConfig maps a secret name references to a Kubernetes secret.
type SecretConfig struct {
Name string `yaml:"name"`
Config KubernetesSecretConfig `yaml:"config"`
}
type secretEntry struct {
config KubernetesSecretConfig
secret Secret
}
// Manager manages the Kubernetes secret provider.
type Manager struct {
ctx context.Context
opts ProviderOptions
mtx sync.Mutex
cancelFn func()
provider *watchProvider
config *WatchSPConfig
secrets map[string]*secretEntry
}
// NewManager creates a new secret manager with the provided options.
func NewManager(ctx context.Context, reg prometheus.Registerer, opts ProviderOptions) Manager {
if reg != nil {
reg.MustRegister(failedSecretConfigs)
reg.MustRegister(secretsTotal)
}
// Note, we do not create the Kubernetes client until we have secrets.
return Manager{
ctx: ctx,
cancelFn: func() {},
secrets: make(map[string]*secretEntry),
opts: opts,
}
}
// ApplyConfig applies the new secrets, diffing each one with the last configuration to apply the
// relevant update.
func (m *Manager) ApplyConfig(providerConfig *WatchSPConfig, configs []SecretConfig) error {
m.mtx.Lock()
defer m.mtx.Unlock()
// If no secrets are provided, cancel any existing secret provider.
if len(configs) == 0 {
m.cancelFn()
m.provider = nil
m.cancelFn = func() {}
m.secrets = map[string]*secretEntry{}
m.config = nil
return nil
}
// We must recreate the Kubernetes client and reconnect all secrets if the client configuration
// changes. Hypothetically this could be a different API server (or just different parameters).
eq, err := yamlEqual(m.config, providerConfig)
if err != nil {
return err
}
defer func() {
m.config = providerConfig
}()
// We may have an empty Kubernetes configuration (indicating default parameters). Since we don't
// have a client until we have secrets, we must create one now, or recreate it if the
// configuration changed.
if !eq || m.provider == nil {
ctx, cancel := context.WithCancel(m.ctx)
provider, err := providerConfig.newProvider(ctx, m.opts)
if err != nil {
cancel()
return err
}
m.cancelFn()
m.provider = provider
m.cancelFn = cancel
m.secrets = map[string]*secretEntry{}
}
return m.updateSecrets(configs)
}
func (m *Manager) updateSecrets(configs []SecretConfig) error {
var errs []error
// Do a first pass to check for errors and disable those secrets.
secretNamesEnabled := make(map[string]bool)
for _, secret := range configs {
if enabled, ok := secretNamesEnabled[secret.Name]; ok {
if !enabled {
continue
}
errs = append(errs, fmt.Errorf("duplicate secret key %q", secret.Name))
secretNamesEnabled[secret.Name] = false
} else {
secretNamesEnabled[secret.Name] = true
}
}
secretsFinal := map[string]*secretEntry{}
for i := range configs {
secretIncoming := &configs[i]
if enabled := secretNamesEnabled[secretIncoming.Name]; !enabled {
continue
}
// First check if we've registered this secret before.
if secretPrevious, ok := m.secrets[secretIncoming.Name]; ok {
// Track all the secrets we saw. The leftover are later removed.
delete(m.secrets, secretIncoming.Name)
// If the config didn't change, we skip this one.
eq, err := yamlEqual(&secretPrevious.config, &secretIncoming.Config)
if err != nil {
errs = append(errs, err)
continue
}
if eq {
secretsFinal[secretIncoming.Name] = secretPrevious
continue
}
// The config changed, so update it.
s, err := m.provider.Update(&secretPrevious.config, &secretIncoming.Config)
if err != nil {
errs = append(errs, err)
continue
}
secretPrevious.secret = s
secretsFinal[secretIncoming.Name] = secretPrevious
} else {
// We've never seen this secret before, so add it.
s, err := m.provider.Add(&secretIncoming.Config)
if err != nil {
errs = append(errs, err)
continue
}
secretsFinal[secretIncoming.Name] = &secretEntry{
config: secretIncoming.Config,
secret: s,
}
}
}
for _, secretUnused := range m.secrets {
m.provider.Remove(&secretUnused.config)
}
m.secrets = secretsFinal
total := len(secretNamesEnabled)
success := len(m.secrets)
failedSecretConfigs.Set(float64(total - success))
secretsTotal.Set(float64(total))
return errors.Join(errs...)
}
// Fetch implements github.com/prometheus/common/config.SecretManager.Fetch.
func (m *Manager) Fetch(ctx context.Context, name string) (string, error) {
secret, ok := m.secrets[name]
if !ok {
return "", fmt.Errorf("secret %q not found", name)
}
return secret.secret.Fetch(ctx)
}
// Close cancels the manager, stopping the Kubernetes secret provider.
func (m *Manager) Close(reg prometheus.Registerer) {
m.cancelFn()
if reg != nil {
reg.Unregister(failedSecretConfigs)
reg.Unregister(secretsTotal)
}
}