Skip to content

Commit

Permalink
Augment Deployments and Daemonsets with related configs hashes
Browse files Browse the repository at this point in the history
To ensure that operands are using most recent configurations,
such as cloud configs or credentials, check for related config content
was added.

This patch introduces hash calculation for secrets and configmaps content
if they are detected in deployment/daemonset pod template spec.
  • Loading branch information
lobziik committed Feb 17, 2023
1 parent 67013b7 commit 8ec1840
Show file tree
Hide file tree
Showing 5 changed files with 633 additions and 16 deletions.
1 change: 0 additions & 1 deletion pkg/controllers/clusteroperator_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1031,7 +1031,6 @@ var _ = Describe("Apply resources should", func() {
return apierrors.IsNotFound(cl.Get(context.Background(), client.ObjectKeyFromObject(operand), operand))
}, timeout).Should(BeTrue())
}
Consistently(recorder.Events).ShouldNot(Receive())
})

})
135 changes: 135 additions & 0 deletions pkg/controllers/resourceapply/config_hash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package resourceapply

import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
)

const configHashAnnotation = "operator.openshift.io/config-hash"

type configSources struct {
ConfigMaps sets.String
Secrets sets.String
}

// collectRelatedConfigSources looks into pod template spec for secret or config map references.
// Currently, checks volumes and env vars for each container,
// returns configSources structure which contains sets of config maps and secrets names.
func collectRelatedConfigSources(spec *corev1.PodTemplateSpec) configSources {
sources := configSources{
ConfigMaps: sets.String{},
Secrets: sets.String{},
}

if spec == nil {
return sources
}

for _, volume := range spec.Spec.Volumes {
if volume.ConfigMap != nil {
sources.ConfigMaps.Insert(volume.ConfigMap.Name)
}
if volume.Secret != nil {
sources.Secrets.Insert(volume.Secret.SecretName)
}
}

for _, initContainer := range spec.Spec.InitContainers {
collectRelatedConfigsFromContainer(&initContainer, &sources)
}

for _, container := range spec.Spec.Containers {
collectRelatedConfigsFromContainer(&container, &sources)
}

return sources
}

// collectRelatedConfigsFromContainer collects related configs names into passed configSources instance.
// Looks into env and envVar of the passed container spec and populates configSources with configmaps and secrets names.
func collectRelatedConfigsFromContainer(container *corev1.Container, sources *configSources) {
for _, envVar := range container.EnvFrom {
if envVar.ConfigMapRef != nil {
sources.ConfigMaps.Insert(envVar.ConfigMapRef.Name)
}
if envVar.SecretRef != nil {
sources.Secrets.Insert(envVar.SecretRef.Name)
}
}
for _, envVar := range container.Env {
if envVar.ValueFrom == nil {
continue
}
if envVar.ValueFrom.ConfigMapKeyRef != nil {
sources.ConfigMaps.Insert(envVar.ValueFrom.ConfigMapKeyRef.Name)
}
if envVar.ValueFrom.SecretKeyRef != nil {
sources.Secrets.Insert(envVar.ValueFrom.SecretKeyRef.Name)
}
}
}

// calculateRelatedConfigsHash calculates configmaps and secrets content hash.
// Returns error in case object was not found or error during object request occured.
func calculateRelatedConfigsHash(ctx context.Context, cl runtimeclient.Client, ns string, source configSources) (string, error) {
hashSource := struct {
ConfigMaps map[string]map[string]string `json:"configMaps"`
Secrets map[string]map[string][]byte `json:"secrets"`
}{
ConfigMaps: make(map[string]map[string]string),
Secrets: make(map[string]map[string][]byte),
}

var errList []error

for _, cm := range source.ConfigMaps.UnsortedList() {
obj := &corev1.ConfigMap{}
if err := cl.Get(ctx, types.NamespacedName{Namespace: ns, Name: cm}, obj); err != nil {
errList = append(errList, err)
} else {
hashSource.ConfigMaps[cm] = obj.Data
}
}

for _, secret := range source.Secrets.UnsortedList() {
obj := &corev1.Secret{}
if err := cl.Get(ctx, types.NamespacedName{Namespace: ns, Name: secret}, obj); err != nil {
errList = append(errList, err)
} else {
hashSource.Secrets[secret] = obj.Data
}
}

if len(errList) > 0 {
return "", errors.NewAggregate(errList)
}

hashSourceBytes, err := json.Marshal(hashSource)
if err != nil {
return "", fmt.Errorf("unable to marshal dependant config content into JSON: %v", err)
}
hashBytes := sha256.Sum256(hashSourceBytes)
return fmt.Sprintf("%x", hashBytes), nil
}

// annotatePodSpecWithRelatedConfigsHash annotates pod template spec with a hash of related config maps and secrets content.
func annotatePodSpecWithRelatedConfigsHash(ctx context.Context, cl runtimeclient.Client, ns string, spec *corev1.PodTemplateSpec) error {
sources := collectRelatedConfigSources(spec)
hash, err := calculateRelatedConfigsHash(ctx, cl, ns, sources)
if err != nil {
return fmt.Errorf("error calculating configuration hash: %w", err)
}
if spec.Annotations == nil {
spec.Annotations = map[string]string{}
}
spec.Annotations[configHashAnnotation] = hash
return nil
}

0 comments on commit 8ec1840

Please sign in to comment.