apis/v1alpha1/instrumentation_webhook.go (292 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/aws/amazon-cloudwatch-agent-operator/internal/config"
"github.com/aws/amazon-cloudwatch-agent-operator/pkg/constants"
)
const (
envPrefix = "OTEL_"
envSplunkPrefix = "SPLUNK_"
)
var (
_ admission.CustomValidator = &InstrumentationWebhook{}
_ admission.CustomDefaulter = &InstrumentationWebhook{}
initContainerDefaultLimitResources = corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("500m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
}
initContainerDefaultRequestedResources = corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
}
)
// +kubebuilder:webhook:path=/mutate-cloudwatch-aws-amazon-com-v1alpha1-instrumentation,mutating=true,failurePolicy=fail,sideEffects=None,groups=cloudwatch.aws.amazon.com,resources=instrumentations,verbs=create;update,versions=v1alpha1,name=minstrumentation.kb.io,admissionReviewVersions=v1
// +kubebuilder:webhook:verbs=create;update,path=/validate-cloudwatch-aws-amazon-com-v1alpha1-instrumentation,mutating=false,failurePolicy=fail,groups=cloudwatch.aws.amazon.com,resources=instrumentations,versions=v1alpha1,name=vinstrumentationcreateupdate.kb.io,sideEffects=none,admissionReviewVersions=v1
// +kubebuilder:webhook:verbs=delete,path=/validate-cloudwatch-aws-amazon-com-v1alpha1-instrumentation,mutating=false,failurePolicy=ignore,groups=cloudwatch.aws.amazon.com,resources=instrumentations,versions=v1alpha1,name=vinstrumentationdelete.kb.io,sideEffects=none,admissionReviewVersions=v1
// +kubebuilder:object:generate=false
type InstrumentationWebhook struct {
logger logr.Logger
cfg config.Config
scheme *runtime.Scheme
}
func (w InstrumentationWebhook) Default(ctx context.Context, obj runtime.Object) error {
instrumentation, ok := obj.(*Instrumentation)
if !ok {
return fmt.Errorf("expected an Instrumentation, received %T", obj)
}
return w.defaulter(instrumentation)
}
func (w InstrumentationWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
inst, ok := obj.(*Instrumentation)
if !ok {
return nil, fmt.Errorf("expected an Instrumentation, received %T", obj)
}
return w.validate(inst)
}
func (w InstrumentationWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
inst, ok := newObj.(*Instrumentation)
if !ok {
return nil, fmt.Errorf("expected an Instrumentation, received %T", newObj)
}
return w.validate(inst)
}
func (w InstrumentationWebhook) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
inst, ok := obj.(*Instrumentation)
if !ok || inst == nil {
return nil, fmt.Errorf("expected an Instrumentation, received %T", obj)
}
return w.validate(inst)
}
func (w InstrumentationWebhook) defaulter(r *Instrumentation) error {
if r.Labels == nil {
r.Labels = map[string]string{}
}
if r.Labels["app.kubernetes.io/managed-by"] == "" {
r.Labels["app.kubernetes.io/managed-by"] = "amazon-cloudwatch-agent-operator"
}
if r.Spec.Java.Image == "" {
r.Spec.Java.Image = w.cfg.AutoInstrumentationJavaImage()
}
if r.Spec.Java.Resources.Limits == nil {
r.Spec.Java.Resources.Limits = corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("500m"),
corev1.ResourceMemory: resource.MustParse("64Mi"),
}
}
if r.Spec.Java.Resources.Requests == nil {
r.Spec.Java.Resources.Requests = corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("50m"),
corev1.ResourceMemory: resource.MustParse("64Mi"),
}
}
if r.Spec.NodeJS.Image == "" {
r.Spec.NodeJS.Image = w.cfg.AutoInstrumentationNodeJSImage()
}
if r.Spec.NodeJS.Resources.Limits == nil {
r.Spec.NodeJS.Resources.Limits = corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("500m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
}
}
if r.Spec.NodeJS.Resources.Requests == nil {
r.Spec.NodeJS.Resources.Requests = corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("50m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
}
}
if r.Spec.Python.Image == "" {
r.Spec.Python.Image = w.cfg.AutoInstrumentationPythonImage()
}
if r.Spec.Python.Resources.Limits == nil {
r.Spec.Python.Resources.Limits = corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("500m"),
corev1.ResourceMemory: resource.MustParse("32Mi"),
}
}
if r.Spec.Python.Resources.Requests == nil {
r.Spec.Python.Resources.Requests = corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("50m"),
corev1.ResourceMemory: resource.MustParse("32Mi"),
}
}
if r.Spec.DotNet.Image == "" {
r.Spec.DotNet.Image = w.cfg.AutoInstrumentationDotNetImage()
}
if r.Spec.DotNet.Resources.Limits == nil {
r.Spec.DotNet.Resources.Limits = corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("500m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
}
}
if r.Spec.DotNet.Resources.Requests == nil {
r.Spec.DotNet.Resources.Requests = corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("50m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
}
}
if r.Spec.Go.Image == "" {
r.Spec.Go.Image = w.cfg.AutoInstrumentationGoImage()
}
if r.Spec.Go.Resources.Limits == nil {
r.Spec.Go.Resources.Limits = corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("500m"),
corev1.ResourceMemory: resource.MustParse("32Mi"),
}
}
if r.Spec.Go.Resources.Requests == nil {
r.Spec.Go.Resources.Requests = corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("50m"),
corev1.ResourceMemory: resource.MustParse("32Mi"),
}
}
if r.Spec.ApacheHttpd.Image == "" {
r.Spec.ApacheHttpd.Image = w.cfg.AutoInstrumentationApacheHttpdImage()
}
if r.Spec.ApacheHttpd.Resources.Limits == nil {
r.Spec.ApacheHttpd.Resources.Limits = initContainerDefaultLimitResources
}
if r.Spec.ApacheHttpd.Resources.Requests == nil {
r.Spec.ApacheHttpd.Resources.Requests = initContainerDefaultRequestedResources
}
if r.Spec.ApacheHttpd.Version == "" {
r.Spec.ApacheHttpd.Version = "2.4"
}
if r.Spec.ApacheHttpd.ConfigPath == "" {
r.Spec.ApacheHttpd.ConfigPath = "/usr/local/apache2/conf"
}
if r.Spec.Nginx.Image == "" {
r.Spec.Nginx.Image = w.cfg.AutoInstrumentationNginxImage()
}
if r.Spec.Nginx.Resources.Limits == nil {
r.Spec.Nginx.Resources.Limits = initContainerDefaultLimitResources
}
if r.Spec.Nginx.Resources.Requests == nil {
r.Spec.Nginx.Resources.Requests = initContainerDefaultRequestedResources
}
if r.Spec.Nginx.ConfigFile == "" {
r.Spec.Nginx.ConfigFile = "/etc/nginx/nginx.conf"
}
// Set the defaulting annotations
if r.Annotations == nil {
r.Annotations = map[string]string{}
}
r.Annotations[constants.AnnotationDefaultAutoInstrumentationJava] = w.cfg.AutoInstrumentationJavaImage()
r.Annotations[constants.AnnotationDefaultAutoInstrumentationNodeJS] = w.cfg.AutoInstrumentationNodeJSImage()
r.Annotations[constants.AnnotationDefaultAutoInstrumentationPython] = w.cfg.AutoInstrumentationPythonImage()
r.Annotations[constants.AnnotationDefaultAutoInstrumentationDotNet] = w.cfg.AutoInstrumentationDotNetImage()
r.Annotations[constants.AnnotationDefaultAutoInstrumentationGo] = w.cfg.AutoInstrumentationGoImage()
r.Annotations[constants.AnnotationDefaultAutoInstrumentationApacheHttpd] = w.cfg.AutoInstrumentationApacheHttpdImage()
r.Annotations[constants.AnnotationDefaultAutoInstrumentationNginx] = w.cfg.AutoInstrumentationNginxImage()
return nil
}
func (w InstrumentationWebhook) validate(r *Instrumentation) (admission.Warnings, error) {
var warnings []string
switch r.Spec.Sampler.Type {
case "":
warnings = append(warnings, "sampler type not set")
case TraceIDRatio, ParentBasedTraceIDRatio:
if r.Spec.Sampler.Argument != "" {
rate, err := strconv.ParseFloat(r.Spec.Sampler.Argument, 64)
if err != nil {
return warnings, fmt.Errorf("spec.sampler.argument is not a number: %s", r.Spec.Sampler.Argument)
}
if rate < 0 || rate > 1 {
return warnings, fmt.Errorf("spec.sampler.argument should be in rage [0..1]: %s", r.Spec.Sampler.Argument)
}
}
case JaegerRemote, ParentBasedJaegerRemote:
// value is a comma separated list of endpoint, pollingIntervalMs, initialSamplingRate
// Example: `endpoint=http://localhost:14250,pollingIntervalMs=5000,initialSamplingRate=0.25`
if r.Spec.Sampler.Argument != "" {
err := validateJaegerRemoteSamplerArgument(r.Spec.Sampler.Argument)
if err != nil {
return warnings, fmt.Errorf("spec.sampler.argument is not a valid argument for sampler %s: %w", r.Spec.Sampler.Type, err)
}
}
case AlwaysOn, AlwaysOff, ParentBasedAlwaysOn, ParentBasedAlwaysOff, XRaySampler:
default:
return warnings, fmt.Errorf("spec.sampler.type is not valid: %s", r.Spec.Sampler.Type)
}
// validate env vars
if err := w.validateEnv(r.Spec.Env); err != nil {
return warnings, err
}
if err := w.validateEnv(r.Spec.Java.Env); err != nil {
return warnings, err
}
if err := w.validateEnv(r.Spec.NodeJS.Env); err != nil {
return warnings, err
}
if err := w.validateEnv(r.Spec.Python.Env); err != nil {
return warnings, err
}
if err := w.validateEnv(r.Spec.DotNet.Env); err != nil {
return warnings, err
}
if err := w.validateEnv(r.Spec.Go.Env); err != nil {
return warnings, err
}
if err := w.validateEnv(r.Spec.ApacheHttpd.Env); err != nil {
return warnings, err
}
if err := w.validateEnv(r.Spec.Nginx.Env); err != nil {
return warnings, err
}
return warnings, nil
}
func (w InstrumentationWebhook) validateEnv(envs []corev1.EnvVar) error {
for _, env := range envs {
if !strings.HasPrefix(env.Name, envPrefix) && !strings.HasPrefix(env.Name, envSplunkPrefix) {
return fmt.Errorf("env name should start with \"OTEL_\" or \"SPLUNK_\": %s", env.Name)
}
}
return nil
}
func validateJaegerRemoteSamplerArgument(argument string) error {
parts := strings.Split(argument, ",")
for _, part := range parts {
kv := strings.Split(part, "=")
if len(kv) != 2 {
return fmt.Errorf("invalid argument: %s, the argument should be in the form of key=value", part)
}
switch kv[0] {
case "endpoint":
if kv[1] == "" {
return fmt.Errorf("endpoint cannot be empty")
}
case "pollingIntervalMs":
if _, err := strconv.Atoi(kv[1]); err != nil {
return fmt.Errorf("invalid pollingIntervalMs: %s", kv[1])
}
case "initialSamplingRate":
rate, err := strconv.ParseFloat(kv[1], 64)
if err != nil {
return fmt.Errorf("invalid initialSamplingRate: %s", kv[1])
}
if rate < 0 || rate > 1 {
return fmt.Errorf("initialSamplingRate should be in rage [0..1]: %s", kv[1])
}
}
}
return nil
}
func NewInstrumentationWebhook(logger logr.Logger, scheme *runtime.Scheme, cfg config.Config) *InstrumentationWebhook {
return &InstrumentationWebhook{
logger: logger,
scheme: scheme,
cfg: cfg,
}
}
func SetupInstrumentationWebhook(mgr ctrl.Manager, cfg config.Config) error {
ivw := NewInstrumentationWebhook(
mgr.GetLogger().WithValues("handler", "InstrumentationWebhook"),
mgr.GetScheme(),
cfg,
)
return ctrl.NewWebhookManagedBy(mgr).
For(&Instrumentation{}).
WithValidator(ivw).
WithDefaulter(ivw).
Complete()
}