diff --git a/.chloggen/monolithic_mode.yaml b/.chloggen/monolithic_mode.yaml new file mode 100755 index 000000000..ddca4642f --- /dev/null +++ b/.chloggen/monolithic_mode.yaml @@ -0,0 +1,18 @@ +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. operator, github action) +component: operator + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Support monolithic deployment mode + +# One or more tracking issues related to the change +issues: [710] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: | + The operator exposes a new CRD `TempoMonolithic`, which manages a Tempo instance in monolithic mode. + The monolithic mode supports the following additional storage backends: in-memory and file system (persistent volume). diff --git a/Makefile b/Makefile index 0a1c0d05f..aa0596902 100644 --- a/Makefile +++ b/Makefile @@ -455,7 +455,7 @@ cmctl: } .PHONY: api-docs -api-docs: docs/operator/api.md docs/operator/feature-gates.md docs/spec/tempo.grafana.com_tempostacks.yaml +api-docs: docs/operator/api.md docs/operator/feature-gates.md docs/spec/tempo.grafana.com_tempostacks.yaml docs/spec/tempo.grafana.com_tempomonolithics.yaml TYPES_TARGET := $(shell find apis/tempo -type f -iname "*_types.go") docs/operator/api.md: $(TYPES_TARGET) gen-crd-api-reference-docs diff --git a/PROJECT b/PROJECT index 37a0e69b5..cf3097871 100644 --- a/PROJECT +++ b/PROJECT @@ -21,4 +21,17 @@ resources: defaulting: true validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: grafana.com + group: tempo + kind: TempoMonolithic + path: github.com/grafana/tempo-operator/api/v1alpha1 + version: v1alpha1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 version: "3" diff --git a/apis/tempo/v1alpha1/tempomonolithic_types.go b/apis/tempo/v1alpha1/tempomonolithic_types.go new file mode 100644 index 000000000..bf89df673 --- /dev/null +++ b/apis/tempo/v1alpha1/tempomonolithic_types.go @@ -0,0 +1,236 @@ +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TempoMonolithicSpec defines the desired state of TempoMonolithic. +type TempoMonolithicSpec struct { + // Storage defines the backend storage configuration + // + // +kubebuilder:validation:Optional + Storage *MonolithicStorageSpec `json:"storage,omitempty"` + + // Ingestion defines the trace ingestion configuration + // + // +kubebuilder:validation:Optional + Ingestion *MonolithicIngestionSpec `json:"ingestion,omitempty"` + + // JaegerUI defines the Jaeger UI configuration + // + // +kubebuilder:validation:Optional + JaegerUI *MonolithicJaegerUISpec `json:"jaegerui,omitempty"` + + // ManagementState defines whether this instance is managed by the operator or self-managed + // + // +kubebuilder:validation:Optional + Management ManagementStateType `json:"management,omitempty"` + + // Observability defines observability configuration for the Tempo deployment + // + // +kubebuilder:validation:Optional + Observability *MonolithicObservabilitySpec `json:"observability,omitempty"` + + // ExtraConfig defines any extra (overlay) configuration for components + // + // +kubebuilder:validation:Optional + ExtraConfig *ExtraConfigSpec `json:"extraConfig,omitempty"` +} + +// MonolithicStorageSpec defines the storage for the Tempo deployment. +type MonolithicStorageSpec struct { + // Traces defines the backend storage configuration for traces + // + // +kubebuilder:validation:Required + Traces MonolithicTracesStorageSpec `json:"traces"` +} + +// MonolithicTracesStorageSpec defines the traces storage for the Tempo deployment. +type MonolithicTracesStorageSpec struct { + // Backend defines the backend for storing traces. Default: memory + // + // +kubebuilder:validation:Required + // +kubebuilder:default=memory + Backend MonolithicTracesStorageBackend `json:"backend"` + + // WAL defines the write-ahead logging (WAL) configuration + // + // +kubebuilder:validation:Optional + WAL *MonolithicTracesStorageWALSpec `json:"wal,omitempty"` + + // PV defines the Persistent Volume configuration + // + // +kubebuilder:validation:Optional + PV *MonolithicTracesStoragePVSpec `json:"pv,omitempty"` +} + +// MonolithicTracesStorageBackend defines the backend storage for traces. +// +// +kubebuilder:validation:Enum=memory;pv +type MonolithicTracesStorageBackend string + +const ( + // MonolithicTracesStorageBackendMemory defines storing traces in a tmpfs (in-memory filesystem). + MonolithicTracesStorageBackendMemory MonolithicTracesStorageBackend = "memory" + // MonolithicTracesStorageBackendPV defines storing traces in a Persistent Volume. + MonolithicTracesStorageBackendPV MonolithicTracesStorageBackend = "pv" +) + +// MonolithicTracesStorageWALSpec defines the write-ahead logging (WAL) configuration. +type MonolithicTracesStorageWALSpec struct { + // Size defines the size of the Persistent Volume for storing the WAL. Defaults to 10Gi. + // + // +kubebuilder:validation:Required + // +kubebuilder:default="10Gi" + Size resource.Quantity `json:"size"` +} + +// MonolithicTracesStoragePVSpec defines the Persistent Volume configuration. +type MonolithicTracesStoragePVSpec struct { + // Size defines the size of the Persistent Volume for storing the traces. Defaults to 10Gi. + // + // +kubebuilder:validation:Required + // +kubebuilder:default="10Gi" + Size resource.Quantity `json:"size"` +} + +// MonolithicIngestionSpec defines the ingestion settings. +type MonolithicIngestionSpec struct { + // OTLP defines the ingestion configuration for OTLP + // + // +kubebuilder:validation:Optional + OTLP *MonolithicIngestionOTLPSpec `json:"otlp,omitempty"` +} + +// MonolithicIngestionOTLPSpec defines the settings for OTLP ingestion. +type MonolithicIngestionOTLPSpec struct { + // GRPC defines the OTLP/gRPC configuration + // + // +kubebuilder:validation:Optional + GRPC *MonolithicIngestionOTLPProtocolsGRPCSpec `json:"grpc,omitempty"` + + // HTTP defines the OTLP/HTTP configuration + // + // +kubebuilder:validation:Optional + HTTP *MonolithicIngestionOTLPProtocolsHTTPSpec `json:"http,omitempty"` +} + +// MonolithicIngestionOTLPProtocolsGRPCSpec defines the settings for OTLP ingestion over GRPC. +type MonolithicIngestionOTLPProtocolsGRPCSpec struct { + // Enabled defines if OTLP over gRPC is enabled + // + // +kubebuilder:validation:Required + // +kubebuilder:default=true + Enabled bool `json:"enabled"` +} + +// MonolithicIngestionOTLPProtocolsHTTPSpec defines the settings for OTLP ingestion over HTTP. +type MonolithicIngestionOTLPProtocolsHTTPSpec struct { + // Enabled defines if OTLP over HTTP is enabled + // + // +kubebuilder:validation:Required + Enabled bool `json:"enabled"` +} + +// MonolithicJaegerUISpec defines the settings for the Jaeger UI. +type MonolithicJaegerUISpec struct { + // Enabled defines if the Jaeger UI should be enabled + // + // +kubebuilder:validation:Required + Enabled bool `json:"enabled"` + + // Ingress defines the ingress configuration for Jaeger UI + // + // +kubebuilder:validation:Optional + Ingress *MonolithicJaegerUIIngressSpec `json:"ingress,omitempty"` + + // Route defines the route configuration for Jaeger UI + // + // +kubebuilder:validation:Optional + Route *MonolithicJaegerUIRouteSpec `json:"route,omitempty"` +} + +// MonolithicJaegerUIIngressSpec defines the settings for the Jaeger UI ingress. +type MonolithicJaegerUIIngressSpec struct { + // Enabled defines if an Ingress object should be created for Jaeger UI + // + // +kubebuilder:validation:Required + Enabled bool `json:"enabled"` +} + +// MonolithicJaegerUIRouteSpec defines the settings for the Jaeger UI route. +type MonolithicJaegerUIRouteSpec struct { + // Enabled defines if a Route object should be created for Jaeger UI + // + // +kubebuilder:validation:Required + Enabled bool `json:"enabled"` +} + +// MonolithicObservabilitySpec defines the observability settings of the Tempo deployment. +type MonolithicObservabilitySpec struct { + // Metrics defines the metrics configuration of the Tempo deployment + // + // +kubebuilder:validation:Optional + Metrics *MonolithicObservabilityMetricsSpec `json:"metrics,omitempty"` +} + +// MonolithicObservabilityMetricsSpec defines the metrics settings of the Tempo deployment. +type MonolithicObservabilityMetricsSpec struct { + // ServiceMonitors defines the ServiceMonitor configuration + // + // +kubebuilder:validation:Optional + ServiceMonitors *MonolithicObservabilityMetricsServiceMonitorsSpec `json:"serviceMonitors,omitempty"` + + // ServiceMonitors defines the PrometheusRule configuration + // + // +kubebuilder:validation:Optional + PrometheusRules *MonolithicObservabilityMetricsPrometheusRulesSpec `json:"prometheusRules,omitempty"` +} + +// MonolithicObservabilityMetricsServiceMonitorsSpec defines the ServiceMonitor settings. +type MonolithicObservabilityMetricsServiceMonitorsSpec struct { + // Enabled defines if the operator should create ServiceMonitors for this Tempo deployment + // + // +kubebuilder:validation:Required + Enabled bool `json:"enabled"` +} + +// MonolithicObservabilityMetricsPrometheusRulesSpec defines the PrometheusRules settings. +type MonolithicObservabilityMetricsPrometheusRulesSpec struct { + // Enabled defines if the operator should create PrometheusRules for this Tempo deployment + // + // +kubebuilder:validation:Required + Enabled bool `json:"enabled"` +} + +// TempoMonolithicStatus defines the observed state of TempoMonolithic. +type TempoMonolithicStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// TempoMonolithic is the Schema for the tempomonolithics API. +type TempoMonolithic struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec TempoMonolithicSpec `json:"spec,omitempty"` + Status TempoMonolithicStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// TempoMonolithicList contains a list of TempoMonolithic. +type TempoMonolithicList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []TempoMonolithic `json:"items"` +} + +func init() { + SchemeBuilder.Register(&TempoMonolithic{}, &TempoMonolithicList{}) +} diff --git a/apis/tempo/v1alpha1/tempomonolithic_webhook.go b/apis/tempo/v1alpha1/tempomonolithic_webhook.go new file mode 100644 index 000000000..e0d240986 --- /dev/null +++ b/apis/tempo/v1alpha1/tempomonolithic_webhook.go @@ -0,0 +1,96 @@ +package v1alpha1 + +import ( + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// SetupWebhookWithManager will setup the manager to manage the webhooks. +func (r *TempoMonolithic) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// Default sets all default values in a central place, instead of setting it at every place where the value is accessed. +// NOTE: This function is called inside the Reconcile loop, NOT in the webhook. +// We want to keep the CR as minimal as the user configures it, and not modify it in any way (except for upgrades). +func (r *TempoMonolithic) Default() { + if r.Spec.Storage == nil { + r.Spec.Storage = &MonolithicStorageSpec{} + } + + if r.Spec.Storage.Traces.Backend == "" { + r.Spec.Storage.Traces.Backend = MonolithicTracesStorageBackendMemory + } + + if r.Spec.Storage.Traces.Backend != MonolithicTracesStorageBackendMemory && r.Spec.Storage.Traces.WAL == nil { + r.Spec.Storage.Traces.WAL = &MonolithicTracesStorageWALSpec{ + Size: tenGBQuantity, + } + } + + if r.Spec.Storage.Traces.Backend == MonolithicTracesStorageBackendPV && r.Spec.Storage.Traces.PV == nil { + r.Spec.Storage.Traces.PV = &MonolithicTracesStoragePVSpec{ + Size: tenGBQuantity, + } + } + + if r.Spec.Ingestion == nil { + r.Spec.Ingestion = &MonolithicIngestionSpec{} + } + if r.Spec.Ingestion.OTLP == nil { + r.Spec.Ingestion.OTLP = &MonolithicIngestionOTLPSpec{} + } + if r.Spec.Ingestion.OTLP.GRPC == nil { + r.Spec.Ingestion.OTLP.GRPC = &MonolithicIngestionOTLPProtocolsGRPCSpec{ + Enabled: true, + } + } + if r.Spec.Ingestion.OTLP.HTTP == nil { + r.Spec.Ingestion.OTLP.HTTP = &MonolithicIngestionOTLPProtocolsHTTPSpec{ + Enabled: true, + } + } +} + +//+kubebuilder:webhook:path=/validate-tempo-grafana-com-v1alpha1-tempomonolithic,mutating=false,failurePolicy=fail,sideEffects=None,groups=tempo.grafana.com,resources=tempomonolithics,verbs=create;update,versions=v1alpha1,name=vtempomonolithic.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &TempoMonolithic{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type. +func (r *TempoMonolithic) ValidateCreate() (admission.Warnings, error) { + return r.validate() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. +func (r *TempoMonolithic) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + return r.validate() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type. +func (r *TempoMonolithic) ValidateDelete() (admission.Warnings, error) { + // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. + return r.validate() +} + +func (tempo *TempoMonolithic) validate() (admission.Warnings, error) { + log := ctrl.Log.WithName("tempomonolithic-webhook") + log.V(1).Info("running validating webhook", "name", tempo.Name) + + allWarnings := admission.Warnings{} + allErrors := field.ErrorList{} + + if tempo.Spec.ExtraConfig != nil && len(tempo.Spec.ExtraConfig.Tempo.Raw) > 0 { + allWarnings = append(allWarnings, "overriding Tempo configuration could potentially break the deployment, use it carefully") + } + + if len(allErrors) == 0 { + return allWarnings, nil + } + return allWarnings, apierrors.NewInvalid(tempo.GroupVersionKind().GroupKind(), tempo.Name, allErrors) +} diff --git a/apis/tempo/v1alpha1/tempomonolithic_webhook_test.go b/apis/tempo/v1alpha1/tempomonolithic_webhook_test.go new file mode 100644 index 000000000..93c68c8db --- /dev/null +++ b/apis/tempo/v1alpha1/tempomonolithic_webhook_test.go @@ -0,0 +1,133 @@ +package v1alpha1 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMonolithicDefault(t *testing.T) { + tests := []struct { + name string + input *TempoMonolithic + expected *TempoMonolithic + }{ + { + name: "empty spec, set memory backend and enable OTLP/gRPC and OTLP/HTTP", + input: &TempoMonolithic{ + Spec: TempoMonolithicSpec{}, + }, + expected: &TempoMonolithic{ + Spec: TempoMonolithicSpec{ + Storage: &MonolithicStorageSpec{ + Traces: MonolithicTracesStorageSpec{ + Backend: "memory", + }, + }, + Ingestion: &MonolithicIngestionSpec{ + OTLP: &MonolithicIngestionOTLPSpec{ + GRPC: &MonolithicIngestionOTLPProtocolsGRPCSpec{ + Enabled: true, + }, + HTTP: &MonolithicIngestionOTLPProtocolsHTTPSpec{ + Enabled: true, + }, + }, + }, + }, + }, + }, + { + name: "set default values for PV", + input: &TempoMonolithic{ + Spec: TempoMonolithicSpec{ + Storage: &MonolithicStorageSpec{ + Traces: MonolithicTracesStorageSpec{ + Backend: "pv", + }, + }, + }, + }, + expected: &TempoMonolithic{ + Spec: TempoMonolithicSpec{ + Storage: &MonolithicStorageSpec{ + Traces: MonolithicTracesStorageSpec{ + Backend: "pv", + WAL: &MonolithicTracesStorageWALSpec{ + Size: tenGBQuantity, + }, + PV: &MonolithicTracesStoragePVSpec{ + Size: tenGBQuantity, + }, + }, + }, + Ingestion: &MonolithicIngestionSpec{ + OTLP: &MonolithicIngestionOTLPSpec{ + GRPC: &MonolithicIngestionOTLPProtocolsGRPCSpec{ + Enabled: true, + }, + HTTP: &MonolithicIngestionOTLPProtocolsHTTPSpec{ + Enabled: true, + }, + }, + }, + }, + }, + }, + { + name: "do not change already set values", + input: &TempoMonolithic{ + Spec: TempoMonolithicSpec{ + Storage: &MonolithicStorageSpec{ + Traces: MonolithicTracesStorageSpec{ + Backend: "s3", + WAL: &MonolithicTracesStorageWALSpec{ + Size: tenGBQuantity, + }, + }, + }, + Ingestion: &MonolithicIngestionSpec{ + OTLP: &MonolithicIngestionOTLPSpec{ + // GRPC is explicitly disabled and should not be enabled by webhook + GRPC: &MonolithicIngestionOTLPProtocolsGRPCSpec{ + Enabled: false, + }, + HTTP: &MonolithicIngestionOTLPProtocolsHTTPSpec{ + Enabled: true, + }, + }, + }, + }, + }, + expected: &TempoMonolithic{ + Spec: TempoMonolithicSpec{ + Storage: &MonolithicStorageSpec{ + Traces: MonolithicTracesStorageSpec{ + Backend: "s3", + WAL: &MonolithicTracesStorageWALSpec{ + Size: tenGBQuantity, + }, + }, + }, + Ingestion: &MonolithicIngestionSpec{ + OTLP: &MonolithicIngestionOTLPSpec{ + GRPC: &MonolithicIngestionOTLPProtocolsGRPCSpec{ + Enabled: false, + }, + HTTP: &MonolithicIngestionOTLPProtocolsHTTPSpec{ + Enabled: true, + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.input.Default() + assert.Equal(t, test.expected, test.input) + }) + } +} diff --git a/apis/tempo/v1alpha1/tempostack_types.go b/apis/tempo/v1alpha1/tempostack_types.go index 5e293ff06..d5c2a9d73 100644 --- a/apis/tempo/v1alpha1/tempostack_types.go +++ b/apis/tempo/v1alpha1/tempostack_types.go @@ -132,6 +132,8 @@ type TempoStackSpec struct { // ExtraConfigSpec defines extra configurations for tempo that will be merged with the operator generated, configurations defined here // has precedence and could override generated config. type ExtraConfigSpec struct { + // Tempo defines any extra Tempo configuration, which will be merged with the operator's generated Tempo configuration + // // +optional // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Tempo Extra Configurations" Tempo apiextensionsv1.JSON `json:"tempo,omitempty"` diff --git a/apis/tempo/v1alpha1/zz_generated.deepcopy.go b/apis/tempo/v1alpha1/zz_generated.deepcopy.go index f83331837..ea10afd40 100644 --- a/apis/tempo/v1alpha1/zz_generated.deepcopy.go +++ b/apis/tempo/v1alpha1/zz_generated.deepcopy.go @@ -382,6 +382,284 @@ func (in *MetricsConfigSpec) DeepCopy() *MetricsConfigSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MonolithicIngestionOTLPProtocolsGRPCSpec) DeepCopyInto(out *MonolithicIngestionOTLPProtocolsGRPCSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonolithicIngestionOTLPProtocolsGRPCSpec. +func (in *MonolithicIngestionOTLPProtocolsGRPCSpec) DeepCopy() *MonolithicIngestionOTLPProtocolsGRPCSpec { + if in == nil { + return nil + } + out := new(MonolithicIngestionOTLPProtocolsGRPCSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MonolithicIngestionOTLPProtocolsHTTPSpec) DeepCopyInto(out *MonolithicIngestionOTLPProtocolsHTTPSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonolithicIngestionOTLPProtocolsHTTPSpec. +func (in *MonolithicIngestionOTLPProtocolsHTTPSpec) DeepCopy() *MonolithicIngestionOTLPProtocolsHTTPSpec { + if in == nil { + return nil + } + out := new(MonolithicIngestionOTLPProtocolsHTTPSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MonolithicIngestionOTLPSpec) DeepCopyInto(out *MonolithicIngestionOTLPSpec) { + *out = *in + if in.GRPC != nil { + in, out := &in.GRPC, &out.GRPC + *out = new(MonolithicIngestionOTLPProtocolsGRPCSpec) + **out = **in + } + if in.HTTP != nil { + in, out := &in.HTTP, &out.HTTP + *out = new(MonolithicIngestionOTLPProtocolsHTTPSpec) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonolithicIngestionOTLPSpec. +func (in *MonolithicIngestionOTLPSpec) DeepCopy() *MonolithicIngestionOTLPSpec { + if in == nil { + return nil + } + out := new(MonolithicIngestionOTLPSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MonolithicIngestionSpec) DeepCopyInto(out *MonolithicIngestionSpec) { + *out = *in + if in.OTLP != nil { + in, out := &in.OTLP, &out.OTLP + *out = new(MonolithicIngestionOTLPSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonolithicIngestionSpec. +func (in *MonolithicIngestionSpec) DeepCopy() *MonolithicIngestionSpec { + if in == nil { + return nil + } + out := new(MonolithicIngestionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MonolithicJaegerUIIngressSpec) DeepCopyInto(out *MonolithicJaegerUIIngressSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonolithicJaegerUIIngressSpec. +func (in *MonolithicJaegerUIIngressSpec) DeepCopy() *MonolithicJaegerUIIngressSpec { + if in == nil { + return nil + } + out := new(MonolithicJaegerUIIngressSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MonolithicJaegerUIRouteSpec) DeepCopyInto(out *MonolithicJaegerUIRouteSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonolithicJaegerUIRouteSpec. +func (in *MonolithicJaegerUIRouteSpec) DeepCopy() *MonolithicJaegerUIRouteSpec { + if in == nil { + return nil + } + out := new(MonolithicJaegerUIRouteSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MonolithicJaegerUISpec) DeepCopyInto(out *MonolithicJaegerUISpec) { + *out = *in + if in.Ingress != nil { + in, out := &in.Ingress, &out.Ingress + *out = new(MonolithicJaegerUIIngressSpec) + **out = **in + } + if in.Route != nil { + in, out := &in.Route, &out.Route + *out = new(MonolithicJaegerUIRouteSpec) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonolithicJaegerUISpec. +func (in *MonolithicJaegerUISpec) DeepCopy() *MonolithicJaegerUISpec { + if in == nil { + return nil + } + out := new(MonolithicJaegerUISpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MonolithicObservabilityMetricsPrometheusRulesSpec) DeepCopyInto(out *MonolithicObservabilityMetricsPrometheusRulesSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonolithicObservabilityMetricsPrometheusRulesSpec. +func (in *MonolithicObservabilityMetricsPrometheusRulesSpec) DeepCopy() *MonolithicObservabilityMetricsPrometheusRulesSpec { + if in == nil { + return nil + } + out := new(MonolithicObservabilityMetricsPrometheusRulesSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MonolithicObservabilityMetricsServiceMonitorsSpec) DeepCopyInto(out *MonolithicObservabilityMetricsServiceMonitorsSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonolithicObservabilityMetricsServiceMonitorsSpec. +func (in *MonolithicObservabilityMetricsServiceMonitorsSpec) DeepCopy() *MonolithicObservabilityMetricsServiceMonitorsSpec { + if in == nil { + return nil + } + out := new(MonolithicObservabilityMetricsServiceMonitorsSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MonolithicObservabilityMetricsSpec) DeepCopyInto(out *MonolithicObservabilityMetricsSpec) { + *out = *in + if in.ServiceMonitors != nil { + in, out := &in.ServiceMonitors, &out.ServiceMonitors + *out = new(MonolithicObservabilityMetricsServiceMonitorsSpec) + **out = **in + } + if in.PrometheusRules != nil { + in, out := &in.PrometheusRules, &out.PrometheusRules + *out = new(MonolithicObservabilityMetricsPrometheusRulesSpec) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonolithicObservabilityMetricsSpec. +func (in *MonolithicObservabilityMetricsSpec) DeepCopy() *MonolithicObservabilityMetricsSpec { + if in == nil { + return nil + } + out := new(MonolithicObservabilityMetricsSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MonolithicObservabilitySpec) DeepCopyInto(out *MonolithicObservabilitySpec) { + *out = *in + if in.Metrics != nil { + in, out := &in.Metrics, &out.Metrics + *out = new(MonolithicObservabilityMetricsSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonolithicObservabilitySpec. +func (in *MonolithicObservabilitySpec) DeepCopy() *MonolithicObservabilitySpec { + if in == nil { + return nil + } + out := new(MonolithicObservabilitySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MonolithicStorageSpec) DeepCopyInto(out *MonolithicStorageSpec) { + *out = *in + in.Traces.DeepCopyInto(&out.Traces) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonolithicStorageSpec. +func (in *MonolithicStorageSpec) DeepCopy() *MonolithicStorageSpec { + if in == nil { + return nil + } + out := new(MonolithicStorageSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MonolithicTracesStoragePVSpec) DeepCopyInto(out *MonolithicTracesStoragePVSpec) { + *out = *in + out.Size = in.Size.DeepCopy() +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonolithicTracesStoragePVSpec. +func (in *MonolithicTracesStoragePVSpec) DeepCopy() *MonolithicTracesStoragePVSpec { + if in == nil { + return nil + } + out := new(MonolithicTracesStoragePVSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MonolithicTracesStorageSpec) DeepCopyInto(out *MonolithicTracesStorageSpec) { + *out = *in + if in.WAL != nil { + in, out := &in.WAL, &out.WAL + *out = new(MonolithicTracesStorageWALSpec) + (*in).DeepCopyInto(*out) + } + if in.PV != nil { + in, out := &in.PV, &out.PV + *out = new(MonolithicTracesStoragePVSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonolithicTracesStorageSpec. +func (in *MonolithicTracesStorageSpec) DeepCopy() *MonolithicTracesStorageSpec { + if in == nil { + return nil + } + out := new(MonolithicTracesStorageSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MonolithicTracesStorageWALSpec) DeepCopyInto(out *MonolithicTracesStorageWALSpec) { + *out = *in + out.Size = in.Size.DeepCopy() +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonolithicTracesStorageWALSpec. +func (in *MonolithicTracesStorageWALSpec) DeepCopy() *MonolithicTracesStorageWALSpec { + if in == nil { + return nil + } + out := new(MonolithicTracesStorageWALSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCSpec) DeepCopyInto(out *OIDCSpec) { *out = *in @@ -787,6 +1065,120 @@ func (in *TempoGatewaySpec) DeepCopy() *TempoGatewaySpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TempoMonolithic) DeepCopyInto(out *TempoMonolithic) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TempoMonolithic. +func (in *TempoMonolithic) DeepCopy() *TempoMonolithic { + if in == nil { + return nil + } + out := new(TempoMonolithic) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TempoMonolithic) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TempoMonolithicList) DeepCopyInto(out *TempoMonolithicList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TempoMonolithic, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TempoMonolithicList. +func (in *TempoMonolithicList) DeepCopy() *TempoMonolithicList { + if in == nil { + return nil + } + out := new(TempoMonolithicList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TempoMonolithicList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TempoMonolithicSpec) DeepCopyInto(out *TempoMonolithicSpec) { + *out = *in + if in.Storage != nil { + in, out := &in.Storage, &out.Storage + *out = new(MonolithicStorageSpec) + (*in).DeepCopyInto(*out) + } + if in.Ingestion != nil { + in, out := &in.Ingestion, &out.Ingestion + *out = new(MonolithicIngestionSpec) + (*in).DeepCopyInto(*out) + } + if in.JaegerUI != nil { + in, out := &in.JaegerUI, &out.JaegerUI + *out = new(MonolithicJaegerUISpec) + (*in).DeepCopyInto(*out) + } + if in.Observability != nil { + in, out := &in.Observability, &out.Observability + *out = new(MonolithicObservabilitySpec) + (*in).DeepCopyInto(*out) + } + if in.ExtraConfig != nil { + in, out := &in.ExtraConfig, &out.ExtraConfig + *out = new(ExtraConfigSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TempoMonolithicSpec. +func (in *TempoMonolithicSpec) DeepCopy() *TempoMonolithicSpec { + if in == nil { + return nil + } + out := new(TempoMonolithicSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TempoMonolithicStatus) DeepCopyInto(out *TempoMonolithicStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TempoMonolithicStatus. +func (in *TempoMonolithicStatus) DeepCopy() *TempoMonolithicStatus { + if in == nil { + return nil + } + out := new(TempoMonolithicStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TempoQueryFrontendSpec) DeepCopyInto(out *TempoQueryFrontendSpec) { *out = *in diff --git a/bundle/community/manifests/tempo-operator.clusterserviceversion.yaml b/bundle/community/manifests/tempo-operator.clusterserviceversion.yaml index bcb36cb74..f839ea08d 100644 --- a/bundle/community/manifests/tempo-operator.clusterserviceversion.yaml +++ b/bundle/community/manifests/tempo-operator.clusterserviceversion.yaml @@ -4,6 +4,20 @@ metadata: annotations: alm-examples: |- [ + { + "apiVersion": "tempo.grafana.com/v1alpha1", + "kind": "TempoMonolithic", + "metadata": { + "name": "sample" + }, + "spec": { + "storage": { + "traces": { + "backend": "memory" + } + } + } + }, { "apiVersion": "tempo.grafana.com/v1alpha1", "kind": "TempoStack", @@ -42,7 +56,7 @@ metadata: capabilities: Deep Insights categories: Logging & Tracing,Monitoring containerImage: ghcr.io/grafana/tempo-operator/tempo-operator:v0.7.0 - createdAt: "2024-01-17T16:42:02Z" + createdAt: "2024-01-26T12:10:07Z" description: Create and manage deployments of Tempo, a high-scale distributed tracing backend. operatorframework.io/cluster-monitoring: "true" @@ -57,6 +71,16 @@ spec: apiservicedefinitions: {} customresourcedefinitions: owned: + - description: TempoMonolithic is the Schema for the tempomonolithics API. + displayName: Tempo Monolithic + kind: TempoMonolithic + name: tempomonolithics.tempo.grafana.com + specDescriptors: + - description: Tempo defines any extra Tempo configuration, which will be merged + with the operator's generated Tempo configuration + displayName: Tempo Extra Configurations + path: extraConfig.tempo + version: v1alpha1 - description: TempoStack is the spec for Tempo deployments. displayName: TempoStack kind: TempoStack @@ -89,7 +113,9 @@ spec: specDescriptors: - displayName: Extra Configurations path: extraConfig - - displayName: Tempo Extra Configurations + - description: Tempo defines any extra Tempo configuration, which will be merged + with the operator's generated Tempo configuration + displayName: Tempo Extra Configurations path: extraConfig.tempo - description: HashRing defines the spec for the distributed hash ring configuration. displayName: Hash Ring @@ -743,6 +769,32 @@ spec: - list - update - watch + - apiGroups: + - tempo.grafana.com + resources: + - tempomonolithics + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - tempo.grafana.com + resources: + - tempomonolithics/finalizers + verbs: + - update + - apiGroups: + - tempo.grafana.com + resources: + - tempomonolithics/status + verbs: + - get + - patch + - update - apiGroups: - tempo.grafana.com resources: @@ -989,6 +1041,26 @@ spec: targetPort: 9443 type: MutatingAdmissionWebhook webhookPath: /mutate-tempo-grafana-com-v1alpha1-tempostack + - admissionReviewVersions: + - v1 + containerPort: 443 + deploymentName: tempo-operator-controller + failurePolicy: Fail + generateName: vtempomonolithic.kb.io + rules: + - apiGroups: + - tempo.grafana.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - tempomonolithics + sideEffects: None + targetPort: 9443 + type: ValidatingAdmissionWebhook + webhookPath: /validate-tempo-grafana-com-v1alpha1-tempomonolithic - admissionReviewVersions: - v1 containerPort: 443 diff --git a/bundle/community/manifests/tempo.grafana.com_tempomonolithics.yaml b/bundle/community/manifests/tempo.grafana.com_tempomonolithics.yaml new file mode 100644 index 000000000..e43b04047 --- /dev/null +++ b/bundle/community/manifests/tempo.grafana.com_tempomonolithics.yaml @@ -0,0 +1,210 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + labels: + app.kubernetes.io/managed-by: operator-lifecycle-manager + app.kubernetes.io/name: tempo-operator + app.kubernetes.io/part-of: tempo-operator + name: tempomonolithics.tempo.grafana.com +spec: + group: tempo.grafana.com + names: + kind: TempoMonolithic + listKind: TempoMonolithicList + plural: tempomonolithics + singular: tempomonolithic + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: TempoMonolithic is the Schema for the tempomonolithics API. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: TempoMonolithicSpec defines the desired state of TempoMonolithic. + properties: + extraConfig: + description: ExtraConfig defines any extra (overlay) configuration + for components + properties: + tempo: + description: Tempo defines any extra Tempo configuration, which + will be merged with the operator's generated Tempo configuration + x-kubernetes-preserve-unknown-fields: true + type: object + ingestion: + description: Ingestion defines the trace ingestion configuration + properties: + otlp: + description: OTLP defines the ingestion configuration for OTLP + properties: + grpc: + description: GRPC defines the OTLP/gRPC configuration + properties: + enabled: + default: true + description: Enabled defines if OTLP over gRPC is enabled + type: boolean + required: + - enabled + type: object + http: + description: HTTP defines the OTLP/HTTP configuration + properties: + enabled: + description: Enabled defines if OTLP over HTTP is enabled + type: boolean + required: + - enabled + type: object + type: object + type: object + jaegerui: + description: JaegerUI defines the Jaeger UI configuration + properties: + enabled: + description: Enabled defines if the Jaeger UI should be enabled + type: boolean + ingress: + description: Ingress defines the ingress configuration for Jaeger + UI + properties: + enabled: + description: Enabled defines if an Ingress object should be + created for Jaeger UI + type: boolean + required: + - enabled + type: object + route: + description: Route defines the route configuration for Jaeger + UI + properties: + enabled: + description: Enabled defines if a Route object should be created + for Jaeger UI + type: boolean + required: + - enabled + type: object + required: + - enabled + type: object + management: + description: ManagementState defines whether this instance is managed + by the operator or self-managed + enum: + - Managed + - Unmanaged + type: string + observability: + description: Observability defines observability configuration for + the Tempo deployment + properties: + metrics: + description: Metrics defines the metrics configuration of the + Tempo deployment + properties: + prometheusRules: + description: ServiceMonitors defines the PrometheusRule configuration + properties: + enabled: + description: Enabled defines if the operator should create + PrometheusRules for this Tempo deployment + type: boolean + required: + - enabled + type: object + serviceMonitors: + description: ServiceMonitors defines the ServiceMonitor configuration + properties: + enabled: + description: Enabled defines if the operator should create + ServiceMonitors for this Tempo deployment + type: boolean + required: + - enabled + type: object + type: object + type: object + storage: + description: Storage defines the backend storage configuration + properties: + traces: + description: Traces defines the backend storage configuration + for traces + properties: + backend: + default: memory + description: 'Backend defines the backend for storing traces. + Default: memory' + enum: + - memory + - pv + type: string + pv: + description: PV defines the Persistent Volume configuration + properties: + size: + anyOf: + - type: integer + - type: string + default: 10Gi + description: Size defines the size of the Persistent Volume + for storing the traces. Defaults to 10Gi. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - size + type: object + wal: + description: WAL defines the write-ahead logging (WAL) configuration + properties: + size: + anyOf: + - type: integer + - type: string + default: 10Gi + description: Size defines the size of the Persistent Volume + for storing the WAL. Defaults to 10Gi. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - size + type: object + required: + - backend + type: object + required: + - traces + type: object + type: object + status: + description: TempoMonolithicStatus defines the observed state of TempoMonolithic. + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/bundle/community/manifests/tempo.grafana.com_tempostacks.yaml b/bundle/community/manifests/tempo.grafana.com_tempostacks.yaml index 5a309fae3..de8d6a55a 100644 --- a/bundle/community/manifests/tempo.grafana.com_tempostacks.yaml +++ b/bundle/community/manifests/tempo.grafana.com_tempostacks.yaml @@ -59,6 +59,8 @@ spec: defined here has precedence and could override generated config. properties: tempo: + description: Tempo defines any extra Tempo configuration, which + will be merged with the operator's generated Tempo configuration x-kubernetes-preserve-unknown-fields: true type: object hashRing: diff --git a/bundle/openshift/manifests/tempo-operator.clusterserviceversion.yaml b/bundle/openshift/manifests/tempo-operator.clusterserviceversion.yaml index 3c08fef68..c449b0849 100644 --- a/bundle/openshift/manifests/tempo-operator.clusterserviceversion.yaml +++ b/bundle/openshift/manifests/tempo-operator.clusterserviceversion.yaml @@ -4,6 +4,20 @@ metadata: annotations: alm-examples: |- [ + { + "apiVersion": "tempo.grafana.com/v1alpha1", + "kind": "TempoMonolithic", + "metadata": { + "name": "sample" + }, + "spec": { + "storage": { + "traces": { + "backend": "memory" + } + } + } + }, { "apiVersion": "tempo.grafana.com/v1alpha1", "kind": "TempoStack", @@ -42,7 +56,7 @@ metadata: capabilities: Deep Insights categories: Logging & Tracing,Monitoring containerImage: ghcr.io/grafana/tempo-operator/tempo-operator:v0.7.0 - createdAt: "2024-01-17T16:42:01Z" + createdAt: "2024-01-26T12:10:06Z" description: Create and manage deployments of Tempo, a high-scale distributed tracing backend. operatorframework.io/cluster-monitoring: "true" @@ -57,6 +71,16 @@ spec: apiservicedefinitions: {} customresourcedefinitions: owned: + - description: TempoMonolithic is the Schema for the tempomonolithics API. + displayName: Tempo Monolithic + kind: TempoMonolithic + name: tempomonolithics.tempo.grafana.com + specDescriptors: + - description: Tempo defines any extra Tempo configuration, which will be merged + with the operator's generated Tempo configuration + displayName: Tempo Extra Configurations + path: extraConfig.tempo + version: v1alpha1 - description: TempoStack is the spec for Tempo deployments. displayName: TempoStack kind: TempoStack @@ -89,7 +113,9 @@ spec: specDescriptors: - displayName: Extra Configurations path: extraConfig - - displayName: Tempo Extra Configurations + - description: Tempo defines any extra Tempo configuration, which will be merged + with the operator's generated Tempo configuration + displayName: Tempo Extra Configurations path: extraConfig.tempo - description: HashRing defines the spec for the distributed hash ring configuration. displayName: Hash Ring @@ -753,6 +779,32 @@ spec: - list - update - watch + - apiGroups: + - tempo.grafana.com + resources: + - tempomonolithics + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - tempo.grafana.com + resources: + - tempomonolithics/finalizers + verbs: + - update + - apiGroups: + - tempo.grafana.com + resources: + - tempomonolithics/status + verbs: + - get + - patch + - update - apiGroups: - tempo.grafana.com resources: @@ -1010,6 +1062,26 @@ spec: targetPort: 9443 type: MutatingAdmissionWebhook webhookPath: /mutate-tempo-grafana-com-v1alpha1-tempostack + - admissionReviewVersions: + - v1 + containerPort: 443 + deploymentName: tempo-operator-controller + failurePolicy: Fail + generateName: vtempomonolithic.kb.io + rules: + - apiGroups: + - tempo.grafana.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - tempomonolithics + sideEffects: None + targetPort: 9443 + type: ValidatingAdmissionWebhook + webhookPath: /validate-tempo-grafana-com-v1alpha1-tempomonolithic - admissionReviewVersions: - v1 containerPort: 443 diff --git a/bundle/openshift/manifests/tempo.grafana.com_tempomonolithics.yaml b/bundle/openshift/manifests/tempo.grafana.com_tempomonolithics.yaml new file mode 100644 index 000000000..e43b04047 --- /dev/null +++ b/bundle/openshift/manifests/tempo.grafana.com_tempomonolithics.yaml @@ -0,0 +1,210 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + labels: + app.kubernetes.io/managed-by: operator-lifecycle-manager + app.kubernetes.io/name: tempo-operator + app.kubernetes.io/part-of: tempo-operator + name: tempomonolithics.tempo.grafana.com +spec: + group: tempo.grafana.com + names: + kind: TempoMonolithic + listKind: TempoMonolithicList + plural: tempomonolithics + singular: tempomonolithic + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: TempoMonolithic is the Schema for the tempomonolithics API. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: TempoMonolithicSpec defines the desired state of TempoMonolithic. + properties: + extraConfig: + description: ExtraConfig defines any extra (overlay) configuration + for components + properties: + tempo: + description: Tempo defines any extra Tempo configuration, which + will be merged with the operator's generated Tempo configuration + x-kubernetes-preserve-unknown-fields: true + type: object + ingestion: + description: Ingestion defines the trace ingestion configuration + properties: + otlp: + description: OTLP defines the ingestion configuration for OTLP + properties: + grpc: + description: GRPC defines the OTLP/gRPC configuration + properties: + enabled: + default: true + description: Enabled defines if OTLP over gRPC is enabled + type: boolean + required: + - enabled + type: object + http: + description: HTTP defines the OTLP/HTTP configuration + properties: + enabled: + description: Enabled defines if OTLP over HTTP is enabled + type: boolean + required: + - enabled + type: object + type: object + type: object + jaegerui: + description: JaegerUI defines the Jaeger UI configuration + properties: + enabled: + description: Enabled defines if the Jaeger UI should be enabled + type: boolean + ingress: + description: Ingress defines the ingress configuration for Jaeger + UI + properties: + enabled: + description: Enabled defines if an Ingress object should be + created for Jaeger UI + type: boolean + required: + - enabled + type: object + route: + description: Route defines the route configuration for Jaeger + UI + properties: + enabled: + description: Enabled defines if a Route object should be created + for Jaeger UI + type: boolean + required: + - enabled + type: object + required: + - enabled + type: object + management: + description: ManagementState defines whether this instance is managed + by the operator or self-managed + enum: + - Managed + - Unmanaged + type: string + observability: + description: Observability defines observability configuration for + the Tempo deployment + properties: + metrics: + description: Metrics defines the metrics configuration of the + Tempo deployment + properties: + prometheusRules: + description: ServiceMonitors defines the PrometheusRule configuration + properties: + enabled: + description: Enabled defines if the operator should create + PrometheusRules for this Tempo deployment + type: boolean + required: + - enabled + type: object + serviceMonitors: + description: ServiceMonitors defines the ServiceMonitor configuration + properties: + enabled: + description: Enabled defines if the operator should create + ServiceMonitors for this Tempo deployment + type: boolean + required: + - enabled + type: object + type: object + type: object + storage: + description: Storage defines the backend storage configuration + properties: + traces: + description: Traces defines the backend storage configuration + for traces + properties: + backend: + default: memory + description: 'Backend defines the backend for storing traces. + Default: memory' + enum: + - memory + - pv + type: string + pv: + description: PV defines the Persistent Volume configuration + properties: + size: + anyOf: + - type: integer + - type: string + default: 10Gi + description: Size defines the size of the Persistent Volume + for storing the traces. Defaults to 10Gi. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - size + type: object + wal: + description: WAL defines the write-ahead logging (WAL) configuration + properties: + size: + anyOf: + - type: integer + - type: string + default: 10Gi + description: Size defines the size of the Persistent Volume + for storing the WAL. Defaults to 10Gi. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - size + type: object + required: + - backend + type: object + required: + - traces + type: object + type: object + status: + description: TempoMonolithicStatus defines the observed state of TempoMonolithic. + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/bundle/openshift/manifests/tempo.grafana.com_tempostacks.yaml b/bundle/openshift/manifests/tempo.grafana.com_tempostacks.yaml index 5a309fae3..de8d6a55a 100644 --- a/bundle/openshift/manifests/tempo.grafana.com_tempostacks.yaml +++ b/bundle/openshift/manifests/tempo.grafana.com_tempostacks.yaml @@ -59,6 +59,8 @@ spec: defined here has precedence and could override generated config. properties: tempo: + description: Tempo defines any extra Tempo configuration, which + will be merged with the operator's generated Tempo configuration x-kubernetes-preserve-unknown-fields: true type: object hashRing: diff --git a/cmd/start/main.go b/cmd/start/main.go index 83d400b8e..d9bb5b119 100644 --- a/cmd/start/main.go +++ b/cmd/start/main.go @@ -67,12 +67,25 @@ func start(c *cobra.Command, args []string) { os.Exit(1) } + if err = (&controllers.TempoMonolithicReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + CtrlConfig: ctrlConfig, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "TempoMonolithic") + os.Exit(1) + } + enableWebhooks := os.Getenv("ENABLE_WEBHOOKS") != "false" if enableWebhooks { if err = (&tempov1alpha1.TempoStack{}).SetupWebhookWithManager(mgr, ctrlConfig); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "TempoStack") os.Exit(1) } + if err = (&tempov1alpha1.TempoMonolithic{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "TempoMonolithic") + os.Exit(1) + } } //+kubebuilder:scaffold:builder diff --git a/config/crd/bases/tempo.grafana.com_tempomonolithics.yaml b/config/crd/bases/tempo.grafana.com_tempomonolithics.yaml new file mode 100644 index 000000000..f81b4b6c7 --- /dev/null +++ b/config/crd/bases/tempo.grafana.com_tempomonolithics.yaml @@ -0,0 +1,201 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: tempomonolithics.tempo.grafana.com +spec: + group: tempo.grafana.com + names: + kind: TempoMonolithic + listKind: TempoMonolithicList + plural: tempomonolithics + singular: tempomonolithic + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: TempoMonolithic is the Schema for the tempomonolithics API. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: TempoMonolithicSpec defines the desired state of TempoMonolithic. + properties: + extraConfig: + description: ExtraConfig defines any extra (overlay) configuration + for components + properties: + tempo: + description: Tempo defines any extra Tempo configuration, which + will be merged with the operator's generated Tempo configuration + x-kubernetes-preserve-unknown-fields: true + type: object + ingestion: + description: Ingestion defines the trace ingestion configuration + properties: + otlp: + description: OTLP defines the ingestion configuration for OTLP + properties: + grpc: + description: GRPC defines the OTLP/gRPC configuration + properties: + enabled: + default: true + description: Enabled defines if OTLP over gRPC is enabled + type: boolean + required: + - enabled + type: object + http: + description: HTTP defines the OTLP/HTTP configuration + properties: + enabled: + description: Enabled defines if OTLP over HTTP is enabled + type: boolean + required: + - enabled + type: object + type: object + type: object + jaegerui: + description: JaegerUI defines the Jaeger UI configuration + properties: + enabled: + description: Enabled defines if the Jaeger UI should be enabled + type: boolean + ingress: + description: Ingress defines the ingress configuration for Jaeger + UI + properties: + enabled: + description: Enabled defines if an Ingress object should be + created for Jaeger UI + type: boolean + required: + - enabled + type: object + route: + description: Route defines the route configuration for Jaeger + UI + properties: + enabled: + description: Enabled defines if a Route object should be created + for Jaeger UI + type: boolean + required: + - enabled + type: object + required: + - enabled + type: object + management: + description: ManagementState defines whether this instance is managed + by the operator or self-managed + enum: + - Managed + - Unmanaged + type: string + observability: + description: Observability defines observability configuration for + the Tempo deployment + properties: + metrics: + description: Metrics defines the metrics configuration of the + Tempo deployment + properties: + prometheusRules: + description: ServiceMonitors defines the PrometheusRule configuration + properties: + enabled: + description: Enabled defines if the operator should create + PrometheusRules for this Tempo deployment + type: boolean + required: + - enabled + type: object + serviceMonitors: + description: ServiceMonitors defines the ServiceMonitor configuration + properties: + enabled: + description: Enabled defines if the operator should create + ServiceMonitors for this Tempo deployment + type: boolean + required: + - enabled + type: object + type: object + type: object + storage: + description: Storage defines the backend storage configuration + properties: + traces: + description: Traces defines the backend storage configuration + for traces + properties: + backend: + default: memory + description: 'Backend defines the backend for storing traces. + Default: memory' + enum: + - memory + - pv + type: string + pv: + description: PV defines the Persistent Volume configuration + properties: + size: + anyOf: + - type: integer + - type: string + default: 10Gi + description: Size defines the size of the Persistent Volume + for storing the traces. Defaults to 10Gi. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - size + type: object + wal: + description: WAL defines the write-ahead logging (WAL) configuration + properties: + size: + anyOf: + - type: integer + - type: string + default: 10Gi + description: Size defines the size of the Persistent Volume + for storing the WAL. Defaults to 10Gi. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - size + type: object + required: + - backend + type: object + required: + - traces + type: object + type: object + status: + description: TempoMonolithicStatus defines the observed state of TempoMonolithic. + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/tempo.grafana.com_tempostacks.yaml b/config/crd/bases/tempo.grafana.com_tempostacks.yaml index 21a86ff84..a59411931 100644 --- a/config/crd/bases/tempo.grafana.com_tempostacks.yaml +++ b/config/crd/bases/tempo.grafana.com_tempostacks.yaml @@ -56,6 +56,8 @@ spec: defined here has precedence and could override generated config. properties: tempo: + description: Tempo defines any extra Tempo configuration, which + will be merged with the operator's generated Tempo configuration x-kubernetes-preserve-unknown-fields: true type: object hashRing: diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 04bf60698..2eee3e92c 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -3,6 +3,7 @@ # It should be run by config/default resources: - bases/tempo.grafana.com_tempostacks.yaml +- bases/tempo.grafana.com_tempomonolithics.yaml #- bases/config.tempo.grafana.com_projectconfigs.yaml #+kubebuilder:scaffold:crdkustomizeresource diff --git a/config/crd/patches/cainjection_in_tempomonolithics.yaml b/config/crd/patches/cainjection_in_tempomonolithics.yaml new file mode 100644 index 000000000..689bf4436 --- /dev/null +++ b/config/crd/patches/cainjection_in_tempomonolithics.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: tempomonolithics.tempo.grafana.com diff --git a/config/crd/patches/webhook_in_tempomonolithics.yaml b/config/crd/patches/webhook_in_tempomonolithics.yaml new file mode 100644 index 000000000..87d5b6053 --- /dev/null +++ b/config/crd/patches/webhook_in_tempomonolithics.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: tempomonolithics.tempo.grafana.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/manifests/community/bases/tempo-operator.clusterserviceversion.yaml b/config/manifests/community/bases/tempo-operator.clusterserviceversion.yaml index c28b47328..b97879b30 100644 --- a/config/manifests/community/bases/tempo-operator.clusterserviceversion.yaml +++ b/config/manifests/community/bases/tempo-operator.clusterserviceversion.yaml @@ -18,6 +18,16 @@ spec: apiservicedefinitions: {} customresourcedefinitions: owned: + - description: TempoMonolithic is the Schema for the tempomonolithics API. + displayName: Tempo Monolithic + kind: TempoMonolithic + name: tempomonolithics.tempo.grafana.com + specDescriptors: + - description: Tempo defines any extra Tempo configuration, which will be merged + with the operator's generated Tempo configuration + displayName: Tempo Extra Configurations + path: extraConfig.tempo + version: v1alpha1 - description: TempoStack is the spec for Tempo deployments. displayName: TempoStack kind: TempoStack @@ -50,7 +60,9 @@ spec: specDescriptors: - displayName: Extra Configurations path: extraConfig - - displayName: Tempo Extra Configurations + - description: Tempo defines any extra Tempo configuration, which will be merged + with the operator's generated Tempo configuration + displayName: Tempo Extra Configurations path: extraConfig.tempo - description: HashRing defines the spec for the distributed hash ring configuration. displayName: Hash Ring diff --git a/config/manifests/openshift/bases/tempo-operator.clusterserviceversion.yaml b/config/manifests/openshift/bases/tempo-operator.clusterserviceversion.yaml index c28b47328..b97879b30 100644 --- a/config/manifests/openshift/bases/tempo-operator.clusterserviceversion.yaml +++ b/config/manifests/openshift/bases/tempo-operator.clusterserviceversion.yaml @@ -18,6 +18,16 @@ spec: apiservicedefinitions: {} customresourcedefinitions: owned: + - description: TempoMonolithic is the Schema for the tempomonolithics API. + displayName: Tempo Monolithic + kind: TempoMonolithic + name: tempomonolithics.tempo.grafana.com + specDescriptors: + - description: Tempo defines any extra Tempo configuration, which will be merged + with the operator's generated Tempo configuration + displayName: Tempo Extra Configurations + path: extraConfig.tempo + version: v1alpha1 - description: TempoStack is the spec for Tempo deployments. displayName: TempoStack kind: TempoStack @@ -50,7 +60,9 @@ spec: specDescriptors: - displayName: Extra Configurations path: extraConfig - - displayName: Tempo Extra Configurations + - description: Tempo defines any extra Tempo configuration, which will be merged + with the operator's generated Tempo configuration + displayName: Tempo Extra Configurations path: extraConfig.tempo - description: HashRing defines the spec for the distributed hash ring configuration. displayName: Hash Ring diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index e4ebc88bd..7c433e504 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -118,6 +118,32 @@ rules: - list - update - watch +- apiGroups: + - tempo.grafana.com + resources: + - tempomonolithics + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - tempo.grafana.com + resources: + - tempomonolithics/finalizers + verbs: + - update +- apiGroups: + - tempo.grafana.com + resources: + - tempomonolithics/status + verbs: + - get + - patch + - update - apiGroups: - tempo.grafana.com resources: diff --git a/config/samples/community/kustomization.yaml b/config/samples/community/kustomization.yaml index c7b35e7c5..71b5852c7 100644 --- a/config/samples/community/kustomization.yaml +++ b/config/samples/community/kustomization.yaml @@ -1,4 +1,5 @@ ## Append samples you want in your CSV to this file as resources ## resources: - tempo_v1alpha1_tempostack.yaml +- tempo_v1alpha1_tempomonolithic.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/community/tempo_v1alpha1_tempomonolithic.yaml b/config/samples/community/tempo_v1alpha1_tempomonolithic.yaml new file mode 100644 index 000000000..7530f6a54 --- /dev/null +++ b/config/samples/community/tempo_v1alpha1_tempomonolithic.yaml @@ -0,0 +1,8 @@ +apiVersion: tempo.grafana.com/v1alpha1 +kind: TempoMonolithic +metadata: + name: sample +spec: + storage: + traces: + backend: memory diff --git a/config/samples/openshift/kustomization.yaml b/config/samples/openshift/kustomization.yaml index c7b35e7c5..71b5852c7 100644 --- a/config/samples/openshift/kustomization.yaml +++ b/config/samples/openshift/kustomization.yaml @@ -1,4 +1,5 @@ ## Append samples you want in your CSV to this file as resources ## resources: - tempo_v1alpha1_tempostack.yaml +- tempo_v1alpha1_tempomonolithic.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/openshift/tempo_v1alpha1_tempomonolithic.yaml b/config/samples/openshift/tempo_v1alpha1_tempomonolithic.yaml new file mode 100644 index 000000000..7530f6a54 --- /dev/null +++ b/config/samples/openshift/tempo_v1alpha1_tempomonolithic.yaml @@ -0,0 +1,8 @@ +apiVersion: tempo.grafana.com/v1alpha1 +kind: TempoMonolithic +metadata: + name: sample +spec: + storage: + traces: + backend: memory diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index ac8ff9096..b51b201fb 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -32,6 +32,26 @@ metadata: creationTimestamp: null name: validating-webhook-configuration webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-tempo-grafana-com-v1alpha1-tempomonolithic + failurePolicy: Fail + name: vtempomonolithic.kb.io + rules: + - apiGroups: + - tempo.grafana.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - tempomonolithics + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/controllers/tempo/common.go b/controllers/tempo/common.go new file mode 100644 index 000000000..9806ce3db --- /dev/null +++ b/controllers/tempo/common.go @@ -0,0 +1,109 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + + "github.com/go-logr/logr" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/grafana/tempo-operator/internal/manifests" +) + +func isNamespaceScoped(obj client.Object) bool { + switch obj.(type) { + case *rbacv1.ClusterRole, *rbacv1.ClusterRoleBinding: + return false + default: + return true + } +} + +// reconcileManagedObjects creates or updates all managed objects. +// If immutable fields are changed, the object will be deleted and re-created. +func reconcileManagedObjects( + ctx context.Context, + log logr.Logger, + k8sclient client.Client, + owner metav1.Object, + scheme *runtime.Scheme, + managedObjects []client.Object, + ownedObjects map[types.UID]client.Object, +) error { + pruneObjects := ownedObjects + + // Create or update all objects managed by the operator + errs := []error{} + for _, obj := range managedObjects { + l := log.WithValues( + "objectName", obj.GetName(), + "objectKind", obj.GetObjectKind().GroupVersionKind(), + ) + + if isNamespaceScoped(obj) { + if err := ctrl.SetControllerReference(owner, obj, scheme); err != nil { + l.Error(err, "failed to set controller owner reference to resource") + errs = append(errs, err) + continue + } + } + + desired := obj.DeepCopyObject().(client.Object) + mutateFn := manifests.MutateFuncFor(obj, desired) + + var op controllerutil.OperationResult + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + var err error + op, err = ctrl.CreateOrUpdate(ctx, k8sclient, obj, mutateFn) + return err + }) + + var immutableErr *manifests.ImmutableErr + if err != nil && errors.As(err, &immutableErr) { + l.Error(err, "detected a change in an immutable field. The object will be deleted, and re-created on next reconcile", "obj", obj.GetName()) + err = k8sclient.Delete(ctx, desired) + } + + if err != nil { + l.Error(err, "failed to configure resource") + errs = append(errs, err) + } else { + l.V(1).Info(fmt.Sprintf("resource has been %s", op)) + } + + // This object is still managed by the operator, remove it from the list of objects to prune + delete(pruneObjects, obj.GetUID()) + } + if len(errs) > 0 { + return fmt.Errorf("failed to create objects for %s: %w", owner.GetName(), errors.Join(errs...)) + } + + // Prune owned objects in the cluster which are not managed anymore + pruneErrs := []error{} + for _, obj := range pruneObjects { + l := log.WithValues( + "objectName", obj.GetName(), + "objectKind", obj.GetObjectKind(), + ) + + l.Info("pruning unmanaged resource") + err := k8sclient.Delete(ctx, obj) + if err != nil { + l.Error(err, "failed to delete resource") + pruneErrs = append(pruneErrs, err) + } + } + if len(pruneErrs) > 0 { + return fmt.Errorf("failed to prune objects for %s: %w", owner.GetName(), errors.Join(pruneErrs...)) + } + + return nil +} diff --git a/controllers/tempo/tempomonolithic_controller.go b/controllers/tempo/tempomonolithic_controller.go new file mode 100644 index 000000000..0e3492d1d --- /dev/null +++ b/controllers/tempo/tempomonolithic_controller.go @@ -0,0 +1,160 @@ +package controllers + +import ( + "context" + "fmt" + + grafanav1 "github.com/grafana-operator/grafana-operator/v5/api/v1beta1" + routev1 "github.com/openshift/api/route/v1" + monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + configv1alpha1 "github.com/grafana/tempo-operator/apis/config/v1alpha1" + "github.com/grafana/tempo-operator/apis/tempo/v1alpha1" + "github.com/grafana/tempo-operator/internal/manifests/monolithic" +) + +// TempoMonolithicReconciler reconciles a TempoMonolithic object. +type TempoMonolithicReconciler struct { + client.Client + Scheme *runtime.Scheme + CtrlConfig configv1alpha1.ProjectConfig +} + +//+kubebuilder:rbac:groups=tempo.grafana.com,resources=tempomonolithics,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=tempo.grafana.com,resources=tempomonolithics/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=tempo.grafana.com,resources=tempomonolithics/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +func (r *TempoMonolithicReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx).WithName("tempomonolithic-reconcile") + + log.V(1).Info("starting reconcile loop") + defer log.V(1).Info("finished reconcile loop") + + tempo := v1alpha1.TempoMonolithic{} + if err := r.Get(ctx, req.NamespacedName, &tempo); err != nil { + if !apierrors.IsNotFound(err) { + log.Error(err, "unable to fetch TempoMonolithic") + return ctrl.Result{}, fmt.Errorf("could not fetch tempo: %w", err) + } + + // we'll ignore not-found errors, since they can't be fixed by an immediate + // requeue (we'll need to wait for a new notification), and we can get them + // on deleted requests. + return ctrl.Result{}, nil + } + + // apply defaults + tempo.Default() + + if tempo.Spec.Management == v1alpha1.ManagementStateUnmanaged { + log.Info("Skipping reconciliation for unmanaged TempoMonolithic resource", "name", req.String()) + return ctrl.Result{}, nil + } + + managedObjects, err := monolithic.BuildAll(monolithic.Options{ + CtrlConfig: r.CtrlConfig, + Tempo: tempo, + }) + if err != nil { + return ctrl.Result{}, fmt.Errorf("error building manifests: %w", err) + } + + ownedObjects, err := r.getOwnedObjects(ctx, tempo) + if err != nil { + return ctrl.Result{}, err + } + + err = reconcileManagedObjects(ctx, log, r.Client, &tempo, r.Scheme, managedObjects, ownedObjects) + if err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func (r *TempoMonolithicReconciler) getOwnedObjects(ctx context.Context, tempo v1alpha1.TempoMonolithic) (map[types.UID]client.Object, error) { + ownedObjects := map[types.UID]client.Object{} + listOps := &client.ListOptions{ + Namespace: tempo.GetNamespace(), + LabelSelector: labels.SelectorFromSet(monolithic.Labels(tempo.Name)), + } + + // Add all resources where the operator can conditionally create an object. + // For example, Ingress and Route can be enabled or disabled in the CR. + + ingressList := &networkingv1.IngressList{} + err := r.List(ctx, ingressList, listOps) + if err != nil { + return nil, fmt.Errorf("error listing ingress: %w", err) + } + for i := range ingressList.Items { + ownedObjects[ingressList.Items[i].GetUID()] = &ingressList.Items[i] + } + + if r.CtrlConfig.Gates.PrometheusOperator { + servicemonitorList := &monitoringv1.ServiceMonitorList{} + err := r.List(ctx, servicemonitorList, listOps) + if err != nil { + return nil, fmt.Errorf("error listing service monitors: %w", err) + } + for i := range servicemonitorList.Items { + ownedObjects[servicemonitorList.Items[i].GetUID()] = servicemonitorList.Items[i] + } + + prometheusRulesList := &monitoringv1.PrometheusRuleList{} + err = r.List(ctx, prometheusRulesList, listOps) + if err != nil { + return nil, fmt.Errorf("error listing prometheus rules: %w", err) + } + for i := range prometheusRulesList.Items { + ownedObjects[prometheusRulesList.Items[i].GetUID()] = prometheusRulesList.Items[i] + } + } + + if r.CtrlConfig.Gates.OpenShift.OpenShiftRoute { + routesList := &routev1.RouteList{} + err := r.List(ctx, routesList, listOps) + if err != nil { + return nil, fmt.Errorf("error listing routes: %w", err) + } + for i := range routesList.Items { + ownedObjects[routesList.Items[i].GetUID()] = &routesList.Items[i] + } + } + + if r.CtrlConfig.Gates.GrafanaOperator { + datasourceList := &grafanav1.GrafanaDatasourceList{} + err := r.List(ctx, datasourceList, listOps) + if err != nil { + return nil, fmt.Errorf("error listing datasources: %w", err) + } + for i := range datasourceList.Items { + ownedObjects[datasourceList.Items[i].GetUID()] = &datasourceList.Items[i] + } + } + + return ownedObjects, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *TempoMonolithicReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.TempoMonolithic{}). + Owns(&corev1.ConfigMap{}). + Owns(&corev1.Service{}). + Owns(&appsv1.StatefulSet{}). + Owns(&networkingv1.Ingress{}). + Complete(r) +} diff --git a/controllers/tempo/tempomonolithic_controller_test.go b/controllers/tempo/tempomonolithic_controller_test.go new file mode 100644 index 000000000..2114df792 --- /dev/null +++ b/controllers/tempo/tempomonolithic_controller_test.go @@ -0,0 +1,66 @@ +package controllers + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + configv1alpha1 "github.com/grafana/tempo-operator/apis/config/v1alpha1" + "github.com/grafana/tempo-operator/apis/tempo/v1alpha1" +) + +func TestReconcileMonolithic(t *testing.T) { + nsn := types.NamespacedName{Name: "sample", Namespace: "default"} + tempo := &v1alpha1.TempoMonolithic{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsn.Name, + Namespace: nsn.Namespace, + }, + } + err := k8sClient.Create(context.Background(), tempo) + require.NoError(t, err) + + reconciler := TempoMonolithicReconciler{ + Client: k8sClient, + Scheme: testScheme, + CtrlConfig: configv1alpha1.ProjectConfig{}, + } + reconcile, err := reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: nsn}) + require.NoError(t, err) + assert.Equal(t, false, reconcile.Requeue) + + // Check if objects of specific types were created and are managed by the operator + opts := []client.ListOption{ + client.InNamespace(nsn.Namespace), + client.MatchingLabels(map[string]string{ + "app.kubernetes.io/instance": nsn.Name, + "app.kubernetes.io/managed-by": "tempo-operator", + }), + } + { + list := &corev1.ConfigMapList{} + err = k8sClient.List(context.Background(), list, opts...) + assert.NoError(t, err) + assert.NotEmpty(t, list.Items) + } + { + list := &appsv1.StatefulSetList{} + err = k8sClient.List(context.Background(), list, opts...) + assert.NoError(t, err) + assert.NotEmpty(t, list.Items) + } + { + list := &corev1.ServiceList{} + err = k8sClient.List(context.Background(), list, opts...) + assert.NoError(t, err) + assert.NotEmpty(t, list.Items) + } +} diff --git a/controllers/tempo/tempostack_controller.go b/controllers/tempo/tempostack_controller.go index 3c3dd53df..6f0bd87ab 100644 --- a/controllers/tempo/tempostack_controller.go +++ b/controllers/tempo/tempostack_controller.go @@ -72,7 +72,7 @@ func (r *TempoStackReconciler) Reconcile(ctx context.Context, req ctrl.Request) tempo := v1alpha1.TempoStack{} if err := r.Get(ctx, req.NamespacedName, &tempo); err != nil { if !apierrors.IsNotFound(err) { - log.Error(err, "unable to fetch TempoTempoStack") + log.Error(err, "unable to fetch TempoStack") return ctrl.Result{}, fmt.Errorf("could not fetch tempo: %w", err) } @@ -113,7 +113,7 @@ func (r *TempoStackReconciler) Reconcile(ctx context.Context, req ctrl.Request) } } - err := r.createOrUpdate(ctx, log, req, tempo) + err := r.createOrUpdate(ctx, log, tempo) if err != nil { return r.handleReconcileStatus(ctx, log, tempo, err) } diff --git a/controllers/tempo/tempostack_controller_test.go b/controllers/tempo/tempostack_controller_test.go index 823da603e..e8d7138cf 100644 --- a/controllers/tempo/tempostack_controller_test.go +++ b/controllers/tempo/tempostack_controller_test.go @@ -888,8 +888,7 @@ func TestReconcileManifestsValidateModes(t *testing.T) { err := k8sClient.Update(context.Background(), tempo) require.NoError(t, err) reconciler := TempoStackReconciler{Client: k8sClient, Scheme: testScheme} - req := ctrl.Request{NamespacedName: nsn} - err = reconciler.createOrUpdate(context.Background(), logr.Discard(), req, *tempo) + err = reconciler.createOrUpdate(context.Background(), logr.Discard(), *tempo) tc.validate(t, err) }) } diff --git a/controllers/tempo/tempostack_create_or_update.go b/controllers/tempo/tempostack_create_or_update.go index 8b4799786..c2d912187 100644 --- a/controllers/tempo/tempostack_create_or_update.go +++ b/controllers/tempo/tempostack_create_or_update.go @@ -2,7 +2,6 @@ package controllers import ( "context" - "errors" "fmt" "strings" @@ -12,11 +11,9 @@ import ( monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" - rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation/field" - ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/grafana/tempo-operator/apis/tempo/v1alpha1" @@ -75,16 +72,7 @@ func (r *TempoStackReconciler) getStorageConfig(ctx context.Context, tempo v1alp return params, nil } -func isNamespaceScoped(obj client.Object) bool { - switch obj.(type) { - case *rbacv1.ClusterRole, *rbacv1.ClusterRoleBinding: - return false - default: - return true - } -} - -func (r *TempoStackReconciler) createOrUpdate(ctx context.Context, log logr.Logger, req ctrl.Request, tempo v1alpha1.TempoStack) error { +func (r *TempoStackReconciler) createOrUpdate(ctx context.Context, log logr.Logger, tempo v1alpha1.TempoStack) error { storageConfig, err := r.getStorageConfig(ctx, tempo) if err != nil { return &status.ConfigurationError{ @@ -124,15 +112,6 @@ func (r *TempoStackReconciler) createOrUpdate(ctx context.Context, log logr.Logg } - // Collect all objects owned by the operator, to be able to prune objects - // which exist in the cluster but are not managed by the operator anymore. - // For example, when the Jaeger Query Ingress is enabled and later disabled, - // the Ingress object should be removed from the cluster. - pruneObjects, err := r.findObjectsOwnedByTempoOperator(ctx, tempo) - if err != nil { - return err - } - var tenantSecrets []*manifestutils.GatewayTenantOIDCSecret if tempo.Spec.Tenants != nil && tempo.Spec.Tenants.Mode == v1alpha1.ModeStatic { tenantSecrets, err = gateway.GetOIDCTenantSecrets(ctx, r.Client, tempo) @@ -163,59 +142,18 @@ func (r *TempoStackReconciler) createOrUpdate(ctx context.Context, log logr.Logg return fmt.Errorf("error building manifests: %w", err) } - errs := []error{} - for _, obj := range managedObjects { - l := log.WithValues( - "object_name", obj.GetName(), - "object_kind", obj.GetObjectKind(), - ) - - if isNamespaceScoped(obj) { - obj.SetNamespace(req.Namespace) - if err := ctrl.SetControllerReference(&tempo, obj, r.Scheme); err != nil { - l.Error(err, "failed to set controller owner reference to resource") - errs = append(errs, err) - continue - } - } - - desired := obj.DeepCopyObject().(client.Object) - mutateFn := manifests.MutateFuncFor(obj, desired) - - op, err := ctrl.CreateOrUpdate(ctx, r.Client, obj, mutateFn) - if err != nil { - l.Error(err, "failed to configure resource") - errs = append(errs, err) - continue - } - - l.V(1).Info(fmt.Sprintf("resource has been %s", op)) - - // This object is still managed by the operator, remove it from the list of objects to prune - delete(pruneObjects, obj.GetUID()) - } - - if len(errs) > 0 { - return fmt.Errorf("failed to create objects for TempoStack %s: %w", req.NamespacedName, errors.Join(errs...)) + // Collect all objects owned by the operator, to be able to prune objects + // which exist in the cluster but are not managed by the operator anymore. + // For example, when the Jaeger Query Ingress is enabled and later disabled, + // the Ingress object should be removed from the cluster. + ownedObjects, err := r.findObjectsOwnedByTempoOperator(ctx, tempo) + if err != nil { + return err } - // Prune owned objects in the cluster which are not managed anymore. - pruneErrs := []error{} - for _, obj := range pruneObjects { - l := log.WithValues( - "object_name", obj.GetName(), - "object_kind", obj.GetObjectKind(), - ) - l.Info("pruning unmanaged resource") - - err = r.Delete(ctx, obj) - if err != nil { - l.Error(err, "failed to delete resource") - pruneErrs = append(pruneErrs, err) - } - } - if len(pruneErrs) > 0 { - return fmt.Errorf("failed to prune objects of TempoStack %s: %w", req.NamespacedName, errors.Join(pruneErrs...)) + err = reconcileManagedObjects(ctx, log, r.Client, &tempo, r.Scheme, managedObjects, ownedObjects) + if err != nil { + return err } return nil diff --git a/docs/operator/api.md b/docs/operator/api.md index 153c4244f..abf7529a2 100644 --- a/docs/operator/api.md +++ b/docs/operator/api.md @@ -572,7 +572,7 @@ Feature Gates.ProjectConfig
-(Appears on:TempoStackSpec) +(Appears on:TempoMonolithicSpec, TempoStackSpec)
@@ -621,6 +621,8 @@ k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON (Optional) +Tempo defines any extra Tempo configuration, which will be merged with the operator’s generated Tempo configuration
+ @@ -1390,7 +1392,7 @@ RateLimitSpec-(Appears on:TempoStackSpec) +(Appears on:TempoMonolithicSpec, TempoStackSpec)
@@ -1609,17 +1611,17 @@ using an in-process OpenPolicyAgent Rego authorizer. -## OIDCSpec { #tempo-grafana-com-v1alpha1-OIDCSpec } +## MonolithicIngestionOTLPProtocolsGRPCSpec { #tempo-grafana-com-v1alpha1-MonolithicIngestionOTLPProtocolsGRPCSpec }-(Appears on:AuthenticationSpec) +(Appears on:MonolithicIngestionOTLPSpec)
OIDCSpec defines the oidc configuration spec for Tempo Gateway component.
+MonolithicIngestionOTLPProtocolsGRPCSpec defines the settings for OTLP ingestion over GRPC.
secret
enabled
Secret defines the spec for the clientID, clientSecret and issuerCAPath for tenant’s authentication.
+Enabled defines if OTLP over gRPC is enabled
issuerURL
-
+(Appears on:MonolithicIngestionOTLPSpec) -MonolithicIngestionOTLPProtocolsHTTPSpec defines the settings for OTLP ingestion over HTTP.
+ +IssuerURL defines the URL for issuer.
+
-
-redirectURL - - - -string - - - - |
-
-- -(Optional) + | Field | -Description | -
---|---|---|---|
-
-groupClaim - - - -string - - - - |
-
-
-
-(Optional)
-
- Group claim field from ID Token + | ||
-usernameClaim + enabled -string +bool @@ -1751,9 +1711,7 @@ string |
-(Optional)
-
- User claim field from ID Token +Enabled defines if OTLP over HTTP is enabled |
-(Appears on:ObjectStorageSpec) +(Appears on:MonolithicIngestionSpec)
ObjectStorageSecretSpec is a secret reference containing name only, no namespace.
+MonolithicIngestionOTLPSpec defines the settings for OTLP ingestion.
type
grpc
Type of object storage that should be used
+GRPC defines the OTLP/gRPC configuration
name
http
Name of a secret in the namespace configured for object storage secrets.
+HTTP defines the OTLP/HTTP configuration
string
alias)
+## MonolithicIngestionSpec { #tempo-grafana-com-v1alpha1-MonolithicIngestionSpec }
-(Appears on:ObjectStorageSecretSpec) +(Appears on:TempoMonolithicSpec)
ObjectStorageSecretType defines the type of storage which can be used with the Tempo cluster.
+MonolithicIngestionSpec defines the ingestion settings.
"azure"
ObjectStorageSecretAzure when using Azure Storage for Tempo storage.
-"gcs"
otlp
ObjectStorageSecretGCS when using Google Cloud Storage for Tempo storage.
"s3"
OTLP defines the ingestion configuration for OTLP
-ObjectStorageSecretS3 when using S3 for Tempo storage.
-(Appears on:TempoStackSpec) +(Appears on:MonolithicJaegerUISpec)
ObjectStorageSpec defines the requirements to access the object -storage bucket to persist traces by the ingester component.
+MonolithicJaegerUIIngressSpec defines the settings for the Jaeger UI ingress.
tls
TLS configuration for reaching the object storage endpoint.
- -secret
enabled
Secret for object storage authentication. -Name of a secret in the same namespace as the TempoStack custom resource.
+Enabled defines if an Ingress object should be created for Jaeger UI
-(Appears on:ObjectStorageSpec) +(Appears on:MonolithicJaegerUISpec)
ObjectStorageTLSSpec is the TLS configuration for reaching the object storage endpoint.
+MonolithicJaegerUIRouteSpec defines the settings for the Jaeger UI route.
caName
enabled
CA is the name of a ConfigMap containing a ca.crt
key with a CA certificate.
-It needs to be in the same namespace as the TempoStack custom resource.
Enabled defines if a Route object should be created for Jaeger UI
-(Appears on:TempoStackSpec) +(Appears on:TempoMonolithicSpec)
ObservabilitySpec defines how telemetry data gets handled.
+MonolithicJaegerUISpec defines the settings for the Jaeger UI.
metrics
enabled
Metrics defines the metrics configuration for operands.
+Enabled defines if the Jaeger UI should be enabled
tracing
ingress
Tracing defines a config for operands.
+Ingress defines the ingress configuration for Jaeger UI
grafana
route
Grafana defines the Grafana configuration for operands.
+Route defines the route configuration for Jaeger UI
string
alias)
+## MonolithicObservabilityMetricsPrometheusRulesSpec { #tempo-grafana-com-v1alpha1-MonolithicObservabilityMetricsPrometheusRulesSpec }
-(Appears on:RoleSpec) +(Appears on:MonolithicObservabilityMetricsSpec)
PermissionType is a Tempo Gateway RBAC permission.
+MonolithicObservabilityMetricsPrometheusRulesSpec defines the PrometheusRules settings.
"read"
Read gives access to read data from a tenant.
-"write"
Write gives access to write data to a tenant.
-enabled
map[k8s.io/api/core/v1.PodPhase][]string
alias)
+
-+
Enabled defines if the operator should create PrometheusRules for this Tempo deployment
-PodStatusMap defines the type for mapping pod status to pod name.
- - + + -## QueryLimit { #tempo-grafana-com-v1alpha1-QueryLimit } +## MonolithicObservabilityMetricsServiceMonitorsSpec { #tempo-grafana-com-v1alpha1-MonolithicObservabilityMetricsServiceMonitorsSpec }-(Appears on:RateLimitSpec) +(Appears on:MonolithicObservabilityMetricsSpec)
QueryLimit defines query limits.
+MonolithicObservabilityMetricsServiceMonitorsSpec defines the ServiceMonitor settings.
maxBytesPerTagValues
enabled
MaxBytesPerTagValues defines the maximum size in bytes of a tag-values query.
- -maxSearchBytesPerTrace
DEPRECATED. MaxSearchBytesPerTrace defines the maximum size of search data for a single
-trace in bytes.
-default: 0
to disable.
maxSearchDuration
MaxSearchDuration defines the maximum allowed time range for a search. -If this value is not set, then spec.search.maxDuration is used.
+Enabled defines if the operator should create ServiceMonitors for this Tempo deployment
-(Appears on:LimitSpec) +(Appears on:MonolithicObservabilitySpec)
RateLimitSpec defines rate limits for Ingestion and Query components.
+MonolithicObservabilityMetricsSpec defines the metrics settings of the Tempo deployment.
ingestion
serviceMonitors
Ingestion is used to define ingestion rate limits.
+ServiceMonitors defines the ServiceMonitor configuration
query
prometheusRules
Query is used to define query rate limits.
+ServiceMonitors defines the PrometheusRule configuration
-(Appears on:TempoDistributorSpec) +(Appears on:TempoMonolithicSpec)
ReceiversTLSSpec is the TLS configuration for the receivers.
+MonolithicObservabilitySpec defines the observability settings of the Tempo deployment.
enabled
caName
caName is the name of a ConfigMap containing a CA certificate. -It needs to be in the same namespace as the Tempo custom resource.
- -certName
metrics
certName is the name of a Secret containing a certificate and the private key -It needs to be in the same namespace as the Tempo custom resource.
- -minVersion
minVersion is the name of a Secret containing a certificate and the private key -It needs to be in the same namespace as the Tempo custom resource.
+Metrics defines the metrics configuration of the Tempo deployment
-(Appears on:TempoStackSpec) +(Appears on:TempoMonolithicSpec)
Resources defines resources configuration.
+MonolithicStorageSpec defines the storage for the Tempo deployment.
total
traces
The total amount of resources for Tempo instance. -The operator autonomously splits resources between deployed Tempo components. -Only limits are supported, the operator calculates requests automatically. -See http://github.com/grafana/tempo/issues/1540.
+Traces defines the backend storage configuration for traces
string
alias)
-(Appears on:RetentionSpec) +(Appears on:MonolithicTracesStorageSpec)
RetentionConfig defines how long data should be provided.
+MonolithicTracesStorageBackend defines the backend storage for traces.
traces
"memory"
MonolithicTracesStorageBackendMemory defines storing traces in a tmpfs (in-memory filesystem).
Traces defines retention period. Supported parameter suffixes are “s”, “m” and “h”. -example: 336h -default: value is 48h.
+"pv"
MonolithicTracesStorageBackendPV defines storing traces in a Persistent Volume.
-(Appears on:TempoStackSpec) +(Appears on:MonolithicTracesStorageSpec)
RetentionSpec defines global and per tenant retention configurations.
+MonolithicTracesStoragePVSpec defines the Persistent Volume configuration.
perTenant
PerTenant is used to configure retention per tenant.
- -global
size
Global is used to configure global retention.
+Size defines the size of the Persistent Volume for storing the traces. Defaults to 10Gi.
-(Appears on:AuthorizationSpec) +(Appears on:MonolithicStorageSpec)
RoleBindingsSpec binds a set of roles to a set of subjects.
+MonolithicTracesStorageSpec defines the traces storage for the Tempo deployment.
name
backend
Backend defines the backend for storing traces. Default: memory
+subjects
wal
WAL defines the write-ahead logging (WAL) configuration
+roles
pv
PV defines the Persistent Volume configuration
+-(Appears on:AuthorizationSpec) +(Appears on:MonolithicTracesStorageSpec)
RoleSpec describes a set of permissions to interact with a tenant.
+MonolithicTracesStorageWALSpec defines the write-ahead logging (WAL) configuration.
name
size
Size defines the size of the Persistent Volume for storing the WAL. Defaults to 10Gi.
++ +(Appears on:AuthenticationSpec) + +
+ +OIDCSpec defines the oidc configuration spec for Tempo Gateway component.
+ +Field | + +Description | + +
---|---|
-resources + secret -[]string + + +TenantSecretSpec + + @@ -2892,6 +2688,10 @@ string |
+(Optional)
+
+ Secret defines the spec for the clientID, clientSecret and issuerCAPath for tenant’s authentication. + |
-tenants + issuerURL -[]string +string @@ -2911,6 +2711,10 @@ string |
+(Optional)
+
+ IssuerURL defines the URL for issuer. + |
@@ -2918,15 +2722,57 @@ string
-permissions + redirectURL - +string -[]PermissionType + - + |
+
+
+
+(Optional)
+
+ RedirectURL defines the URL for redirect. + + |
+
+
+
+
+groupClaim + + + +string + + + + |
+
+
+
+(Optional)
+
+ Group claim field from ID Token + + |
+
+
+usernameClaim + + + +string @@ -2934,23 +2780,27 @@ string |
+(Optional)
+
+ User claim field from ID Token + |
-(Appears on:IngressSpec) +(Appears on:ObjectStorageSpec)
RouteSpec defines OpenShift Route specific options.
+ObjectStorageSecretSpec is a secret reference containing name only, no namespace.
termination
type
Type of object storage that should be used
-Termination specifies the termination type. By default “edge” is used.
+name
Name of a secret in the namespace configured for object storage secrets.
string
alias)
+
++ +(Appears on:ObjectStorageSecretSpec) + +
+ +ObjectStorageSecretType defines the type of storage which can be used with the Tempo cluster.
+ +Value | + +Description | + +
---|---|
"azure" |
+
+ObjectStorageSecretAzure when using Azure Storage for Tempo storage. + |
+
+
"gcs" |
+
+ObjectStorageSecretGCS when using Google Cloud Storage for Tempo storage. + |
+
+
"s3" |
+
+ObjectStorageSecretS3 when using S3 for Tempo storage. + |
+
+
@@ -3010,7 +2927,8 @@ TLSRouteTerminationType
SearchSpec specified the global search parameters.
+ObjectStorageSpec defines the requirements to access the object +storage bucket to persist traces by the ingester component.
defaultResultLimit
tls
Limit used for search requests if none is set by the caller (default: 20)
+TLS configuration for reaching the object storage endpoint.
maxDuration
secret
The maximum allowed time range for a search, default: 0s which means unlimited.
+Secret for object storage authentication. +Name of a secret in the same namespace as the TempoStack custom resource.
+ +(Appears on:ObjectStorageSpec) + +
+ +ObjectStorageTLSSpec is the TLS configuration for reaching the object storage endpoint.
+ +Field | + +Description | + +
---|---|
-maxResultLimit + caName -int +string + + + + |
+
+
+
+(Optional)
+
+ CA is the name of a ConfigMap containing a |
+
+ +(Appears on:TempoStackSpec) + +
+ +ObservabilitySpec defines how telemetry data gets handled.
+ +Field | + +Description | + +
---|---|
+
+metrics + + + + + +MetricsConfigSpec + + + + + + |
+
+
+
+(Optional)
+
+ Metrics defines the metrics configuration for operands. + + |
+
+
+tracing + + + + + +TracingConfigSpec + + + + + + |
+
+
+
+(Optional)
+
+ Tracing defines a config for operands. + + |
+
+
+grafana + + + + + +GrafanaConfigSpec + + + + + + |
+
+
+
+(Optional)
+
+ Grafana defines the Grafana configuration for operands. + + |
+
string
alias)
+
++ +(Appears on:RoleSpec) + +
+ +PermissionType is a Tempo Gateway RBAC permission.
+ +Value | + +Description | + +
---|---|
"read" |
+
+Read gives access to read data from a tenant. + |
+
+
"write" |
+
+Write gives access to write data to a tenant. + |
+
+
map[k8s.io/api/core/v1.PodPhase][]string
alias)
+
++ +(Appears on:ComponentStatus) + +
+ +PodStatusMap defines the type for mapping pod status to pod name.
+ ++ +(Appears on:RateLimitSpec) + +
+ +QueryLimit defines query limits.
+ +Field | + +Description | + +
---|---|
+
+maxBytesPerTagValues + + + +int + + + + |
+
+
+
+(Optional)
+
+ MaxBytesPerTagValues defines the maximum size in bytes of a tag-values query. + + |
+
+
+maxSearchBytesPerTrace + + + +int + + + + |
+
+
+
+(Optional)
+
+ DEPRECATED. MaxSearchBytesPerTrace defines the maximum size of search data for a single
+trace in bytes.
+default: |
+
+
+maxSearchDuration + + + + + +Kubernetes meta/v1.Duration + + + + + + |
+
+
+
+(Optional)
+
+ MaxSearchDuration defines the maximum allowed time range for a search. +If this value is not set, then spec.search.maxDuration is used. + + |
+
+ +(Appears on:LimitSpec) + +
+ +RateLimitSpec defines rate limits for Ingestion and Query components.
+ +Field | + +Description | + +
---|---|
+
+ingestion + + + + + +IngestionLimitSpec + + + + + + |
+
+
+
+(Optional)
+
+ Ingestion is used to define ingestion rate limits. + + |
+
+
+query + + + + + +QueryLimit + + + + + + |
+
+
+
+(Optional)
+
+ Query is used to define query rate limits. + + |
+
+ +(Appears on:TempoDistributorSpec) + +
+ +ReceiversTLSSpec is the TLS configuration for the receivers.
+ +Field | + +Description | + +
---|---|
+
+enabled + + + +bool + + + + |
+
++ + | +
+
+caName + + + +string + + + + |
+
+
+
+ caName is the name of a ConfigMap containing a CA certificate. +It needs to be in the same namespace as the Tempo custom resource. + + |
+
+
+certName + + + +string + + + + |
+
+
+
+ certName is the name of a Secret containing a certificate and the private key +It needs to be in the same namespace as the Tempo custom resource. + + |
+
+
+minVersion + + + +string + + + + |
+
+
+
+(Optional)
+
+ minVersion is the name of a Secret containing a certificate and the private key +It needs to be in the same namespace as the Tempo custom resource. + + |
+
+ +(Appears on:TempoStackSpec) + +
+ +Resources defines resources configuration.
+ +Field | + +Description | + +
---|---|
+
+total + + + + + +Kubernetes core/v1.ResourceRequirements + + + + + + |
+
+
+
+(Optional)
+
+ The total amount of resources for Tempo instance. +The operator autonomously splits resources between deployed Tempo components. +Only limits are supported, the operator calculates requests automatically. +See http://github.com/grafana/tempo/issues/1540. + + |
+
+ +(Appears on:RetentionSpec) + +
+ +RetentionConfig defines how long data should be provided.
+ +Field | + +Description | + +
---|---|
+
+traces + + + + + +Kubernetes meta/v1.Duration + + + + + + |
+
+
+
+(Optional)
+
+ Traces defines retention period. Supported parameter suffixes are “s”, “m” and “h”. +example: 336h +default: value is 48h. + + |
+
+ +(Appears on:TempoStackSpec) + +
+ +RetentionSpec defines global and per tenant retention configurations.
+ +Field | + +Description | + +
---|---|
+
+perTenant + + + + + +map[string]github.com/grafana/tempo-operator/apis/tempo/v1alpha1.RetentionConfig + + + + + + |
+
+
+
+(Optional)
+
+ PerTenant is used to configure retention per tenant. + + |
+
+
+global + + + + + +RetentionConfig + + + + + + |
+
+
+
+(Optional)
+
+ Global is used to configure global retention. + + |
+
+ +(Appears on:AuthorizationSpec) + +
+ +RoleBindingsSpec binds a set of roles to a set of subjects.
+ +Field | + +Description | + +
---|---|
+
+name + + + +string + + + + |
+
++ + | +
+
+subjects + + + + + +[]Subject + + + + + + |
+
++ + | +
+
+roles + + + +[]string + + + + |
+
++ + | +
+ +(Appears on:AuthorizationSpec) + +
+ +RoleSpec describes a set of permissions to interact with a tenant.
+ +Field | + +Description | + +
---|---|
+
+name + + + +string + + + + |
+
++ + | +
+
+resources + + + +[]string + + + + |
+
++ + | +
+
+tenants + + + +[]string + + + + |
+
++ + | +
+
+permissions + + + + + +[]PermissionType + + + + + + |
+
++ + | +
+ +(Appears on:IngressSpec) + +
+ +RouteSpec defines OpenShift Route specific options.
+ +Field | + +Description | + +
---|---|
+
+termination + + + + + +TLSRouteTerminationType + + + + + + |
+
+
+
+(Optional)
+
+ Termination specifies the termination type. By default “edge” is used. + + |
+
+ +(Appears on:TempoStackSpec) + +
+ +SearchSpec specified the global search parameters.
+ +Field | + +Description | + +
---|---|
+
+defaultResultLimit + + + +int + + + + |
+
+
+
+(Optional)
+
+ Limit used for search requests if none is set by the caller (default: 20) + + |
+
+
+maxDuration + + + + + +Kubernetes meta/v1.Duration + + + + + + |
+
+
+
+(Optional)
+
+ The maximum allowed time range for a search, default: 0s which means unlimited. + + |
+
+
+maxResultLimit + + + +int + + + + |
+
+
+
+(Optional)
+
+ The maximum allowed value of the limit parameter on search requests. If the search request limit parameter +exceeds the value configured here it will be set to the value configured here. +The default value of 0 disables this limit. + + |
+
+ +(Appears on:RoleBindingsSpec) + +
+ +Subject represents a subject that has been bound to a role.
+ +Field | + +Description | + +
---|---|
+
+name + + + +string + + + + |
+
++ + | +
+
+kind + + + + + +SubjectKind + + + + + + |
+
++ + | +
string
alias)
+
++ +(Appears on:Subject) + +
+ +SubjectKind is a kind of Tempo Gateway RBAC subject.
+ +Value | + +Description | + +
---|---|
"group" |
+
+Group represents a subject that is a group. + |
+
+
"user" |
+
+User represents a subject that is a user. + |
+
+
string
alias)
+
++ +(Appears on:RouteSpec) + +
+ +TLSRouteTerminationType is used to indicate which TLS settings should be used.
+ +Value | + +Description | + +
---|---|
"edge" |
+
+TLSRouteTerminationTypeEdge indicates that encryption should be terminated +at the edge router. + |
+
+
"insecure" |
+
+TLSRouteTerminationTypeInsecure indicates that insecure connections are allowed. + |
+
+
"passthrough" |
+
+TLSRouteTerminationTypePassthrough indicates that the destination service is +responsible for decrypting traffic. + |
+
+
"reencrypt" |
+
+TLSRouteTerminationTypeReencrypt indicates that traffic will be decrypted on the edge +and re-encrypt using a new certificate. + |
+
+
"passthrough" |
+
++ + |
"edge" |
+
++ + |
+ +(Appears on:TempoDistributorSpec, TempoGatewaySpec, TempoQueryFrontendSpec, TempoTemplateSpec) + +
+ +TempoComponentSpec defines specific schedule settings for tempo components.
+ +Field | + +Description | + +
---|---|
+
+replicas + + + +int32 + + + + |
+
+
+
+(Optional)
+
+ Replicas represents the number of replicas to create for this component. + + |
+
+
+nodeSelector + + + +map[string]string + + + + |
+
+
+
+(Optional)
+
+ NodeSelector is the simplest recommended form of node selection constraint. + + |
+
+
+tolerations + + + + + +[]Kubernetes core/v1.Toleration + + @@ -3098,9 +4417,7 @@ int (Optional) - The maximum allowed value of the limit parameter on search requests. If the search request limit parameter -exceeds the value configured here it will be set to the value configured here. -The default value of 0 disables this limit. +Tolerations defines component specific pod tolerations. |
-(Appears on:RoleBindingsSpec) +(Appears on:TempoTemplateSpec)
Subject represents a subject that has been bound to a role.
+TempoDistributorSpec defines the template of all requirements to configure +scheduling of Tempo distributor component to be deployed.
name
component
TempoComponentSpec is embedded to extend this definition with further options.
+ +Currently, there is no way to inline this field. +See: https://github.com/golang/go/issues/6213
+kind
tls
TLS defines TLS configuration for distributor receivers
+string
alias)
+## TempoGatewaySpec { #tempo-grafana-com-v1alpha1-TempoGatewaySpec }
-(Appears on:Subject) +(Appears on:TempoTemplateSpec)
SubjectKind is a kind of Tempo Gateway RBAC subject.
+TempoGatewaySpec extends TempoComponentSpec with gateway parameters.
"group"
Group represents a subject that is a group.
-"user"
User represents a subject that is a user.
-component
string
alias)
+TempoComponentSpec
-+
-(Appears on:RouteSpec) + - +TLSRouteTerminationType is used to indicate which TLS settings should be used.
- - +(Optional) -Value | +- | Description | +
---|---|---|
"edge" |
+
-TLSRouteTerminationTypeEdge indicates that encryption should be terminated -at the edge router. |
-|
"insecure" |
+- | TLSRouteTerminationTypeInsecure indicates that insecure connections are allowed. |
+
"passthrough" |
+||
TLSRouteTerminationTypePassthrough indicates that the destination service is -responsible for decrypting traffic. - |
+- | |
"reencrypt" |
+TLSRouteTerminationTypeReencrypt indicates that traffic will be decrypted on the edge -and re-encrypt using a new certificate. - |
+
-|
"passthrough" |
+
-+IngressSpec - | |
"edge" |
+
-+ - |
+(Optional) -(Appears on:TempoDistributorSpec, TempoGatewaySpec, TempoQueryFrontendSpec, TempoTemplateSpec) +
Ingress defines gateway Ingress options.
- +TempoComponentSpec defines specific schedule settings for tempo components.
+TempoMonolithic is the Schema for the tempomonolithics API.
replicas
metadata
Replicas represents the number of replicas to create for this component.
+metadata
field.
nodeSelector
spec
NodeSelector is the simplest recommended form of node selection constraint.
-tolerations
status
Tolerations defines component specific pod tolerations.
--(Appears on:TempoTemplateSpec) +(Appears on:TempoMonolithic)
TempoDistributorSpec defines the template of all requirements to configure -scheduling of Tempo distributor component to be deployed.
+TempoMonolithicSpec defines the desired state of TempoMonolithic.
component
storage
TempoComponentSpec is embedded to extend this definition with further options.
- -Currently, there is no way to inline this field. -See: https://github.com/golang/go/issues/6213
+Storage defines the backend storage configuration
tls
ingestion
TLS defines TLS configuration for distributor receivers
+Ingestion defines the trace ingestion configuration
+
jaegerui
Field | +- | Description | +
---|---|---|
-component + management - + -TempoComponentSpec +ManagementStateType @@ -3537,12 +4850,7 @@ TempoComponentSpec
-(Optional)
-
- |
TempoComponentSpec is embedded to extend this definition with further options. - -Currently there is no way to inline this field. -See: https://github.com/golang/go/issues/6213 +ManagementState defines whether this instance is managed by the operator or self-managed |
-enabled + observability -bool + + +MonolithicObservabilitySpec + + @@ -3563,6 +4875,8 @@ bool |
+ Observability defines observability configuration for the Tempo deployment + |
@@ -3570,13 +4884,13 @@ bool
-ingress + extraConfig - + -IngressSpec +ExtraConfigSpec @@ -3586,9 +4900,7 @@ IngressSpec
-(Optional)
-
- |
@@ -3596,6 +4908,20 @@ IngressSpec
Ingress defines gateway Ingress options. +ExtraConfig defines any extra (overlay) configuration for components |
+ +(Appears on:TempoMonolithic) + +
+ +TempoMonolithicStatus defines the observed state of TempoMonolithic.
+ +diff --git a/docs/spec/tempo.grafana.com_tempomonolithics.yaml b/docs/spec/tempo.grafana.com_tempomonolithics.yaml new file mode 100644 index 000000000..f3c18340f --- /dev/null +++ b/docs/spec/tempo.grafana.com_tempomonolithics.yaml @@ -0,0 +1,34 @@ +apiVersion: tempo.grafana.com/v1alpha1 # APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources +kind: TempoMonolithic # Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds +metadata: + name: example +spec: # TempoMonolithicSpec defines the desired state of TempoMonolithic. + extraConfig: # ExtraConfig defines any extra (overlay) configuration for components + tempo: {} # Tempo defines any extra Tempo configuration, which will be merged with the operator's generated Tempo configuration + ingestion: # Ingestion defines the trace ingestion configuration + otlp: # OTLP defines the ingestion configuration for OTLP + grpc: # GRPC defines the OTLP/gRPC configuration + enabled: true # Enabled defines if OTLP over gRPC is enabled + http: # HTTP defines the OTLP/HTTP configuration + enabled: false # Enabled defines if OTLP over HTTP is enabled + jaegerui: # JaegerUI defines the Jaeger UI configuration + enabled: false # Enabled defines if the Jaeger UI should be enabled + ingress: # Ingress defines the ingress configuration for Jaeger UI + enabled: false # Enabled defines if an Ingress object should be created for Jaeger UI + route: # Route defines the route configuration for Jaeger UI + enabled: false # Enabled defines if a Route object should be created for Jaeger UI + management: "" # ManagementState defines whether this instance is managed by the operator or self-managed + observability: # Observability defines observability configuration for the Tempo deployment + metrics: # Metrics defines the metrics configuration of the Tempo deployment + prometheusRules: # ServiceMonitors defines the PrometheusRule configuration + enabled: false # Enabled defines if the operator should create PrometheusRules for this Tempo deployment + serviceMonitors: # ServiceMonitors defines the ServiceMonitor configuration + enabled: false # Enabled defines if the operator should create ServiceMonitors for this Tempo deployment + storage: # Storage defines the backend storage configuration + traces: # Traces defines the backend storage configuration for traces + backend: "memory" # Backend defines the backend for storing traces. Default: memory + pv: # PV defines the Persistent Volume configuration + size: "10Gi" # Size defines the size of the Persistent Volume for storing the traces. Defaults to 10Gi. + wal: # WAL defines the write-ahead logging (WAL) configuration + size: "10Gi" # Size defines the size of the Persistent Volume for storing the WAL. Defaults to 10Gi. +status: # TempoMonolithicStatus defines the observed state of TempoMonolithic. diff --git a/docs/spec/tempo.grafana.com_tempostacks.yaml b/docs/spec/tempo.grafana.com_tempostacks.yaml index a2d94299b..cfbbe0631 100644 --- a/docs/spec/tempo.grafana.com_tempostacks.yaml +++ b/docs/spec/tempo.grafana.com_tempostacks.yaml @@ -4,7 +4,7 @@ metadata: name: example spec: # TempoStackSpec defines the desired state of TempoStack. extraConfig: # ExtraConfigSpec defines extra configurations for tempo that will be merged with the operator generated, configurations defined here has precedence and could override generated config. - tempo: {} + tempo: {} # Tempo defines any extra Tempo configuration, which will be merged with the operator's generated Tempo configuration hashRing: # HashRing defines the spec for the distributed hash ring configuration. memberlist: # MemberList configuration spec enableIPv6: false # EnableIPv6 enables IPv6 support for the memberlist based hash ring. diff --git a/go.mod b/go.mod index c96c425ea..faf9f57d0 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/ViaQ/logerr/v2 v2.1.0 github.com/go-logr/logr v1.4.1 github.com/go-logr/zapr v1.3.0 + github.com/google/go-cmp v0.6.0 github.com/grafana-operator/grafana-operator/v5 v5.5.2 github.com/imdario/mergo v0.3.16 github.com/novln/docker-parser v1.0.0 @@ -53,7 +54,6 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20230510103437-eeec1cb781c3 // indirect github.com/google/uuid v1.3.1 // indirect diff --git a/internal/manifests/alerts/prometheus.go b/internal/manifests/alerts/prometheus.go index 34e1fc28d..86a16f3b7 100644 --- a/internal/manifests/alerts/prometheus.go +++ b/internal/manifests/alerts/prometheus.go @@ -44,7 +44,8 @@ func newPrometheusRule(stackName, namespace string) (*monitoringv1.PrometheusRul }, ObjectMeta: metav1.ObjectMeta{ - Name: naming.PrometheusRuleName(stackName), + Name: naming.PrometheusRuleName(stackName), + Namespace: namespace, Labels: map[string]string{ "openshift.io/prometheus-rule-evaluation-scope": "leaf-prometheus", }, diff --git a/internal/manifests/config/configmap.go b/internal/manifests/config/configmap.go index d3c0187c5..1d9c75017 100644 --- a/internal/manifests/config/configmap.go +++ b/internal/manifests/config/configmap.go @@ -40,13 +40,13 @@ func BuildConfigMap(params manifestutils.Params) (*corev1.ConfigMap, string, err if params.Tempo.Spec.ExtraConfig != nil { // For we only support tempo for now. - config, err = mergeExtraConfigWithConfig(params.Tempo.Spec.ExtraConfig.Tempo, config) + config, err = MergeExtraConfigWithConfig(params.Tempo.Spec.ExtraConfig.Tempo, config) if err != nil { return nil, "", err } // Is the same tempo config with certain TLS fields disabled. - frontendConfig, err = mergeExtraConfigWithConfig(params.Tempo.Spec.ExtraConfig.Tempo, frontendConfig) + frontendConfig, err = MergeExtraConfigWithConfig(params.Tempo.Spec.ExtraConfig.Tempo, frontendConfig) if err != nil { return nil, "", err } @@ -55,8 +55,9 @@ func BuildConfigMap(params manifestutils.Params) (*corev1.ConfigMap, string, err labels := manifestutils.ComponentLabels("config", tempo.Name) configMap := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: naming.Name("", tempo.Name), - Labels: labels, + Name: naming.Name("", tempo.Name), + Namespace: tempo.Namespace, + Labels: labels, }, Data: map[string]string{ tempoConfigKey: string(config), diff --git a/internal/manifests/config/extra.go b/internal/manifests/config/extra.go index 9a14d7272..f80c25c0f 100644 --- a/internal/manifests/config/extra.go +++ b/internal/manifests/config/extra.go @@ -9,7 +9,9 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" ) -func mergeExtraConfigWithConfig(overridesJSON apiextensionsv1.JSON, templateResults []byte) ([]byte, error) { +// MergeExtraConfigWithConfig overlays configuration from overridesJSON onto templateResults. +func MergeExtraConfigWithConfig(overridesJSON apiextensionsv1.JSON, templateResults []byte) ([]byte, error) { + // mergo.Merge requires that both variables have the same type renderedTemplateMap := make(map[string]interface{}) overrides := make(map[string]interface{}) @@ -17,6 +19,7 @@ func mergeExtraConfigWithConfig(overridesJSON apiextensionsv1.JSON, templateResu return templateResults, nil } + // Unmarshal overlay of type []byte to map[string]interface{} if err := json.Unmarshal(overridesJSON.Raw, &overrides); err != nil { return nil, err } @@ -25,6 +28,7 @@ func mergeExtraConfigWithConfig(overridesJSON apiextensionsv1.JSON, templateResu return nil, err } + // Override generated config with extra config if err := mergo.Merge(&renderedTemplateMap, overrides, mergo.WithOverride); err != nil { return nil, err } diff --git a/internal/manifests/config/extra_test.go b/internal/manifests/config/extra_test.go index 3a74b745d..f2d9e3800 100644 --- a/internal/manifests/config/extra_test.go +++ b/internal/manifests/config/extra_test.go @@ -55,7 +55,7 @@ server: extraConfig := apiextensionsv1.JSON{Raw: raw} - result, err := mergeExtraConfigWithConfig(extraConfig, []byte(input)) + result, err := MergeExtraConfigWithConfig(extraConfig, []byte(input)) require.NoError(t, err) require.YAMLEq(t, expCfg, string(result)) @@ -77,7 +77,7 @@ storage: ` extraConfig := apiextensionsv1.JSON{} - result, err := mergeExtraConfigWithConfig(extraConfig, []byte(input)) + result, err := MergeExtraConfigWithConfig(extraConfig, []byte(input)) require.NoError(t, err) require.YAMLEq(t, input, string(result)) } @@ -100,6 +100,6 @@ storage: Raw: []byte("{{{{}"), } - _, err := mergeExtraConfigWithConfig(extraConfig, []byte(input)) + _, err := MergeExtraConfigWithConfig(extraConfig, []byte(input)) require.Error(t, err) } diff --git a/internal/manifests/gateway/openshift.go b/internal/manifests/gateway/openshift.go index c85f18857..1ff5b9092 100644 --- a/internal/manifests/gateway/openshift.go +++ b/internal/manifests/gateway/openshift.go @@ -126,6 +126,7 @@ func configMapCABundle(tempo v1alpha1.TempoStack) *corev1.ConfigMap { return &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: naming.Name("gateway-cabundle", tempo.Name), + Namespace: tempo.Namespace, Labels: manifestutils.ComponentLabels(manifestutils.GatewayComponentName, tempo.Name), Annotations: map[string]string{"service.beta.openshift.io/inject-cabundle": "true"}, }, diff --git a/internal/manifests/manifestutils/constants.go b/internal/manifests/manifestutils/constants.go index 90c7e5d80..481d915eb 100644 --- a/internal/manifests/manifestutils/constants.go +++ b/internal/manifests/manifestutils/constants.go @@ -37,6 +37,21 @@ const ( // PortGRPCServer declares the port number of the tempo gRPC port. PortGRPCServer = 9095 + // JaegerUIPortName declares the name of the Jaeger UI HTTP port. + JaegerUIPortName = "jaeger-ui" + // PortJaegerUI declares the port number of the Jaeger UI HTTP port. + PortJaegerUI = 16686 + + // JaegerGRPCQuery declares the name of the Jaeger UI gPRC port. + JaegerGRPCQuery = "jaeger-grpc" + // PortJaegerGRPCQuery declares the port number of the Jaeger UI gPRC port. + PortJaegerGRPCQuery = 16685 + + // JaegerMetricsPortName declares the name of the Jaeger UI metrics port. + JaegerMetricsPortName = "jaeger-metrics" + // PortJaegerMetrics declares the port number of the Jaeger UI metrics port. + PortJaegerMetrics = 16687 + // OtlpGrpcPortName declares the name of the OpenTelemetry Collector gRPC receiver port. OtlpGrpcPortName = "otlp-grpc" // PortOtlpGrpcServer declares the port number of the OpenTelemetry Collector gRPC receiver port. diff --git a/internal/manifests/monolithic/build.go b/internal/manifests/monolithic/build.go new file mode 100644 index 000000000..4a2662772 --- /dev/null +++ b/internal/manifests/monolithic/build.go @@ -0,0 +1,28 @@ +package monolithic + +import ( + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// BuildAll generates all manifests. +func BuildAll(opts Options) ([]client.Object, error) { + manifests := []client.Object{} + + configMap, configChecksum, err := BuildConfigMap(opts) + if err != nil { + return nil, err + } + manifests = append(manifests, configMap) + opts.ConfigChecksum = configChecksum + + statefulSet, err := BuildTempoStatefulset(opts) + if err != nil { + return nil, err + } + manifests = append(manifests, statefulSet) + + service := BuildTempoService(opts) + manifests = append(manifests, service) + + return manifests, nil +} diff --git a/internal/manifests/monolithic/build_test.go b/internal/manifests/monolithic/build_test.go new file mode 100644 index 000000000..781ecc78e --- /dev/null +++ b/internal/manifests/monolithic/build_test.go @@ -0,0 +1,32 @@ +package monolithic + +import ( + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/grafana/tempo-operator/apis/tempo/v1alpha1" +) + +func TestBuildAll(t *testing.T) { + opts := Options{ + Tempo: v1alpha1.TempoMonolithic{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sample", + Namespace: "default", + }, + Spec: v1alpha1.TempoMonolithicSpec{ + Storage: &v1alpha1.MonolithicStorageSpec{ + Traces: v1alpha1.MonolithicTracesStorageSpec{ + Backend: "memory", + }, + }, + }, + }, + } + + objects, err := BuildAll(opts) + require.NoError(t, err) + require.Len(t, objects, 3) +} diff --git a/internal/manifests/monolithic/configmap.go b/internal/manifests/monolithic/configmap.go new file mode 100644 index 000000000..85f2c67bf --- /dev/null +++ b/internal/manifests/monolithic/configmap.go @@ -0,0 +1,141 @@ +package monolithic + +import ( + "crypto/sha256" + "fmt" + + "gopkg.in/yaml.v2" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/grafana/tempo-operator/apis/tempo/v1alpha1" + tempoStackConfig "github.com/grafana/tempo-operator/internal/manifests/config" + "github.com/grafana/tempo-operator/internal/manifests/manifestutils" + "github.com/grafana/tempo-operator/internal/manifests/naming" +) + +type tempoConfig struct { + Server struct { + HttpListenPort int `yaml:"http_listen_port"` + } `yaml:"server"` + + Storage struct { + Trace struct { + Backend string `yaml:"backend"` + WAL struct { + Path string `yaml:"path"` + } `yaml:"wal"` + Local struct { + Path string `yaml:"path"` + } `yaml:"local"` + } `yaml:"trace"` + } `yaml:"storage"` + + Distributor struct { + Receivers struct { + OTLP struct { + Protocols struct { + GRPC *interface{} `yaml:"grpc,omitempty"` + HTTP *interface{} `yaml:"http,omitempty"` + } `yaml:"protocols,omitempty"` + } `yaml:"otlp,omitempty"` + } `yaml:"receivers,omitempty"` + } `yaml:"distributor,omitempty"` + + UsageReport struct { + ReportingEnabled bool `yaml:"reporting_enabled"` + } `yaml:"usage_report"` +} + +type tempoQueryConfig struct { + Backend string `yaml:"backend"` + TenantHeaderKey string `yaml:"tenant_header_key"` +} + +// BuildConfigMap creates the Tempo ConfigMap for a monolithic deployment. +func BuildConfigMap(opts Options) (*corev1.ConfigMap, string, error) { + tempo := opts.Tempo + labels := Labels(tempo.Name) + + tempoConfig, err := buildTempoConfig(opts) + if err != nil { + return nil, "", err + } + + configMap := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: appsv1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: naming.Name("", tempo.Name), + Namespace: tempo.Namespace, + Labels: labels, + }, + Data: map[string]string{ + "tempo.yaml": string(tempoConfig), + }, + } + + h := sha256.Sum256(tempoConfig) + checksum := fmt.Sprintf("%x", h) + + if tempo.Spec.JaegerUI != nil && tempo.Spec.JaegerUI.Enabled { + tempoQueryConfig, err := buildTempoQueryConfig() + if err != nil { + return nil, "", err + } + configMap.Data["tempo-query.yaml"] = string(tempoQueryConfig) + } + + return configMap, checksum, nil +} + +func buildTempoConfig(opts Options) ([]byte, error) { + tempo := opts.Tempo + + config := tempoConfig{} + config.Server.HttpListenPort = manifestutils.PortHTTPServer + + config.Storage.Trace.WAL.Path = "/var/tempo/wal" + switch tempo.Spec.Storage.Traces.Backend { + case v1alpha1.MonolithicTracesStorageBackendMemory, + v1alpha1.MonolithicTracesStorageBackendPV: + config.Storage.Trace.Backend = "local" + config.Storage.Trace.Local.Path = "/var/tempo/blocks" + + default: + return nil, fmt.Errorf("invalid storage backend: '%s'", tempo.Spec.Storage.Traces.Backend) + } + + if tempo.Spec.Ingestion != nil && tempo.Spec.Ingestion.OTLP != nil { + if tempo.Spec.Ingestion.OTLP.GRPC != nil && tempo.Spec.Ingestion.OTLP.GRPC.Enabled { + var i interface{} + config.Distributor.Receivers.OTLP.Protocols.GRPC = &i + } + if tempo.Spec.Ingestion.OTLP.HTTP != nil && tempo.Spec.Ingestion.OTLP.HTTP.Enabled { + var i interface{} + config.Distributor.Receivers.OTLP.Protocols.HTTP = &i + } + } + + generatedYaml, err := yaml.Marshal(config) + if err != nil { + return nil, err + } + + if tempo.Spec.ExtraConfig == nil || len(tempo.Spec.ExtraConfig.Tempo.Raw) == 0 { + return generatedYaml, nil + } else { + return tempoStackConfig.MergeExtraConfigWithConfig(tempo.Spec.ExtraConfig.Tempo, generatedYaml) + } +} + +func buildTempoQueryConfig() ([]byte, error) { + config := tempoQueryConfig{} + config.Backend = fmt.Sprintf("127.0.0.1:%d", manifestutils.PortHTTPServer) + config.TenantHeaderKey = manifestutils.TenantHeader + + return yaml.Marshal(&config) +} diff --git a/internal/manifests/monolithic/configmap_test.go b/internal/manifests/monolithic/configmap_test.go new file mode 100644 index 000000000..34b22cf5f --- /dev/null +++ b/internal/manifests/monolithic/configmap_test.go @@ -0,0 +1,184 @@ +package monolithic + +import ( + "crypto/sha256" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + configv1alpha1 "github.com/grafana/tempo-operator/apis/config/v1alpha1" + "github.com/grafana/tempo-operator/apis/tempo/v1alpha1" +) + +func TestBuildConfigMap(t *testing.T) { + opts := Options{ + CtrlConfig: configv1alpha1.ProjectConfig{ + DefaultImages: configv1alpha1.ImagesSpec{ + Tempo: "docker.io/grafana/tempo:x.y.z", + }, + }, + Tempo: v1alpha1.TempoMonolithic{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sample", + Namespace: "default", + }, + Spec: v1alpha1.TempoMonolithicSpec{ + Storage: &v1alpha1.MonolithicStorageSpec{ + Traces: v1alpha1.MonolithicTracesStorageSpec{ + Backend: "memory", + }, + }, + Ingestion: &v1alpha1.MonolithicIngestionSpec{ + OTLP: &v1alpha1.MonolithicIngestionOTLPSpec{ + GRPC: &v1alpha1.MonolithicIngestionOTLPProtocolsGRPCSpec{ + Enabled: true, + }, + }, + }, + }, + }, + } + + cm, checksum, err := BuildConfigMap(opts) + require.NoError(t, err) + require.NotNil(t, cm.Data) + require.NotNil(t, cm.Data["tempo.yaml"]) + require.Equal(t, fmt.Sprintf("%x", sha256.Sum256([]byte(cm.Data["tempo.yaml"]))), checksum) +} + +func TestBuildConfig(t *testing.T) { + tests := []struct { + name string + spec v1alpha1.TempoMonolithicSpec + expected string + }{ + { + name: "memory storage", + spec: v1alpha1.TempoMonolithicSpec{ + Storage: &v1alpha1.MonolithicStorageSpec{ + Traces: v1alpha1.MonolithicTracesStorageSpec{ + Backend: "memory", + }, + }, + }, + expected: ` +server: + http_listen_port: 3200 +storage: + trace: + backend: local + wal: + path: /var/tempo/wal + local: + path: /var/tempo/blocks +distributor: + receivers: + otlp: + protocols: + grpc: + http: +usage_report: + reporting_enabled: false +`, + }, + { + name: "PV storage with OTLP/gRPC and OTLP/HTTP", + spec: v1alpha1.TempoMonolithicSpec{ + Storage: &v1alpha1.MonolithicStorageSpec{ + Traces: v1alpha1.MonolithicTracesStorageSpec{ + Backend: "pv", + }, + }, + Ingestion: &v1alpha1.MonolithicIngestionSpec{ + OTLP: &v1alpha1.MonolithicIngestionOTLPSpec{ + GRPC: &v1alpha1.MonolithicIngestionOTLPProtocolsGRPCSpec{ + Enabled: true, + }, + HTTP: &v1alpha1.MonolithicIngestionOTLPProtocolsHTTPSpec{ + Enabled: true, + }, + }, + }, + }, + expected: ` +server: + http_listen_port: 3200 +storage: + trace: + backend: local + wal: + path: /var/tempo/wal + local: + path: /var/tempo/blocks +distributor: + receivers: + otlp: + protocols: + grpc: + http: +usage_report: + reporting_enabled: false +`, + }, + { + name: "extra config", + spec: v1alpha1.TempoMonolithicSpec{ + Storage: &v1alpha1.MonolithicStorageSpec{ + Traces: v1alpha1.MonolithicTracesStorageSpec{ + Backend: "memory", + }, + }, + ExtraConfig: &v1alpha1.ExtraConfigSpec{ + Tempo: apiextensionsv1.JSON{Raw: []byte(`{"storage": {"trace": {"wal": {"overlay_setting": "abc"}}}}`)}, + }, + }, + expected: ` +server: + http_listen_port: 3200 +storage: + trace: + backend: local + wal: + path: /var/tempo/wal + overlay_setting: abc + local: + path: /var/tempo/blocks +distributor: + receivers: + otlp: + protocols: + grpc: + http: +usage_report: + reporting_enabled: false +`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + opts := Options{ + CtrlConfig: configv1alpha1.ProjectConfig{ + DefaultImages: configv1alpha1.ImagesSpec{ + Tempo: "docker.io/grafana/tempo:x.y.z", + }, + }, + Tempo: v1alpha1.TempoMonolithic{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sample", + Namespace: "default", + }, + Spec: test.spec, + }, + } + opts.Tempo.Default() + + cfg, err := buildTempoConfig(opts) + require.NoError(t, err) + require.YAMLEq(t, test.expected, string(cfg)) + }) + } +} diff --git a/internal/manifests/monolithic/labels.go b/internal/manifests/monolithic/labels.go new file mode 100644 index 000000000..9469ba3ef --- /dev/null +++ b/internal/manifests/monolithic/labels.go @@ -0,0 +1,10 @@ +package monolithic + +// Labels returns common labels for each TempoMonolithic object created by the operator. +func Labels(instanceName string) map[string]string { + return map[string]string{ + "app.kubernetes.io/name": "tempo-monolithic", + "app.kubernetes.io/instance": instanceName, + "app.kubernetes.io/managed-by": "tempo-operator", + } +} diff --git a/internal/manifests/monolithic/options.go b/internal/manifests/monolithic/options.go new file mode 100644 index 000000000..add1e6441 --- /dev/null +++ b/internal/manifests/monolithic/options.go @@ -0,0 +1,13 @@ +package monolithic + +import ( + configv1alpha1 "github.com/grafana/tempo-operator/apis/config/v1alpha1" + "github.com/grafana/tempo-operator/apis/tempo/v1alpha1" +) + +// Options defines calculated options required to generate all manifests. +type Options struct { + CtrlConfig configv1alpha1.ProjectConfig + Tempo v1alpha1.TempoMonolithic + ConfigChecksum string +} diff --git a/internal/manifests/monolithic/service.go b/internal/manifests/monolithic/service.go new file mode 100644 index 000000000..6cdeda695 --- /dev/null +++ b/internal/manifests/monolithic/service.go @@ -0,0 +1,81 @@ +package monolithic + +import ( + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/grafana/tempo-operator/internal/manifests/manifestutils" + "github.com/grafana/tempo-operator/internal/manifests/naming" +) + +// BuildTempoService creates the service for a monolithic deployment. +func BuildTempoService(opts Options) *corev1.Service { + tempo := opts.Tempo + labels := Labels(tempo.Name) + ports := []corev1.ServicePort{ + { + Name: manifestutils.HttpPortName, + Protocol: corev1.ProtocolTCP, + Port: manifestutils.PortHTTPServer, + TargetPort: intstr.FromString(manifestutils.HttpPortName), + }, + } + + // TODO: point to gateway + if tempo.Spec.Ingestion != nil && tempo.Spec.Ingestion.OTLP != nil { + if tempo.Spec.Ingestion.OTLP.GRPC != nil && tempo.Spec.Ingestion.OTLP.GRPC.Enabled { + ports = append(ports, corev1.ServicePort{ + Name: manifestutils.OtlpGrpcPortName, + Protocol: corev1.ProtocolTCP, + Port: manifestutils.PortOtlpGrpcServer, + TargetPort: intstr.FromString(manifestutils.OtlpGrpcPortName), + }) + } + if tempo.Spec.Ingestion.OTLP.HTTP != nil && tempo.Spec.Ingestion.OTLP.HTTP.Enabled { + ports = append(ports, corev1.ServicePort{ + Name: manifestutils.PortOtlpHttpName, + Protocol: corev1.ProtocolTCP, + Port: manifestutils.PortOtlpHttp, + TargetPort: intstr.FromString(manifestutils.PortOtlpHttpName), + }) + } + } + + if tempo.Spec.JaegerUI != nil && tempo.Spec.JaegerUI.Enabled { + ports = append(ports, []corev1.ServicePort{ + { + Name: manifestutils.JaegerGRPCQuery, + Port: manifestutils.PortJaegerGRPCQuery, + TargetPort: intstr.FromString(manifestutils.JaegerGRPCQuery), + }, + { + Name: manifestutils.JaegerUIPortName, + Port: manifestutils.PortJaegerUI, + TargetPort: intstr.FromString(manifestutils.JaegerUIPortName), + }, + { + Name: manifestutils.JaegerMetricsPortName, + Port: manifestutils.PortJaegerMetrics, + TargetPort: intstr.FromString(manifestutils.JaegerMetricsPortName), + }, + }...) + } + + return &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: appsv1.SchemeGroupVersion.String(), + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: naming.Name("", tempo.Name), + Namespace: tempo.Namespace, + Labels: labels, + }, + Spec: corev1.ServiceSpec{ + Ports: ports, + Selector: labels, + }, + } +} diff --git a/internal/manifests/monolithic/service_test.go b/internal/manifests/monolithic/service_test.go new file mode 100644 index 000000000..9e2bbfe7a --- /dev/null +++ b/internal/manifests/monolithic/service_test.go @@ -0,0 +1,148 @@ +package monolithic + +import ( + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/grafana/tempo-operator/apis/tempo/v1alpha1" +) + +func TestBuildTempoService(t *testing.T) { + opts := Options{ + Tempo: v1alpha1.TempoMonolithic{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sample", + Namespace: "default", + }, + }, + } + + tests := []struct { + name string + input v1alpha1.TempoMonolithicSpec + expected []corev1.ServicePort + }{ + { + name: "no ingestion ports, no jaeger ui", + input: v1alpha1.TempoMonolithicSpec{}, + expected: []corev1.ServicePort{ + { + Name: "http", + Protocol: corev1.ProtocolTCP, + Port: 3200, + TargetPort: intstr.FromString("http"), + }, + }, + }, + { + name: "ingest OTLP/gRPC", + input: v1alpha1.TempoMonolithicSpec{ + Ingestion: &v1alpha1.MonolithicIngestionSpec{ + OTLP: &v1alpha1.MonolithicIngestionOTLPSpec{ + GRPC: &v1alpha1.MonolithicIngestionOTLPProtocolsGRPCSpec{ + Enabled: true, + }, + }, + }, + }, + expected: []corev1.ServicePort{ + { + Name: "http", + Protocol: corev1.ProtocolTCP, + Port: 3200, + TargetPort: intstr.FromString("http"), + }, + { + Name: "otlp-grpc", + Protocol: corev1.ProtocolTCP, + Port: 4317, + TargetPort: intstr.FromString("otlp-grpc"), + }, + }, + }, + { + name: "ingest OTLP/HTTP", + input: v1alpha1.TempoMonolithicSpec{ + Ingestion: &v1alpha1.MonolithicIngestionSpec{ + OTLP: &v1alpha1.MonolithicIngestionOTLPSpec{ + HTTP: &v1alpha1.MonolithicIngestionOTLPProtocolsHTTPSpec{ + Enabled: true, + }, + }, + }, + }, + expected: []corev1.ServicePort{ + { + Name: "http", + Protocol: corev1.ProtocolTCP, + Port: 3200, + TargetPort: intstr.FromString("http"), + }, + { + Name: "otlp-http", + Protocol: corev1.ProtocolTCP, + Port: 4318, + TargetPort: intstr.FromString("otlp-http"), + }, + }, + }, + { + name: "enable JaegerUI", + input: v1alpha1.TempoMonolithicSpec{ + JaegerUI: &v1alpha1.MonolithicJaegerUISpec{ + Enabled: true, + }, + }, + expected: []corev1.ServicePort{ + { + Name: "http", + Protocol: corev1.ProtocolTCP, + Port: 3200, + TargetPort: intstr.FromString("http"), + }, + { + Name: "jaeger-grpc", + Port: 16685, + TargetPort: intstr.FromString("jaeger-grpc"), + }, + { + Name: "jaeger-ui", + Port: 16686, + TargetPort: intstr.FromString("jaeger-ui"), + }, + { + Name: "jaeger-metrics", + Port: 16687, + TargetPort: intstr.FromString("jaeger-metrics"), + }, + }, + }, + } + + labels := Labels("sample") + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + opts.Tempo.Spec = test.input + svc := BuildTempoService(opts) + require.Equal(t, &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apps/v1", + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "tempo-sample", + Namespace: "default", + Labels: labels, + }, + Spec: corev1.ServiceSpec{ + Ports: test.expected, + Selector: labels, + }, + }, svc) + }) + } +} diff --git a/internal/manifests/monolithic/statefulset.go b/internal/manifests/monolithic/statefulset.go new file mode 100644 index 000000000..a014cc0c1 --- /dev/null +++ b/internal/manifests/monolithic/statefulset.go @@ -0,0 +1,260 @@ +package monolithic + +import ( + "errors" + "fmt" + + "github.com/operator-framework/operator-lib/proxy" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + "github.com/grafana/tempo-operator/apis/tempo/v1alpha1" + "github.com/grafana/tempo-operator/internal/manifests/manifestutils" + "github.com/grafana/tempo-operator/internal/manifests/naming" +) + +const ( + walVolumeName = "tempo-wal" + blocksVolumeName = "tempo-blocks" +) + +// BuildTempoStatefulset creates the Tempo statefulset for a monolithic deployment. +func BuildTempoStatefulset(opts Options) (*appsv1.StatefulSet, error) { + tempo := opts.Tempo + labels := Labels(tempo.Name) + annotations := manifestutils.CommonAnnotations(opts.ConfigChecksum) + + ss := &appsv1.StatefulSet{ + TypeMeta: metav1.TypeMeta{ + APIVersion: appsv1.SchemeGroupVersion.String(), + Kind: "StatefulSet", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: naming.Name("", tempo.Name), + Namespace: tempo.Namespace, + Labels: labels, + }, + Spec: appsv1.StatefulSetSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + + // Changes to a StatefulSet are not propagated to pods in a broken state (e.g. CrashLoopBackOff) + // See https://github.com/kubernetes/kubernetes/issues/67250 + // + // This is a workaround for the above issue. + // This setting is also in the tempo-distributed helm chart: https://github.com/grafana/helm-charts/blob/0fdf2e1900733eb104ac734f5fb0a89dc950d2c2/charts/tempo-distributed/templates/ingester/statefulset-ingester.yaml#L21 + PodManagementPolicy: appsv1.ParallelPodManagement, + + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + Annotations: annotations, + }, + Spec: corev1.PodSpec{ + Affinity: manifestutils.DefaultAffinity(labels), + Containers: []corev1.Container{ + { + Name: "tempo", + Image: opts.CtrlConfig.DefaultImages.Tempo, + Env: proxy.ReadProxyVarsFromEnv(), + Args: []string{ + "-config.file=/conf/tempo.yaml", + "-mem-ballast-size-mbs=1024", + "-log.level=info", + }, + + // The Tempo Helm chart mounts /var/tempo if persistence is enabled. + // Tempo writes its WAL to /var/tempo/wal, and if the local storage backend is enabled, parquet blocks to /var/tempo/blocks. + // + // Let's mount /var/tempo as WAL, in case Tempo writes caches to additional locations in /var/tempo in the future. + // If memory or pv storage is enabled, /var/tempo/blocks will be mounted in a different volume (configured in configureStorage()). + // This is to avoid confusion why a PV is required when selecting object storage. + VolumeMounts: []corev1.VolumeMount{ + { + Name: manifestutils.ConfigVolumeName, + MountPath: "/conf", + ReadOnly: true, + }, + { + Name: walVolumeName, + MountPath: "/var/tempo", + }, + }, + Ports: buildTempoPorts(opts), + ReadinessProbe: manifestutils.TempoReadinessProbe(false), + SecurityContext: manifestutils.TempoContainerSecurityContext(), + }, + }, + Volumes: []corev1.Volume{ + { + Name: manifestutils.ConfigVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: naming.Name("", tempo.Name), + }, + }, + }, + }, + }, + }, + }, + }, + } + + err := configureStorage(opts, ss) + if err != nil { + return nil, err + } + + if tempo.Spec.JaegerUI != nil && tempo.Spec.JaegerUI.Enabled { + configureJaegerUI(opts, ss) + } + + return ss, nil +} + +func buildTempoPorts(opts Options) []corev1.ContainerPort { + tempo := opts.Tempo + ports := []corev1.ContainerPort{ + { + Name: manifestutils.HttpPortName, + ContainerPort: manifestutils.PortHTTPServer, + Protocol: corev1.ProtocolTCP, + }, + } + + if tempo.Spec.Ingestion != nil && tempo.Spec.Ingestion.OTLP != nil { + if tempo.Spec.Ingestion.OTLP.GRPC != nil && tempo.Spec.Ingestion.OTLP.GRPC.Enabled { + ports = append(ports, corev1.ContainerPort{ + Name: manifestutils.OtlpGrpcPortName, + ContainerPort: manifestutils.PortOtlpGrpcServer, + Protocol: corev1.ProtocolTCP, + }) + } + if tempo.Spec.Ingestion.OTLP.HTTP != nil && tempo.Spec.Ingestion.OTLP.HTTP.Enabled { + ports = append(ports, corev1.ContainerPort{ + Name: manifestutils.PortOtlpHttpName, + ContainerPort: manifestutils.PortOtlpHttp, + Protocol: corev1.ProtocolTCP, + }) + } + } + + return ports +} + +func configureStorage(opts Options, sts *appsv1.StatefulSet) error { + tempo := opts.Tempo + switch tempo.Spec.Storage.Traces.Backend { + case v1alpha1.MonolithicTracesStorageBackendMemory: + sts.Spec.Template.Spec.Volumes = append(sts.Spec.Template.Spec.Volumes, corev1.Volume{ + Name: walVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{ + Medium: corev1.StorageMediumMemory, + }, + }, + }) + + sts.Spec.Template.Spec.Containers[0].VolumeMounts = append(sts.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{ + Name: blocksVolumeName, + MountPath: "/var/tempo/blocks", + }) + sts.Spec.Template.Spec.Volumes = append(sts.Spec.Template.Spec.Volumes, corev1.Volume{ + Name: blocksVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{ + Medium: corev1.StorageMediumMemory, + }, + }, + }) + + case v1alpha1.MonolithicTracesStorageBackendPV: + if tempo.Spec.Storage.Traces.WAL == nil { + return errors.New("please configure .spec.storage.traces.wal") + } + sts.Spec.VolumeClaimTemplates = append(sts.Spec.VolumeClaimTemplates, corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: walVolumeName, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: tempo.Spec.Storage.Traces.WAL.Size, + }, + }, + VolumeMode: ptr.To(corev1.PersistentVolumeFilesystem), + }, + }) + + if tempo.Spec.Storage.Traces.PV == nil { + return errors.New("please configure .spec.storage.traces.pv") + } + sts.Spec.Template.Spec.Containers[0].VolumeMounts = append(sts.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{ + Name: blocksVolumeName, + MountPath: "/var/tempo/blocks", + }) + sts.Spec.VolumeClaimTemplates = append(sts.Spec.VolumeClaimTemplates, corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: blocksVolumeName, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: tempo.Spec.Storage.Traces.PV.Size, + }, + }, + VolumeMode: ptr.To(corev1.PersistentVolumeFilesystem), + }, + }) + + default: + return fmt.Errorf("invalid storage backend: '%s'", tempo.Spec.Storage.Traces.Backend) + } + return nil +} + +func configureJaegerUI(opts Options, sts *appsv1.StatefulSet) { + tempoQuery := corev1.Container{ + Name: "tempo-query", + Image: opts.CtrlConfig.DefaultImages.TempoQuery, + Env: proxy.ReadProxyVarsFromEnv(), + Args: []string{ + "--query.base-path=/", + "--grpc-storage-plugin.configuration-file=/conf/tempo-query.yaml", + "--query.bearer-token-propagation=true", + }, + Ports: []corev1.ContainerPort{ + { + Name: manifestutils.JaegerGRPCQuery, + ContainerPort: manifestutils.PortJaegerGRPCQuery, + Protocol: corev1.ProtocolTCP, + }, + { + Name: manifestutils.JaegerUIPortName, + ContainerPort: manifestutils.PortJaegerUI, + Protocol: corev1.ProtocolTCP, + }, + { + Name: manifestutils.JaegerMetricsPortName, + ContainerPort: manifestutils.PortJaegerMetrics, + Protocol: corev1.ProtocolTCP, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: manifestutils.ConfigVolumeName, + MountPath: "/conf", + ReadOnly: true, + }, + }, + } + + sts.Spec.Template.Spec.Containers = append(sts.Spec.Template.Spec.Containers, tempoQuery) +} diff --git a/internal/manifests/monolithic/statefulset_test.go b/internal/manifests/monolithic/statefulset_test.go new file mode 100644 index 000000000..ff35eb612 --- /dev/null +++ b/internal/manifests/monolithic/statefulset_test.go @@ -0,0 +1,337 @@ +package monolithic + +import ( + "testing" + + "github.com/operator-framework/operator-lib/proxy" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + configv1alpha1 "github.com/grafana/tempo-operator/apis/config/v1alpha1" + "github.com/grafana/tempo-operator/apis/tempo/v1alpha1" + "github.com/grafana/tempo-operator/internal/manifests/manifestutils" +) + +var ( + oneGBQuantity = resource.MustParse("1Gi") + tenGBQuantity = resource.MustParse("10Gi") +) + +func TestStatefulsetMemoryStorage(t *testing.T) { + opts := Options{ + CtrlConfig: configv1alpha1.ProjectConfig{ + DefaultImages: configv1alpha1.ImagesSpec{ + Tempo: "docker.io/grafana/tempo:x.y.z", + }, + }, + Tempo: v1alpha1.TempoMonolithic{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sample", + Namespace: "default", + }, + Spec: v1alpha1.TempoMonolithicSpec{ + Storage: &v1alpha1.MonolithicStorageSpec{ + Traces: v1alpha1.MonolithicTracesStorageSpec{ + Backend: "memory", + }, + }, + Ingestion: &v1alpha1.MonolithicIngestionSpec{ + OTLP: &v1alpha1.MonolithicIngestionOTLPSpec{ + GRPC: &v1alpha1.MonolithicIngestionOTLPProtocolsGRPCSpec{ + Enabled: true, + }, + }, + }, + }, + }, + } + sts, err := BuildTempoStatefulset(opts) + require.NoError(t, err) + + labels := Labels("sample") + annotations := manifestutils.CommonAnnotations("") + require.Equal(t, &appsv1.StatefulSet{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apps/v1", + Kind: "StatefulSet", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "tempo-sample", + Namespace: "default", + Labels: labels, + }, + Spec: appsv1.StatefulSetSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + PodManagementPolicy: appsv1.ParallelPodManagement, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + Annotations: annotations, + }, + Spec: corev1.PodSpec{ + Affinity: manifestutils.DefaultAffinity(labels), + Containers: []corev1.Container{ + { + Name: "tempo", + Image: "docker.io/grafana/tempo:x.y.z", + Env: proxy.ReadProxyVarsFromEnv(), + Args: []string{ + "-config.file=/conf/tempo.yaml", + "-mem-ballast-size-mbs=1024", + "-log.level=info", + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "tempo-conf", + MountPath: "/conf", + ReadOnly: true, + }, + { + Name: "tempo-wal", + MountPath: "/var/tempo", + }, + { + Name: "tempo-blocks", + MountPath: "/var/tempo/blocks", + }, + }, + Ports: []corev1.ContainerPort{ + { + Name: "http", + ContainerPort: 3200, + Protocol: corev1.ProtocolTCP, + }, + { + Name: "otlp-grpc", + ContainerPort: 4317, + Protocol: corev1.ProtocolTCP, + }, + }, + ReadinessProbe: manifestutils.TempoReadinessProbe(false), + SecurityContext: manifestutils.TempoContainerSecurityContext(), + }, + }, + Volumes: []corev1.Volume{ + { + Name: "tempo-conf", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "tempo-sample", + }, + }, + }, + }, + { + Name: "tempo-wal", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{ + Medium: corev1.StorageMediumMemory, + }, + }, + }, + { + Name: "tempo-blocks", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{ + Medium: corev1.StorageMediumMemory, + }, + }, + }, + }, + }, + }, + }, + }, sts) +} + +func TestStatefulsetPVStorage(t *testing.T) { + opts := Options{ + CtrlConfig: configv1alpha1.ProjectConfig{ + DefaultImages: configv1alpha1.ImagesSpec{ + Tempo: "docker.io/grafana/tempo:x.y.z", + }, + }, + Tempo: v1alpha1.TempoMonolithic{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sample", + Namespace: "default", + }, + Spec: v1alpha1.TempoMonolithicSpec{ + Storage: &v1alpha1.MonolithicStorageSpec{ + Traces: v1alpha1.MonolithicTracesStorageSpec{ + Backend: "pv", + WAL: &v1alpha1.MonolithicTracesStorageWALSpec{ + Size: oneGBQuantity, + }, + PV: &v1alpha1.MonolithicTracesStoragePVSpec{ + Size: tenGBQuantity, + }, + }, + }, + }, + }, + } + sts, err := BuildTempoStatefulset(opts) + require.NoError(t, err) + + require.Equal(t, []corev1.VolumeMount{ + { + Name: "tempo-conf", + MountPath: "/conf", + ReadOnly: true, + }, + { + Name: "tempo-wal", + MountPath: "/var/tempo", + }, + { + Name: "tempo-blocks", + MountPath: "/var/tempo/blocks", + }, + }, sts.Spec.Template.Spec.Containers[0].VolumeMounts) + + require.Equal(t, []corev1.Volume{ + { + Name: "tempo-conf", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "tempo-sample", + }, + }, + }, + }, + }, sts.Spec.Template.Spec.Volumes) + + require.Equal(t, []corev1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "tempo-wal", + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: oneGBQuantity, + }, + }, + VolumeMode: ptr.To(corev1.PersistentVolumeFilesystem), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "tempo-blocks", + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: tenGBQuantity, + }, + }, + VolumeMode: ptr.To(corev1.PersistentVolumeFilesystem), + }, + }, + }, sts.Spec.VolumeClaimTemplates) +} + +func TestStatefulsetPorts(t *testing.T) { + opts := Options{ + CtrlConfig: configv1alpha1.ProjectConfig{ + DefaultImages: configv1alpha1.ImagesSpec{ + Tempo: "docker.io/grafana/tempo:x.y.z", + }, + }, + Tempo: v1alpha1.TempoMonolithic{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sample", + Namespace: "default", + }, + Spec: v1alpha1.TempoMonolithicSpec{ + Storage: &v1alpha1.MonolithicStorageSpec{ + Traces: v1alpha1.MonolithicTracesStorageSpec{ + Backend: "memory", + }, + }, + }, + }, + } + + tests := []struct { + name string + input *v1alpha1.MonolithicIngestionSpec + expected []corev1.ContainerPort + }{ + { + name: "no ingestion ports", + input: nil, + expected: []corev1.ContainerPort{ + { + Name: "http", + ContainerPort: 3200, + Protocol: corev1.ProtocolTCP, + }, + }, + }, + { + name: "OTLP/gRPC", + input: &v1alpha1.MonolithicIngestionSpec{ + OTLP: &v1alpha1.MonolithicIngestionOTLPSpec{ + GRPC: &v1alpha1.MonolithicIngestionOTLPProtocolsGRPCSpec{ + Enabled: true, + }, + }, + }, + expected: []corev1.ContainerPort{ + { + Name: "http", + ContainerPort: 3200, + Protocol: corev1.ProtocolTCP, + }, + { + Name: "otlp-grpc", + ContainerPort: 4317, + Protocol: corev1.ProtocolTCP, + }, + }, + }, + { + name: "OTLP/HTTP", + input: &v1alpha1.MonolithicIngestionSpec{ + OTLP: &v1alpha1.MonolithicIngestionOTLPSpec{ + HTTP: &v1alpha1.MonolithicIngestionOTLPProtocolsHTTPSpec{ + Enabled: true, + }, + }, + }, + expected: []corev1.ContainerPort{ + { + Name: "http", + ContainerPort: 3200, + Protocol: corev1.ProtocolTCP, + }, + { + Name: "otlp-http", + ContainerPort: 4318, + Protocol: corev1.ProtocolTCP, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + opts.Tempo.Spec.Ingestion = test.input + sts, err := BuildTempoStatefulset(opts) + require.NoError(t, err) + require.Equal(t, test.expected, sts.Spec.Template.Spec.Containers[0].Ports) + }) + } +} diff --git a/internal/manifests/mutate.go b/internal/manifests/mutate.go index 6c2f989e5..029a4f120 100644 --- a/internal/manifests/mutate.go +++ b/internal/manifests/mutate.go @@ -1,9 +1,11 @@ package manifests import ( + "fmt" "reflect" "github.com/ViaQ/logerr/v2/kverrors" + "github.com/google/go-cmp/cmp" grafanav1 "github.com/grafana-operator/grafana-operator/v5/api/v1beta1" "github.com/imdario/mergo" routev1 "github.com/openshift/api/route/v1" @@ -12,10 +14,22 @@ import ( corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" rbacv1 "k8s.io/api/rbac/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) +// ImmutableErr occurs if an immutable field should be changed. +type ImmutableErr struct { + field string + existing interface{} + desired interface{} +} + +func (m *ImmutableErr) Error() string { + return fmt.Sprintf("update to immutable field %s is forbidden, diff: %s", m.field, cmp.Diff(m.existing, m.desired)) +} + // MutateFuncFor returns a mutate function based on the // existing resource's concrete type. It supports currently // only the following types or else panics: @@ -127,6 +141,7 @@ func MutateFuncFor(existing, desired client.Object) controllerutil.MutateFn { } } +// Override non-empty dst attributes with non-empty src attributes values. func mergeWithOverride(dst, src interface{}) error { err := mergo.Merge(dst, src, mergo.WithOverride) if err != nil { @@ -231,12 +246,32 @@ func mutateDeployment(existing, desired *appsv1.Deployment) error { return nil } +func statefulSetVolumeClaimTemplatesChanged(existing, desired *appsv1.StatefulSet) bool { + if len(desired.Spec.VolumeClaimTemplates) != len(existing.Spec.VolumeClaimTemplates) { + return true + } + for i := range desired.Spec.VolumeClaimTemplates { + if desired.Spec.VolumeClaimTemplates[i].Name != existing.Spec.VolumeClaimTemplates[i].Name || + !apiequality.Semantic.DeepEqual(desired.Spec.VolumeClaimTemplates[i].Annotations, existing.Spec.VolumeClaimTemplates[i].Annotations) || + !apiequality.Semantic.DeepEqual(desired.Spec.VolumeClaimTemplates[i].Spec, existing.Spec.VolumeClaimTemplates[i].Spec) { + return true + } + } + return false +} + func mutateStatefulSet(existing, desired *appsv1.StatefulSet) error { - // StatefulSet selector is immutable so we set this value only if - // a new object is going to be created - if existing.CreationTimestamp.IsZero() { - existing.Spec.Selector = desired.Spec.Selector + // list of mutable fields: https://github.com/kubernetes/kubernetes/blob/b1cf91b300a82bd05fdd7b115559e5b83680d768/pkg/apis/apps/validation/validation.go#L184 + if !existing.CreationTimestamp.IsZero() { + if !apiequality.Semantic.DeepEqual(desired.Spec.Selector, existing.Spec.Selector) { + return &ImmutableErr{".spec.selector", existing.Spec.Selector, desired.Spec.Selector} + } + if statefulSetVolumeClaimTemplatesChanged(existing, desired) { + return &ImmutableErr{".spec.volumeClaimTemplates", existing.Spec.VolumeClaimTemplates, desired.Spec.VolumeClaimTemplates} + } } + + existing.Spec.Selector = desired.Spec.Selector existing.Spec.PodManagementPolicy = desired.Spec.PodManagementPolicy existing.Spec.Replicas = desired.Spec.Replicas for i := range existing.Spec.VolumeClaimTemplates { diff --git a/internal/manifests/mutate_test.go b/internal/manifests/mutate_test.go index 85ef87ce4..1f7b431f6 100644 --- a/internal/manifests/mutate_test.go +++ b/internal/manifests/mutate_test.go @@ -642,9 +642,10 @@ func TestGeMutateFunc_MutateStatefulSetSpec(t *testing.T) { one := int32(1) two := int32(2) type test struct { + name string got *appsv1.StatefulSet want *appsv1.StatefulSet - name string + err error } table := []test{ { @@ -710,7 +711,69 @@ func TestGeMutateFunc_MutateStatefulSetSpec(t *testing.T) { }, }, { - name: "update spec without selector", + name: "update mutable field .spec.template", + got: &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.Now()}, + Spec: appsv1.StatefulSetSpec{ + PodManagementPolicy: appsv1.ParallelPodManagement, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "test": "test", + }, + }, + Replicas: &one, + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "test"}, + }, + }, + }, + VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ + { + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + }, + }, + }, + }, + }, + want: &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.Now()}, + Spec: appsv1.StatefulSetSpec{ + PodManagementPolicy: appsv1.OrderedReadyPodManagement, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "test": "test", + }, + }, + Replicas: &two, + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test", + Args: []string{"--do-nothing"}, + }, + }, + }, + }, + VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ + { + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + }, + }, + }, + }, + }, + }, + { + name: "update immutable field .spec.volumeClaimTemplates", got: &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.Now()}, Spec: appsv1.StatefulSetSpec{ @@ -746,7 +809,6 @@ func TestGeMutateFunc_MutateStatefulSetSpec(t *testing.T) { Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "test": "test", - "and": "another", }, }, Replicas: &two, @@ -772,6 +834,7 @@ func TestGeMutateFunc_MutateStatefulSetSpec(t *testing.T) { }, }, }, + err: &manifests.ImmutableErr{}, }, } for _, tst := range table { @@ -780,19 +843,18 @@ func TestGeMutateFunc_MutateStatefulSetSpec(t *testing.T) { t.Parallel() f := manifests.MutateFuncFor(tst.got, tst.want) err := f() - require.NoError(t, err) - // Ensure conditional mutation applied - if tst.got.CreationTimestamp.IsZero() { - require.Equal(t, tst.got.Spec.Selector, tst.want.Spec.Selector) + if tst.err != nil { + require.ErrorAs(t, err, &tst.err) } else { - require.NotEqual(t, tst.got.Spec.Selector, tst.want.Spec.Selector) - } + require.NoError(t, err) - // Ensure partial mutation applied - require.Equal(t, tst.got.Spec.Replicas, tst.want.Spec.Replicas) - require.Equal(t, tst.got.Spec.Template, tst.want.Spec.Template) - require.Equal(t, tst.got.Spec.VolumeClaimTemplates, tst.got.Spec.VolumeClaimTemplates) + // Ensure partial mutation applied + require.Equal(t, tst.got.Spec.Selector, tst.want.Spec.Selector) + require.Equal(t, tst.got.Spec.Replicas, tst.want.Spec.Replicas) + require.Equal(t, tst.got.Spec.Template, tst.want.Spec.Template) + require.Equal(t, tst.got.Spec.VolumeClaimTemplates, tst.want.Spec.VolumeClaimTemplates) + } }) } } diff --git a/internal/manifests/queryfrontend/query_frontend.go b/internal/manifests/queryfrontend/query_frontend.go index 26009317d..dbd9796ac 100644 --- a/internal/manifests/queryfrontend/query_frontend.go +++ b/internal/manifests/queryfrontend/query_frontend.go @@ -23,15 +23,8 @@ import ( ) const ( - grpclbPortName = "grpclb" - jaegerMetricsPortName = "jaeger-metrics" - jaegerGRPCQuery = "jaeger-gprc" - jaegerUIPortName = "jaeger-ui" - portGRPCLBServer = 9096 - portJaegerGRPCQuery = 16685 - portJaegerUI = 16686 - portJaegerMetrics = 16687 - + grpclbPortName = "grpclb" + portGRPCLBServer = 9096 thanosQuerierOpenShiftMonitoring = "https://thanos-querier.openshift-monitoring.svc.cluster.local:9091" ) @@ -205,18 +198,18 @@ func deployment(params manifestutils.Params) (*appsv1.Deployment, error) { }, Ports: []corev1.ContainerPort{ { - Name: jaegerGRPCQuery, - ContainerPort: portJaegerGRPCQuery, + Name: manifestutils.JaegerGRPCQuery, + ContainerPort: manifestutils.PortJaegerGRPCQuery, Protocol: corev1.ProtocolTCP, }, { - Name: jaegerUIPortName, - ContainerPort: portJaegerUI, + Name: manifestutils.JaegerUIPortName, + ContainerPort: manifestutils.PortJaegerUI, Protocol: corev1.ProtocolTCP, }, { - Name: jaegerMetricsPortName, - ContainerPort: portJaegerMetrics, + Name: manifestutils.JaegerMetricsPortName, + ContainerPort: manifestutils.PortJaegerMetrics, Protocol: corev1.ProtocolTCP, }, }, @@ -414,19 +407,19 @@ func services(tempo v1alpha1.TempoStack) []*corev1.Service { if tempo.Spec.Template.QueryFrontend.JaegerQuery.Enabled { jaegerPorts := []corev1.ServicePort{ { - Name: jaegerGRPCQuery, - Port: portJaegerGRPCQuery, - TargetPort: intstr.FromString(jaegerGRPCQuery), + Name: manifestutils.JaegerGRPCQuery, + Port: manifestutils.PortJaegerGRPCQuery, + TargetPort: intstr.FromString(manifestutils.JaegerGRPCQuery), }, { - Name: jaegerUIPortName, - Port: portJaegerUI, - TargetPort: intstr.FromString(jaegerUIPortName), + Name: manifestutils.JaegerUIPortName, + Port: manifestutils.PortJaegerUI, + TargetPort: intstr.FromString(manifestutils.JaegerUIPortName), }, { - Name: jaegerMetricsPortName, - Port: portJaegerMetrics, - TargetPort: intstr.FromString(jaegerMetricsPortName), + Name: manifestutils.JaegerMetricsPortName, + Port: manifestutils.PortJaegerMetrics, + TargetPort: intstr.FromString(manifestutils.JaegerMetricsPortName), }, } @@ -457,7 +450,7 @@ func ingress(tempo v1alpha1.TempoStack) *networkingv1.Ingress { Service: &networkingv1.IngressServiceBackend{ Name: queryFrontendName, Port: networkingv1.ServiceBackendPort{ - Name: jaegerUIPortName, + Name: manifestutils.JaegerUIPortName, }, }, } @@ -519,7 +512,7 @@ func route(tempo v1alpha1.TempoStack) (*routev1.Route, error) { Name: queryFrontendName, }, Port: &routev1.RoutePort{ - TargetPort: intstr.FromString(jaegerUIPortName), + TargetPort: intstr.FromString(manifestutils.JaegerUIPortName), }, TLS: tlsCfg, }, diff --git a/internal/manifests/queryfrontend/query_frontend_test.go b/internal/manifests/queryfrontend/query_frontend_test.go index 3cd128e73..91aad0d76 100644 --- a/internal/manifests/queryfrontend/query_frontend_test.go +++ b/internal/manifests/queryfrontend/query_frontend_test.go @@ -26,19 +26,19 @@ import ( func getJaegerServicePorts() []corev1.ServicePort { jaegerServicePorts := []corev1.ServicePort{ { - Name: jaegerGRPCQuery, - Port: portJaegerGRPCQuery, - TargetPort: intstr.FromString(jaegerGRPCQuery), + Name: manifestutils.JaegerGRPCQuery, + Port: manifestutils.PortJaegerGRPCQuery, + TargetPort: intstr.FromString(manifestutils.JaegerGRPCQuery), }, { - Name: jaegerUIPortName, - Port: portJaegerUI, - TargetPort: intstr.FromString(jaegerUIPortName), + Name: manifestutils.JaegerUIPortName, + Port: manifestutils.PortJaegerUI, + TargetPort: intstr.FromString(manifestutils.JaegerUIPortName), }, { - Name: jaegerMetricsPortName, - Port: portJaegerMetrics, - TargetPort: intstr.FromString(jaegerMetricsPortName), + Name: manifestutils.JaegerMetricsPortName, + Port: manifestutils.PortJaegerMetrics, + TargetPort: intstr.FromString(manifestutils.JaegerMetricsPortName), }, } return jaegerServicePorts @@ -226,18 +226,18 @@ func getExpectedDeployment(withJaeger bool) *v1.Deployment { }, Ports: []corev1.ContainerPort{ { - Name: jaegerGRPCQuery, - ContainerPort: portJaegerGRPCQuery, + Name: manifestutils.JaegerGRPCQuery, + ContainerPort: manifestutils.PortJaegerGRPCQuery, Protocol: corev1.ProtocolTCP, }, { - Name: jaegerUIPortName, - ContainerPort: portJaegerUI, + Name: manifestutils.JaegerUIPortName, + ContainerPort: manifestutils.PortJaegerUI, Protocol: corev1.ProtocolTCP, }, { - Name: jaegerMetricsPortName, - ContainerPort: portJaegerMetrics, + Name: manifestutils.JaegerMetricsPortName, + ContainerPort: manifestutils.PortJaegerMetrics, Protocol: corev1.ProtocolTCP, }, }, @@ -432,7 +432,7 @@ func TestQueryFrontendJaegerIngress(t *testing.T) { Service: &networkingv1.IngressServiceBackend{ Name: naming.Name(manifestutils.QueryFrontendComponentName, "test"), Port: networkingv1.ServiceBackendPort{ - Name: jaegerUIPortName, + Name: manifestutils.JaegerUIPortName, }, }, }, @@ -483,7 +483,7 @@ func TestQueryFrontendJaegerRoute(t *testing.T) { Name: naming.Name(manifestutils.QueryFrontendComponentName, "test"), }, Port: &routev1.RoutePort{ - TargetPort: intstr.FromString(jaegerUIPortName), + TargetPort: intstr.FromString(manifestutils.JaegerUIPortName), }, TLS: &routev1.TLSConfig{ Termination: routev1.TLSTerminationEdge, diff --git a/tests/e2e-openshift/monitoring/02-assert.yaml b/tests/e2e-openshift/monitoring/02-assert.yaml index 8da18ae97..d221c4bc9 100644 --- a/tests/e2e-openshift/monitoring/02-assert.yaml +++ b/tests/e2e-openshift/monitoring/02-assert.yaml @@ -75,10 +75,10 @@ spec: port: 9095 protocol: TCP targetPort: grpc - - name: jaeger-gprc + - name: jaeger-grpc port: 16685 protocol: TCP - targetPort: jaeger-gprc + targetPort: jaeger-grpc - name: jaeger-ui port: 16686 protocol: TCP diff --git a/tests/e2e/compatibility/01-assert.yaml b/tests/e2e/compatibility/01-assert.yaml index a7b1fdea0..56d28738b 100644 --- a/tests/e2e/compatibility/01-assert.yaml +++ b/tests/e2e/compatibility/01-assert.yaml @@ -291,10 +291,10 @@ spec: port: 9095 protocol: TCP targetPort: grpc - - name: jaeger-gprc + - name: jaeger-grpc port: 16685 protocol: TCP - targetPort: jaeger-gprc + targetPort: jaeger-grpc - name: jaeger-ui port: 16686 protocol: TCP @@ -334,10 +334,10 @@ spec: port: 9096 protocol: TCP targetPort: grpclb - - name: jaeger-gprc + - name: jaeger-grpc port: 16685 protocol: TCP - targetPort: jaeger-gprc + targetPort: jaeger-grpc - name: jaeger-ui port: 16686 protocol: TCP diff --git a/tests/e2e/monolithic-smoketest/01-assert.yaml b/tests/e2e/monolithic-smoketest/01-assert.yaml new file mode 100644 index 000000000..8767efc3f --- /dev/null +++ b/tests/e2e/monolithic-smoketest/01-assert.yaml @@ -0,0 +1,60 @@ +apiVersion: tempo.grafana.com/v1alpha1 +kind: TempoMonolithic +metadata: + name: simplest +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: tempo-simplest + labels: + app.kubernetes.io/instance: simplest + app.kubernetes.io/managed-by: tempo-operator + app.kubernetes.io/name: tempo-monolithic +spec: + selector: + matchLabels: + app.kubernetes.io/instance: simplest + app.kubernetes.io/managed-by: tempo-operator + app.kubernetes.io/name: tempo-monolithic +status: + readyReplicas: 1 +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/instance: simplest + app.kubernetes.io/managed-by: tempo-operator + app.kubernetes.io/name: tempo-monolithic + name: tempo-simplest +spec: + ports: + - name: http + port: 3200 + protocol: TCP + targetPort: http + - name: otlp-grpc + port: 4317 + protocol: TCP + targetPort: otlp-grpc + - name: otlp-http + port: 4318 + protocol: TCP + targetPort: otlp-http + - name: jaeger-grpc + port: 16685 + protocol: TCP + targetPort: jaeger-grpc + - name: jaeger-ui + port: 16686 + protocol: TCP + targetPort: jaeger-ui + - name: jaeger-metrics + port: 16687 + protocol: TCP + targetPort: jaeger-metrics + selector: + app.kubernetes.io/instance: simplest + app.kubernetes.io/managed-by: tempo-operator + app.kubernetes.io/name: tempo-monolithic diff --git a/tests/e2e/monolithic-smoketest/01-install-tempo.yaml b/tests/e2e/monolithic-smoketest/01-install-tempo.yaml new file mode 100644 index 000000000..7a3a5f84d --- /dev/null +++ b/tests/e2e/monolithic-smoketest/01-install-tempo.yaml @@ -0,0 +1,7 @@ +apiVersion: tempo.grafana.com/v1alpha1 +kind: TempoMonolithic +metadata: + name: simplest +spec: + jaegerui: + enabled: true diff --git a/tests/e2e/monolithic-smoketest/03-assert.yaml b/tests/e2e/monolithic-smoketest/03-assert.yaml new file mode 100644 index 000000000..3f7323066 --- /dev/null +++ b/tests/e2e/monolithic-smoketest/03-assert.yaml @@ -0,0 +1,8 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: generate-traces +status: + conditions: + - status: "True" + type: Complete diff --git a/tests/e2e/monolithic-smoketest/03-generate-traces.yaml b/tests/e2e/monolithic-smoketest/03-generate-traces.yaml new file mode 100644 index 000000000..87e2fbc76 --- /dev/null +++ b/tests/e2e/monolithic-smoketest/03-generate-traces.yaml @@ -0,0 +1,17 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: generate-traces +spec: + template: + spec: + containers: + - name: telemetrygen + image: ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen:v0.75.0 + args: + - traces + - --otlp-endpoint=tempo-simplest:4317 + - --otlp-insecure + - --traces=10 + restartPolicy: Never + backoffLimit: 4 diff --git a/tests/e2e/monolithic-smoketest/04-assert.yaml b/tests/e2e/monolithic-smoketest/04-assert.yaml new file mode 100644 index 000000000..ab9e98db3 --- /dev/null +++ b/tests/e2e/monolithic-smoketest/04-assert.yaml @@ -0,0 +1,8 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: verify-traces-jaeger +status: + conditions: + - status: "True" + type: Complete diff --git a/tests/e2e/monolithic-smoketest/04-verify-traces-jaeger.yaml b/tests/e2e/monolithic-smoketest/04-verify-traces-jaeger.yaml new file mode 100644 index 000000000..8c1127424 --- /dev/null +++ b/tests/e2e/monolithic-smoketest/04-verify-traces-jaeger.yaml @@ -0,0 +1,36 @@ +# Simulate Jaeger Query API requests. +apiVersion: batch/v1 +kind: Job +metadata: + name: verify-traces-jaeger +spec: + template: + spec: + containers: + - name: verify-traces-jaeger + image: ghcr.io/grafana/tempo-operator/test-utils:main + command: + - /bin/bash + - -eux + - -c + args: + - | + # The query frontend must be accessible via HTTP (no mTLS) to enable connections from Grafana + curl \ + -v -G \ + http://tempo-simplest:3200/api/search \ + --data-urlencode "q={}" \ + | tee /tmp/tempo.out + num_traces=$(jq ".traces | length" /tmp/tempo.out) + if [[ "$num_traces" -ne 10 ]]; then + echo && echo "The Tempo API returned $num_traces instead of 10 traces." + exit 1 + fi + + curl -v -G http://tempo-simplest:16686/api/traces --data-urlencode "service=telemetrygen" | tee /tmp/jaeger.out + num_traces=$(jq ".data | length" /tmp/jaeger.out) + if [[ "$num_traces" -ne 10 ]]; then + echo && echo "The Jaeger API returned $num_traces instead of 10 traces." + exit 1 + fi + restartPolicy: Never diff --git a/tests/e2e/monolithic-smoketest/05-assert.yaml b/tests/e2e/monolithic-smoketest/05-assert.yaml new file mode 100644 index 000000000..7eec01bfb --- /dev/null +++ b/tests/e2e/monolithic-smoketest/05-assert.yaml @@ -0,0 +1,8 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: verify-traces-grafana +status: + conditions: + - status: "True" + type: Complete diff --git a/tests/e2e/monolithic-smoketest/05-verify-traces-grafana.yaml b/tests/e2e/monolithic-smoketest/05-verify-traces-grafana.yaml new file mode 100644 index 000000000..82c56c300 --- /dev/null +++ b/tests/e2e/monolithic-smoketest/05-verify-traces-grafana.yaml @@ -0,0 +1,46 @@ +# Simulate Grafana Dashboard API requests. +apiVersion: batch/v1 +kind: Job +metadata: + name: verify-traces-grafana +spec: + template: + spec: + containers: + - name: verify-traces-grafana + image: registry.gitlab.com/gitlab-ci-utils/curl-jq:1.1.0 + command: + - /bin/bash + - -eux + - -c + args: + - | + # Get the current Unix timestamp for "end" time, which is the current time + end_time=$(date -u +%s) + + # Calculate "start" time by subtracting 24 hours (86400 seconds) from the "end" time + start_time=$((end_time - 86400)) + + # The query frontend must be accessible via HTTP (no mTLS) to enable connections from Grafana + + # Run the curl command and capture the HTTP status code and output in a file + response_file=$(mktemp) + http_status=$(curl -s -o "$response_file" -w "%{http_code}" "http://tempo-simplest:3200/api/search?tags=%20service.name%3D%22telemetrygen%22%20name%3D%22okey-dokey%22&limit=20&start=$start_time&end=$end_time") + + # Check the HTTP status code to detect API call failures + if [[ "$http_status" -ne 200 ]]; then + echo "API call failed with HTTP status code $http_status." + exit 1 + fi + + # Parse the JSON output from the file and check if the "traces" array is empty + output=$(cat "$response_file" | jq .) + + if [[ "$(echo "$output" | jq -r '.traces | length')" -eq 0 ]]; then + echo "The Tempo API returned 0 Traces." + exit 1 + else + echo "Traces found." + exit 0 + fi + restartPolicy: Never