pkg/instrumentation/auto/annotation.go (192 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package auto
import (
"context"
"fmt"
"strings"
"github.com/go-logr/logr"
"golang.org/x/exp/maps"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/aws/amazon-cloudwatch-agent-operator/pkg/instrumentation"
)
const (
autoAnnotatePrefix = "cloudwatch.aws.amazon.com/auto-annotate-"
defaultAnnotationValue = "true"
)
// +kubebuilder:rbac:groups="",resources=namespaces,verbs=list;patch
// +kubebuilder:rbac:groups="apps",resources=daemonsets;deployments;statefulsets,verbs=list;patch
// AnnotationMutators contains functions that can be used to mutate annotations
// on all supported objects based on the configured mutators.
type AnnotationMutators struct {
clientWriter client.Writer
clientReader client.Reader
logger logr.Logger
namespaceMutators map[string]instrumentation.AnnotationMutator
deploymentMutators map[string]instrumentation.AnnotationMutator
daemonSetMutators map[string]instrumentation.AnnotationMutator
statefulSetMutators map[string]instrumentation.AnnotationMutator
defaultMutator instrumentation.AnnotationMutator
injectAnnotations map[string]struct{}
}
// RestartNamespace sets the restartedAtAnnotation for each of the namespace's supported resources and patches them.
func (m *AnnotationMutators) RestartNamespace(ctx context.Context, namespace *corev1.Namespace, mutatedAnnotations map[string]string) {
m.rangeObjectList(ctx, &appsv1.DeploymentList{}, client.InNamespace(namespace.Name),
chainCallbacks(m.shouldRestartFunc(mutatedAnnotations), m.patchFunc(ctx, setRestartAnnotation)))
m.rangeObjectList(ctx, &appsv1.DaemonSetList{}, client.InNamespace(namespace.Name),
chainCallbacks(m.shouldRestartFunc(mutatedAnnotations), m.patchFunc(ctx, setRestartAnnotation)))
m.rangeObjectList(ctx, &appsv1.StatefulSetList{}, client.InNamespace(namespace.Name),
chainCallbacks(m.shouldRestartFunc(mutatedAnnotations), m.patchFunc(ctx, setRestartAnnotation)))
}
// MutateAndPatchAll runs the mutators for each of the supported resources and patches them.
func (m *AnnotationMutators) MutateAndPatchAll(ctx context.Context) {
m.rangeObjectList(ctx, &appsv1.DeploymentList{}, &client.ListOptions{}, m.patchFunc(ctx, m.mutateObject))
m.rangeObjectList(ctx, &appsv1.DaemonSetList{}, &client.ListOptions{}, m.patchFunc(ctx, m.mutateObject))
m.rangeObjectList(ctx, &appsv1.StatefulSetList{}, &client.ListOptions{}, m.patchFunc(ctx, m.mutateObject))
m.rangeObjectList(ctx, &corev1.NamespaceList{}, &client.ListOptions{},
chainCallbacks(m.patchFunc(ctx, m.mutateObject), m.restartNamespaceFunc(ctx)),
)
}
// MutateObject modifies annotations for a single object using the configured mutators.
func (m *AnnotationMutators) MutateObject(obj client.Object) (any, bool) {
return m.mutateObject(obj, nil)
}
// mutateObject modifies annotations for a single object using the configured mutators.
func (m *AnnotationMutators) mutateObject(obj client.Object, _ any) (any, bool) {
switch o := obj.(type) {
case *corev1.Namespace:
return m.mutate(o.GetName(), m.namespaceMutators, o.GetObjectMeta())
case *appsv1.Deployment:
return m.mutate(namespacedName(o.GetObjectMeta()), m.deploymentMutators, o.Spec.Template.GetObjectMeta())
case *appsv1.DaemonSet:
return m.mutate(namespacedName(o.GetObjectMeta()), m.daemonSetMutators, o.Spec.Template.GetObjectMeta())
case *appsv1.StatefulSet:
return m.mutate(namespacedName(o.GetObjectMeta()), m.statefulSetMutators, o.Spec.Template.GetObjectMeta())
default:
return nil, false
}
}
func (m *AnnotationMutators) rangeObjectList(ctx context.Context, list client.ObjectList, option client.ListOption, fn objectCallbackFunc) {
if err := m.clientReader.List(ctx, list, option); err != nil {
m.logger.Error(err, "Unable to list objects",
"kind", fmt.Sprintf("%T", list),
)
return
}
switch l := list.(type) {
case *corev1.NamespaceList:
for _, item := range l.Items {
fn(&item, nil)
}
case *appsv1.DeploymentList:
for _, item := range l.Items {
fn(&item, nil)
}
case *appsv1.DaemonSetList:
for _, item := range l.Items {
fn(&item, nil)
}
case *appsv1.StatefulSetList:
for _, item := range l.Items {
fn(&item, nil)
}
}
}
func (m *AnnotationMutators) mutate(name string, mutators map[string]instrumentation.AnnotationMutator, obj metav1.Object) (map[string]string, bool) {
mutator, ok := mutators[name]
if !ok {
mutator = m.defaultMutator
}
mutatedAnnotations := mutator.Mutate(obj)
return mutatedAnnotations, len(mutatedAnnotations) != 0
}
func namespacedName(obj metav1.Object) string {
return fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())
}
// NewAnnotationMutators creates mutators based on the AnnotationConfig provided and enabled instrumentation.TypeSet.
// The default mutator, which is used for non-configured resources, removes all auto-annotated annotations in the type
// set.
func NewAnnotationMutators(
clientWriter client.Writer,
clientReader client.Reader,
logger logr.Logger,
cfg AnnotationConfig,
typeSet instrumentation.TypeSet,
) *AnnotationMutators {
builder := newMutatorBuilder(typeSet)
return &AnnotationMutators{
clientWriter: clientWriter,
clientReader: clientReader,
logger: logger,
namespaceMutators: builder.buildMutators(getResources(cfg, typeSet, getNamespaces)),
deploymentMutators: builder.buildMutators(getResources(cfg, typeSet, getDeployments)),
daemonSetMutators: builder.buildMutators(getResources(cfg, typeSet, getDaemonSets)),
statefulSetMutators: builder.buildMutators(getResources(cfg, typeSet, getStatefulSets)),
defaultMutator: instrumentation.NewAnnotationMutator(maps.Values(builder.removeMutations)),
injectAnnotations: buildInjectAnnotations(typeSet),
}
}
func getResources(
cfg AnnotationConfig,
typeSet instrumentation.TypeSet,
resourceFn func(AnnotationResources) []string,
) map[instrumentation.Type][]string {
resources := map[instrumentation.Type][]string{}
for instType := range typeSet {
resources[instType] = resourceFn(cfg.getResources(instType))
}
return resources
}
type mutatorBuilder struct {
typeSet instrumentation.TypeSet
insertMutations map[instrumentation.Type]instrumentation.AnnotationMutation
removeMutations map[instrumentation.Type]instrumentation.AnnotationMutation
}
func (b *mutatorBuilder) buildMutators(resources map[instrumentation.Type][]string) map[string]instrumentation.AnnotationMutator {
mutators := map[string]instrumentation.AnnotationMutator{}
typeSetByResource := map[string]instrumentation.TypeSet{}
for instType, resourceNames := range resources {
for _, resourceName := range resourceNames {
typeSet, ok := typeSetByResource[resourceName]
if !ok {
typeSet = instrumentation.NewTypeSet()
}
typeSet[instType] = nil
typeSetByResource[resourceName] = typeSet
}
}
for resourceName, typeSet := range typeSetByResource {
var mutations []instrumentation.AnnotationMutation
for instType := range b.typeSet {
if _, ok := typeSet[instType]; ok {
mutations = append(mutations, b.insertMutations[instType])
} else {
mutations = append(mutations, b.removeMutations[instType])
}
}
mutators[resourceName] = instrumentation.NewAnnotationMutator(mutations)
}
return mutators
}
func newMutatorBuilder(typeSet instrumentation.TypeSet) *mutatorBuilder {
builder := &mutatorBuilder{
typeSet: typeSet,
insertMutations: map[instrumentation.Type]instrumentation.AnnotationMutation{},
removeMutations: map[instrumentation.Type]instrumentation.AnnotationMutation{},
}
for instType := range typeSet {
builder.insertMutations[instType], builder.removeMutations[instType] = buildMutations(instType)
}
return builder
}
// buildMutations builds insert and remove annotation mutations for the instrumentation.Type.
// The insert mutation is configured to modify for any missing annotation key.
// The remove mutation is configured to only modify if all annotation keys are present.
func buildMutations(instType instrumentation.Type) (instrumentation.AnnotationMutation, instrumentation.AnnotationMutation) {
annotations := buildAnnotations(instType)
return instrumentation.NewInsertAnnotationMutation(annotations),
instrumentation.NewRemoveAnnotationMutation(maps.Keys(annotations))
}
// buildAnnotations creates an annotation map of the inject and auto-annotate keys.
func buildAnnotations(instType instrumentation.Type) map[string]string {
return map[string]string{
instrumentation.InjectAnnotationKey(instType): defaultAnnotationValue,
AnnotateKey(instType): defaultAnnotationValue,
}
}
// buildInjectAnnotations returns the set of inject annotations corresponding to the instrumentation types
func buildInjectAnnotations(instTypeSet instrumentation.TypeSet) map[string]struct{} {
ret := map[string]struct{}{}
for instType := range instTypeSet {
ret[instrumentation.InjectAnnotationKey(instType)] = struct{}{}
}
return ret
}
// AnnotateKey joins the auto-annotate annotation prefix with the provided instrumentation.Type.
func AnnotateKey(instType instrumentation.Type) string {
return autoAnnotatePrefix + strings.ToLower(string(instType))
}