mirror of
https://github.com/fluxcd/flagger.git
synced 2026-02-23 14:24:09 +00:00
375 lines
11 KiB
Go
375 lines
11 KiB
Go
package controller
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
flaggerv1 "github.com/stefanprodan/flagger/pkg/apis/flagger/v1alpha3"
|
|
clientset "github.com/stefanprodan/flagger/pkg/client/clientset/versioned"
|
|
"go.uber.org/zap"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/client-go/kubernetes"
|
|
)
|
|
|
|
// ConfigTracker is managing the operations for Kubernetes ConfigMaps and Secrets
|
|
type ConfigTracker struct {
|
|
kubeClient kubernetes.Interface
|
|
flaggerClient clientset.Interface
|
|
logger *zap.SugaredLogger
|
|
}
|
|
|
|
type ConfigRefType string
|
|
|
|
const (
|
|
ConfigRefMap ConfigRefType = "configmap"
|
|
ConfigRefSecret ConfigRefType = "secret"
|
|
)
|
|
|
|
// ConfigRef holds the reference to a tracked Kubernetes ConfigMap or Secret
|
|
type ConfigRef struct {
|
|
Name string
|
|
Type ConfigRefType
|
|
Checksum string
|
|
}
|
|
|
|
// GetName returns the config ref type and name
|
|
func (c *ConfigRef) GetName() string {
|
|
return fmt.Sprintf("%s/%s", c.Type, c.Name)
|
|
}
|
|
|
|
func checksum(data interface{}) string {
|
|
jsonBytes, _ := json.Marshal(data)
|
|
hashBytes := sha256.Sum256(jsonBytes)
|
|
|
|
return fmt.Sprintf("%x", hashBytes[:8])
|
|
}
|
|
|
|
// getRefFromConfigMap transforms a Kubernetes ConfigMap into a ConfigRef
|
|
// and computes the checksum of the ConfigMap data
|
|
func (ct *ConfigTracker) getRefFromConfigMap(name string, namespace string) (*ConfigRef, error) {
|
|
config, err := ct.kubeClient.CoreV1().ConfigMaps(namespace).Get(name, metav1.GetOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ConfigRef{
|
|
Name: config.Name,
|
|
Type: ConfigRefMap,
|
|
Checksum: checksum(config.Data),
|
|
}, nil
|
|
}
|
|
|
|
// getRefFromConfigMap transforms a Kubernetes Secret into a ConfigRef
|
|
// and computes the checksum of the Secret data
|
|
func (ct *ConfigTracker) getRefFromSecret(name string, namespace string) (*ConfigRef, error) {
|
|
secret, err := ct.kubeClient.CoreV1().Secrets(namespace).Get(name, metav1.GetOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// ignore registry secrets (those should be set via service account)
|
|
if secret.Type != corev1.SecretTypeOpaque &&
|
|
secret.Type != corev1.SecretTypeBasicAuth &&
|
|
secret.Type != corev1.SecretTypeSSHAuth &&
|
|
secret.Type != corev1.SecretTypeTLS {
|
|
ct.logger.Debugf("ignoring secret %s.%s type not supported %v", name, namespace, secret.Type)
|
|
return nil, nil
|
|
}
|
|
|
|
return &ConfigRef{
|
|
Name: secret.Name,
|
|
Type: ConfigRefSecret,
|
|
Checksum: checksum(secret.Data),
|
|
}, nil
|
|
}
|
|
|
|
// GetTargetConfigs scans the target deployment for Kubernetes ConfigMaps and Secretes
|
|
// and returns a list of config references
|
|
func (ct *ConfigTracker) GetTargetConfigs(cd *flaggerv1.Canary) (map[string]ConfigRef, error) {
|
|
res := make(map[string]ConfigRef)
|
|
targetName := cd.Spec.TargetRef.Name
|
|
targetDep, err := ct.kubeClient.AppsV1().Deployments(cd.Namespace).Get(targetName, metav1.GetOptions{})
|
|
if err != nil {
|
|
if errors.IsNotFound(err) {
|
|
return res, fmt.Errorf("deployment %s.%s not found", targetName, cd.Namespace)
|
|
}
|
|
return res, fmt.Errorf("deployment %s.%s query error %v", targetName, cd.Namespace, err)
|
|
}
|
|
|
|
// scan volumes
|
|
for _, volume := range targetDep.Spec.Template.Spec.Volumes {
|
|
if cmv := volume.ConfigMap; cmv != nil {
|
|
config, err := ct.getRefFromConfigMap(cmv.Name, cd.Namespace)
|
|
if err != nil {
|
|
ct.logger.Errorf("configMap %s.%s query error %v", cmv.Name, cd.Namespace, err)
|
|
continue
|
|
}
|
|
if config != nil {
|
|
res[config.GetName()] = *config
|
|
}
|
|
}
|
|
|
|
if sv := volume.Secret; sv != nil {
|
|
secret, err := ct.getRefFromSecret(sv.SecretName, cd.Namespace)
|
|
if err != nil {
|
|
ct.logger.Errorf("secret %s.%s query error %v", sv.SecretName, cd.Namespace, err)
|
|
continue
|
|
}
|
|
if secret != nil {
|
|
res[secret.GetName()] = *secret
|
|
}
|
|
}
|
|
}
|
|
// scan containers
|
|
for _, container := range targetDep.Spec.Template.Spec.Containers {
|
|
// scan env
|
|
for _, env := range container.Env {
|
|
if env.ValueFrom != nil {
|
|
switch {
|
|
case env.ValueFrom.ConfigMapKeyRef != nil:
|
|
name := env.ValueFrom.ConfigMapKeyRef.LocalObjectReference.Name
|
|
config, err := ct.getRefFromConfigMap(name, cd.Namespace)
|
|
if err != nil {
|
|
ct.logger.Errorf("configMap %s.%s query error %v", name, cd.Namespace, err)
|
|
continue
|
|
}
|
|
if config != nil {
|
|
res[config.GetName()] = *config
|
|
}
|
|
case env.ValueFrom.SecretKeyRef != nil:
|
|
name := env.ValueFrom.SecretKeyRef.LocalObjectReference.Name
|
|
secret, err := ct.getRefFromSecret(name, cd.Namespace)
|
|
if err != nil {
|
|
ct.logger.Errorf("secret %s.%s query error %v", name, cd.Namespace, err)
|
|
continue
|
|
}
|
|
if secret != nil {
|
|
res[secret.GetName()] = *secret
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// scan envFrom
|
|
for _, envFrom := range container.EnvFrom {
|
|
switch {
|
|
case envFrom.ConfigMapRef != nil:
|
|
name := envFrom.ConfigMapRef.LocalObjectReference.Name
|
|
config, err := ct.getRefFromConfigMap(name, cd.Namespace)
|
|
if err != nil {
|
|
ct.logger.Errorf("configMap %s.%s query error %v", name, cd.Namespace, err)
|
|
continue
|
|
}
|
|
if config != nil {
|
|
res[config.GetName()] = *config
|
|
}
|
|
case envFrom.SecretRef != nil:
|
|
name := envFrom.SecretRef.LocalObjectReference.Name
|
|
secret, err := ct.getRefFromSecret(name, cd.Namespace)
|
|
if err != nil {
|
|
ct.logger.Errorf("secret %s.%s query error %v", name, cd.Namespace, err)
|
|
continue
|
|
}
|
|
if secret != nil {
|
|
res[secret.GetName()] = *secret
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// GetConfigRefs returns a map of configs and their checksum
|
|
func (ct *ConfigTracker) GetConfigRefs(cd *flaggerv1.Canary) (*map[string]string, error) {
|
|
res := make(map[string]string)
|
|
configs, err := ct.GetTargetConfigs(cd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, cfg := range configs {
|
|
res[cfg.GetName()] = cfg.Checksum
|
|
}
|
|
|
|
return &res, nil
|
|
}
|
|
|
|
// HasConfigChanged checks for changes in ConfigMaps and Secretes by comparing
|
|
// the checksum for each ConfigRef stored in Canary.Status.TrackedConfigs
|
|
func (ct *ConfigTracker) HasConfigChanged(cd *flaggerv1.Canary) (bool, error) {
|
|
configs, err := ct.GetTargetConfigs(cd)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if len(configs) == 0 && cd.Status.TrackedConfigs == nil {
|
|
return false, nil
|
|
}
|
|
|
|
if len(configs) > 0 && cd.Status.TrackedConfigs == nil {
|
|
return true, nil
|
|
}
|
|
|
|
trackedConfigs := *cd.Status.TrackedConfigs
|
|
|
|
if len(configs) != len(trackedConfigs) {
|
|
return true, nil
|
|
}
|
|
|
|
for _, cfg := range configs {
|
|
if trackedConfigs[cfg.GetName()] != cfg.Checksum {
|
|
ct.logger.With("canary", fmt.Sprintf("%s.%s", cd.Name, cd.Namespace)).
|
|
Infof("%s %s has changed", cfg.Type, cfg.Name)
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// CreatePrimaryConfigs syncs the primary Kubernetes ConfigMaps and Secretes
|
|
// with those found in the target deployment
|
|
func (ct *ConfigTracker) CreatePrimaryConfigs(cd *flaggerv1.Canary, refs map[string]ConfigRef) error {
|
|
for _, ref := range refs {
|
|
switch ref.Type {
|
|
case ConfigRefMap:
|
|
config, err := ct.kubeClient.CoreV1().ConfigMaps(cd.Namespace).Get(ref.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
primaryName := fmt.Sprintf("%s-primary", config.GetName())
|
|
primaryConfigMap := &corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: primaryName,
|
|
Namespace: cd.Namespace,
|
|
Labels: config.Labels,
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
*metav1.NewControllerRef(cd, schema.GroupVersionKind{
|
|
Group: flaggerv1.SchemeGroupVersion.Group,
|
|
Version: flaggerv1.SchemeGroupVersion.Version,
|
|
Kind: flaggerv1.CanaryKind,
|
|
}),
|
|
},
|
|
},
|
|
Data: config.Data,
|
|
}
|
|
|
|
// update or insert primary ConfigMap
|
|
_, err = ct.kubeClient.CoreV1().ConfigMaps(cd.Namespace).Update(primaryConfigMap)
|
|
if err != nil {
|
|
if errors.IsNotFound(err) {
|
|
_, err = ct.kubeClient.CoreV1().ConfigMaps(cd.Namespace).Create(primaryConfigMap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
ct.logger.With("canary", fmt.Sprintf("%s.%s", cd.Name, cd.Namespace)).
|
|
Infof("ConfigMap %s synced", primaryConfigMap.GetName())
|
|
case ConfigRefSecret:
|
|
secret, err := ct.kubeClient.CoreV1().Secrets(cd.Namespace).Get(ref.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
primaryName := fmt.Sprintf("%s-primary", secret.GetName())
|
|
primarySecret := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: primaryName,
|
|
Namespace: cd.Namespace,
|
|
Labels: secret.Labels,
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
*metav1.NewControllerRef(cd, schema.GroupVersionKind{
|
|
Group: flaggerv1.SchemeGroupVersion.Group,
|
|
Version: flaggerv1.SchemeGroupVersion.Version,
|
|
Kind: flaggerv1.CanaryKind,
|
|
}),
|
|
},
|
|
},
|
|
Type: secret.Type,
|
|
Data: secret.Data,
|
|
}
|
|
|
|
// update or insert primary Secret
|
|
_, err = ct.kubeClient.CoreV1().Secrets(cd.Namespace).Update(primarySecret)
|
|
if err != nil {
|
|
if errors.IsNotFound(err) {
|
|
_, err = ct.kubeClient.CoreV1().Secrets(cd.Namespace).Create(primarySecret)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
ct.logger.With("canary", fmt.Sprintf("%s.%s", cd.Name, cd.Namespace)).
|
|
Infof("Secret %s synced", primarySecret.GetName())
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ApplyPrimaryConfigs appends the primary suffix to all ConfigMaps and Secretes found in the PodSpec
|
|
func (ct *ConfigTracker) ApplyPrimaryConfigs(spec corev1.PodSpec, refs map[string]ConfigRef) corev1.PodSpec {
|
|
// update volumes
|
|
for i, volume := range spec.Volumes {
|
|
if cmv := volume.ConfigMap; cmv != nil {
|
|
name := fmt.Sprintf("%s/%s", ConfigRefMap, cmv.Name)
|
|
if _, exists := refs[name]; exists {
|
|
spec.Volumes[i].ConfigMap.Name += "-primary"
|
|
}
|
|
}
|
|
|
|
if sv := volume.Secret; sv != nil {
|
|
name := fmt.Sprintf("%s/%s", ConfigRefSecret, sv.SecretName)
|
|
if _, exists := refs[name]; exists {
|
|
spec.Volumes[i].Secret.SecretName += "-primary"
|
|
}
|
|
}
|
|
}
|
|
// update containers
|
|
for _, container := range spec.Containers {
|
|
// update env
|
|
for i, env := range container.Env {
|
|
if env.ValueFrom != nil {
|
|
switch {
|
|
case env.ValueFrom.ConfigMapKeyRef != nil:
|
|
name := fmt.Sprintf("%s/%s", ConfigRefMap, env.ValueFrom.ConfigMapKeyRef.Name)
|
|
if _, exists := refs[name]; exists {
|
|
container.Env[i].ValueFrom.ConfigMapKeyRef.Name += "-primary"
|
|
}
|
|
case env.ValueFrom.SecretKeyRef != nil:
|
|
name := fmt.Sprintf("%s/%s", ConfigRefSecret, env.ValueFrom.SecretKeyRef.Name)
|
|
if _, exists := refs[name]; exists {
|
|
container.Env[i].ValueFrom.SecretKeyRef.Name += "-primary"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// update envFrom
|
|
for i, envFrom := range container.EnvFrom {
|
|
switch {
|
|
case envFrom.ConfigMapRef != nil:
|
|
name := fmt.Sprintf("%s/%s", ConfigRefMap, envFrom.ConfigMapRef.Name)
|
|
if _, exists := refs[name]; exists {
|
|
container.EnvFrom[i].ConfigMapRef.Name += "-primary"
|
|
}
|
|
case envFrom.SecretRef != nil:
|
|
name := fmt.Sprintf("%s/%s", ConfigRefSecret, envFrom.SecretRef.Name)
|
|
if _, exists := refs[name]; exists {
|
|
container.EnvFrom[i].SecretRef.Name += "-primary"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return spec
|
|
}
|