diff --git a/.chloggen/monolithic_status.yaml b/.chloggen/monolithic_status.yaml new file mode 100644 index 000000000..1a848cb8b --- /dev/null +++ b/.chloggen/monolithic_status.yaml @@ -0,0 +1,16 @@ +# 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: Expose operand status in TempoMonolithic CR + +# One or more tracking issues related to the change +issues: [787] + +# (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: diff --git a/apis/tempo/v1alpha1/common_types.go b/apis/tempo/v1alpha1/common_types.go index 6b8bd34cb..a38fff386 100644 --- a/apis/tempo/v1alpha1/common_types.go +++ b/apis/tempo/v1alpha1/common_types.go @@ -1,5 +1,10 @@ package v1alpha1 +import corev1 "k8s.io/api/core/v1" + +// PodStatusMap defines the type for mapping pod status to pod name. +type PodStatusMap map[corev1.PodPhase][]string + // TLSSpec is the TLS configuration. type TLSSpec struct { // Enabled defines if TLS is enabled. diff --git a/apis/tempo/v1alpha1/tempomonolithic_types.go b/apis/tempo/v1alpha1/tempomonolithic_types.go index 4e469d08c..dc0cd34bf 100644 --- a/apis/tempo/v1alpha1/tempomonolithic_types.go +++ b/apis/tempo/v1alpha1/tempomonolithic_types.go @@ -277,10 +277,28 @@ type MonolithicJaegerUIRouteSpec struct { Termination TLSRouteTerminationType `json:"termination,omitempty"` } +// MonolithicComponentStatus defines the status of each component. +type MonolithicComponentStatus struct { + // Tempo is a map of the pod status of the Tempo pods. + // + // +optional + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=status,displayName="Tempo",xDescriptors="urn:alm:descriptor:com.tectonic.ui:podStatuses" + Tempo PodStatusMap `json:"tempo"` +} + // 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 + // Components provides summary of all Tempo pod status, grouped per component. + // + // +kubebuilder:validation:Optional + Components MonolithicComponentStatus `json:"components,omitempty"` + + // Conditions of the Tempo deployment health. + // + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors="urn:alm:descriptor:io.kubernetes.conditions" + Conditions []metav1.Condition `json:"conditions,omitempty"` } //+kubebuilder:object:root=true diff --git a/apis/tempo/v1alpha1/tempostack_types.go b/apis/tempo/v1alpha1/tempostack_types.go index 528a2dd33..4b0dff13a 100644 --- a/apis/tempo/v1alpha1/tempostack_types.go +++ b/apis/tempo/v1alpha1/tempostack_types.go @@ -216,9 +216,6 @@ type GrafanaConfigSpec struct { InstanceSelector metav1.LabelSelector `json:"instanceSelector,omitempty"` } -// PodStatusMap defines the type for mapping pod status to pod name. -type PodStatusMap map[corev1.PodPhase][]string - // ComponentStatus defines the status of each component. type ComponentStatus struct { // Compactor is a map to the pod status of the compactor pod. @@ -260,7 +257,7 @@ type ComponentStatus struct { // // +optional // +kubebuilder:validation:Optional - // +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors="urn:alm:descriptor:com.tectonic.ui:podStatuses",displayName="Query Frontend",order=4 + // +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors="urn:alm:descriptor:com.tectonic.ui:podStatuses",displayName="Gateway",order=6 Gateway PodStatusMap `json:"gateway"` } diff --git a/apis/tempo/v1alpha1/zz_generated.deepcopy.go b/apis/tempo/v1alpha1/zz_generated.deepcopy.go index 8db58a081..48d24c86b 100644 --- a/apis/tempo/v1alpha1/zz_generated.deepcopy.go +++ b/apis/tempo/v1alpha1/zz_generated.deepcopy.go @@ -366,6 +366,36 @@ 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 *MonolithicComponentStatus) DeepCopyInto(out *MonolithicComponentStatus) { + *out = *in + if in.Tempo != nil { + in, out := &in.Tempo, &out.Tempo + *out = make(PodStatusMap, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonolithicComponentStatus. +func (in *MonolithicComponentStatus) DeepCopy() *MonolithicComponentStatus { + if in == nil { + return nil + } + out := new(MonolithicComponentStatus) + in.DeepCopyInto(out) + 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 @@ -1013,7 +1043,7 @@ func (in *TempoMonolithic) DeepCopyInto(out *TempoMonolithic) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TempoMonolithic. @@ -1109,6 +1139,14 @@ func (in *TempoMonolithicSpec) DeepCopy() *TempoMonolithicSpec { // 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 + in.Components.DeepCopyInto(&out.Components) + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TempoMonolithicStatus. diff --git a/bundle/community/manifests/tempo-operator.clusterserviceversion.yaml b/bundle/community/manifests/tempo-operator.clusterserviceversion.yaml index a2df9ada9..cbc89efc8 100644 --- a/bundle/community/manifests/tempo-operator.clusterserviceversion.yaml +++ b/bundle/community/manifests/tempo-operator.clusterserviceversion.yaml @@ -74,7 +74,7 @@ metadata: capabilities: Deep Insights categories: Logging & Tracing,Monitoring containerImage: ghcr.io/grafana/tempo-operator/tempo-operator:v0.8.0 - createdAt: "2024-02-02T17:31:35Z" + createdAt: "2024-02-08T13:27:01Z" description: Create and manage deployments of Tempo, a high-scale distributed tracing backend. operatorframework.io/cluster-monitoring: "true" @@ -321,6 +321,17 @@ spec: path: storage.traces.size x-descriptors: - urn:alm:descriptor:com.tectonic.ui:text + statusDescriptors: + - description: Tempo is a map of the pod status of the Tempo pods. + displayName: Tempo + path: components.tempo + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:podStatuses + - description: Conditions of the Tempo deployment health. + displayName: Conditions + path: conditions + x-descriptors: + - urn:alm:descriptor:io.kubernetes.conditions version: v1alpha1 - description: TempoStack manages a Tempo deployment in microservices mode. displayName: TempoStack @@ -864,12 +875,6 @@ spec: path: components.querier x-descriptors: - urn:alm:descriptor:com.tectonic.ui:podStatuses - - description: Gateway is a map to the per pod status of the query frontend - deployment - displayName: Query Frontend - path: components.gateway - x-descriptors: - - urn:alm:descriptor:com.tectonic.ui:podStatuses - description: QueryFrontend is a map to the per pod status of the query frontend deployment displayName: Query Frontend @@ -881,6 +886,12 @@ spec: path: components.compactor x-descriptors: - urn:alm:descriptor:com.tectonic.ui:podStatuses + - description: Gateway is a map to the per pod status of the query frontend + deployment + displayName: Gateway + path: components.gateway + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:podStatuses - description: Conditions of the Tempo deployment health. displayName: Conditions path: conditions diff --git a/bundle/community/manifests/tempo.grafana.com_tempomonolithics.yaml b/bundle/community/manifests/tempo.grafana.com_tempomonolithics.yaml index 194d5e9c6..8d928473e 100644 --- a/bundle/community/manifests/tempo.grafana.com_tempomonolithics.yaml +++ b/bundle/community/manifests/tempo.grafana.com_tempomonolithics.yaml @@ -401,6 +401,88 @@ spec: type: object status: description: TempoMonolithicStatus defines the observed state of TempoMonolithic. + properties: + components: + description: Components provides summary of all Tempo pod status, + grouped per component. + properties: + tempo: + additionalProperties: + items: + type: string + type: array + description: Tempo is a map of the pod status of the Tempo pods. + type: object + type: object + conditions: + description: Conditions of the Tempo deployment health. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array type: object type: object served: true diff --git a/bundle/openshift/manifests/tempo-operator.clusterserviceversion.yaml b/bundle/openshift/manifests/tempo-operator.clusterserviceversion.yaml index f48d9f302..f90bce24e 100644 --- a/bundle/openshift/manifests/tempo-operator.clusterserviceversion.yaml +++ b/bundle/openshift/manifests/tempo-operator.clusterserviceversion.yaml @@ -74,7 +74,7 @@ metadata: capabilities: Deep Insights categories: Logging & Tracing,Monitoring containerImage: ghcr.io/grafana/tempo-operator/tempo-operator:v0.8.0 - createdAt: "2024-02-02T17:31:33Z" + createdAt: "2024-02-08T13:26:59Z" description: Create and manage deployments of Tempo, a high-scale distributed tracing backend. operatorframework.io/cluster-monitoring: "true" @@ -321,6 +321,17 @@ spec: path: storage.traces.size x-descriptors: - urn:alm:descriptor:com.tectonic.ui:text + statusDescriptors: + - description: Tempo is a map of the pod status of the Tempo pods. + displayName: Tempo + path: components.tempo + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:podStatuses + - description: Conditions of the Tempo deployment health. + displayName: Conditions + path: conditions + x-descriptors: + - urn:alm:descriptor:io.kubernetes.conditions version: v1alpha1 - description: TempoStack manages a Tempo deployment in microservices mode. displayName: TempoStack @@ -864,12 +875,6 @@ spec: path: components.querier x-descriptors: - urn:alm:descriptor:com.tectonic.ui:podStatuses - - description: Gateway is a map to the per pod status of the query frontend - deployment - displayName: Query Frontend - path: components.gateway - x-descriptors: - - urn:alm:descriptor:com.tectonic.ui:podStatuses - description: QueryFrontend is a map to the per pod status of the query frontend deployment displayName: Query Frontend @@ -881,6 +886,12 @@ spec: path: components.compactor x-descriptors: - urn:alm:descriptor:com.tectonic.ui:podStatuses + - description: Gateway is a map to the per pod status of the query frontend + deployment + displayName: Gateway + path: components.gateway + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:podStatuses - description: Conditions of the Tempo deployment health. displayName: Conditions path: conditions diff --git a/bundle/openshift/manifests/tempo.grafana.com_tempomonolithics.yaml b/bundle/openshift/manifests/tempo.grafana.com_tempomonolithics.yaml index 194d5e9c6..8d928473e 100644 --- a/bundle/openshift/manifests/tempo.grafana.com_tempomonolithics.yaml +++ b/bundle/openshift/manifests/tempo.grafana.com_tempomonolithics.yaml @@ -401,6 +401,88 @@ spec: type: object status: description: TempoMonolithicStatus defines the observed state of TempoMonolithic. + properties: + components: + description: Components provides summary of all Tempo pod status, + grouped per component. + properties: + tempo: + additionalProperties: + items: + type: string + type: array + description: Tempo is a map of the pod status of the Tempo pods. + type: object + type: object + conditions: + description: Conditions of the Tempo deployment health. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array type: object type: object served: true diff --git a/config/crd/bases/tempo.grafana.com_tempomonolithics.yaml b/config/crd/bases/tempo.grafana.com_tempomonolithics.yaml index a4b56bdc8..000fc6b94 100644 --- a/config/crd/bases/tempo.grafana.com_tempomonolithics.yaml +++ b/config/crd/bases/tempo.grafana.com_tempomonolithics.yaml @@ -398,6 +398,88 @@ spec: type: object status: description: TempoMonolithicStatus defines the observed state of TempoMonolithic. + properties: + components: + description: Components provides summary of all Tempo pod status, + grouped per component. + properties: + tempo: + additionalProperties: + items: + type: string + type: array + description: Tempo is a map of the pod status of the Tempo pods. + type: object + type: object + conditions: + description: Conditions of the Tempo deployment health. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array type: object type: object served: true diff --git a/config/manifests/community/bases/tempo-operator.clusterserviceversion.yaml b/config/manifests/community/bases/tempo-operator.clusterserviceversion.yaml index bd5bcc9e0..15c624b2b 100644 --- a/config/manifests/community/bases/tempo-operator.clusterserviceversion.yaml +++ b/config/manifests/community/bases/tempo-operator.clusterserviceversion.yaml @@ -250,6 +250,17 @@ spec: path: storage.traces.size x-descriptors: - urn:alm:descriptor:com.tectonic.ui:text + statusDescriptors: + - description: Tempo is a map of the pod status of the Tempo pods. + displayName: Tempo + path: components.tempo + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:podStatuses + - description: Conditions of the Tempo deployment health. + displayName: Conditions + path: conditions + x-descriptors: + - urn:alm:descriptor:io.kubernetes.conditions version: v1alpha1 - description: TempoStack manages a Tempo deployment in microservices mode. displayName: TempoStack @@ -793,12 +804,6 @@ spec: path: components.querier x-descriptors: - urn:alm:descriptor:com.tectonic.ui:podStatuses - - description: Gateway is a map to the per pod status of the query frontend - deployment - displayName: Query Frontend - path: components.gateway - x-descriptors: - - urn:alm:descriptor:com.tectonic.ui:podStatuses - description: QueryFrontend is a map to the per pod status of the query frontend deployment displayName: Query Frontend @@ -810,6 +815,12 @@ spec: path: components.compactor x-descriptors: - urn:alm:descriptor:com.tectonic.ui:podStatuses + - description: Gateway is a map to the per pod status of the query frontend + deployment + displayName: Gateway + path: components.gateway + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:podStatuses - description: Conditions of the Tempo deployment health. displayName: Conditions path: conditions diff --git a/config/manifests/openshift/bases/tempo-operator.clusterserviceversion.yaml b/config/manifests/openshift/bases/tempo-operator.clusterserviceversion.yaml index 6aee13bd6..470661b96 100644 --- a/config/manifests/openshift/bases/tempo-operator.clusterserviceversion.yaml +++ b/config/manifests/openshift/bases/tempo-operator.clusterserviceversion.yaml @@ -250,6 +250,17 @@ spec: path: storage.traces.size x-descriptors: - urn:alm:descriptor:com.tectonic.ui:text + statusDescriptors: + - description: Tempo is a map of the pod status of the Tempo pods. + displayName: Tempo + path: components.tempo + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:podStatuses + - description: Conditions of the Tempo deployment health. + displayName: Conditions + path: conditions + x-descriptors: + - urn:alm:descriptor:io.kubernetes.conditions version: v1alpha1 - description: TempoStack manages a Tempo deployment in microservices mode. displayName: TempoStack @@ -793,12 +804,6 @@ spec: path: components.querier x-descriptors: - urn:alm:descriptor:com.tectonic.ui:podStatuses - - description: Gateway is a map to the per pod status of the query frontend - deployment - displayName: Query Frontend - path: components.gateway - x-descriptors: - - urn:alm:descriptor:com.tectonic.ui:podStatuses - description: QueryFrontend is a map to the per pod status of the query frontend deployment displayName: Query Frontend @@ -810,6 +815,12 @@ spec: path: components.compactor x-descriptors: - urn:alm:descriptor:com.tectonic.ui:podStatuses + - description: Gateway is a map to the per pod status of the query frontend + deployment + displayName: Gateway + path: components.gateway + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:podStatuses - description: Conditions of the Tempo deployment health. displayName: Conditions path: conditions diff --git a/controllers/tempo/common.go b/controllers/tempo/common.go index c28cd6763..29d693752 100644 --- a/controllers/tempo/common.go +++ b/controllers/tempo/common.go @@ -6,7 +6,6 @@ import ( "fmt" "strings" - "github.com/go-logr/logr" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -16,6 +15,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" "github.com/grafana/tempo-operator/internal/manifests" ) @@ -33,13 +33,13 @@ func isNamespaceScoped(obj client.Object) bool { // 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 { + log := log.FromContext(ctx) pruneObjects := ownedObjects // Create or update all objects managed by the operator diff --git a/controllers/tempo/tempomonolithic_controller.go b/controllers/tempo/tempomonolithic_controller.go index b182317ea..a0012218f 100644 --- a/controllers/tempo/tempomonolithic_controller.go +++ b/controllers/tempo/tempomonolithic_controller.go @@ -2,7 +2,6 @@ package controllers import ( "context" - "errors" "fmt" grafanav1 "github.com/grafana-operator/grafana-operator/v5/api/v1beta1" @@ -17,12 +16,12 @@ import ( "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/handlers/storage" "github.com/grafana/tempo-operator/internal/manifests/monolithic" + "github.com/grafana/tempo-operator/internal/status" ) // TempoMonolithicReconciler reconciles a TempoMonolithic object. @@ -39,7 +38,8 @@ type TempoMonolithicReconciler struct { // 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 := ctrl.LoggerFrom(ctx).WithName("tempomonolithic-reconcile") + ctx = ctrl.LoggerInto(ctx, log) log.V(1).Info("starting reconcile loop") defer log.V(1).Info("finished reconcile loop") @@ -65,9 +65,24 @@ func (r *TempoMonolithicReconciler) Reconcile(ctx context.Context, req ctrl.Requ return ctrl.Result{}, nil } + err := r.createOrUpdate(ctx, tempo) + if err != nil { + return ctrl.Result{}, status.HandleTempoMonolithicStatus(ctx, r.Client, tempo, err) + } + + // Note: controller-runtime will always requeue a reconcile if Reconcile() returns any error except TerminalError. + // Result.Requeue and Result.RequeueAfter are only respected if err == nil + // https://github.com/kubernetes-sigs/controller-runtime/blob/v0.15.0/pkg/internal/controller/controller.go#L315-L341 + return ctrl.Result{}, status.HandleTempoMonolithicStatus(ctx, r.Client, tempo, nil) +} + +func (r *TempoMonolithicReconciler) createOrUpdate(ctx context.Context, tempo v1alpha1.TempoMonolithic) error { storageParams, errs := storage.GetStorageParamsForTempoMonolithic(ctx, r.Client, tempo) if len(errs) > 0 { - return ctrl.Result{}, errors.New(listFieldErrors(errs)) + return &status.ConfigurationError{ + Reason: v1alpha1.ReasonInvalidStorageConfig, + Message: listFieldErrors(errs), + } } managedObjects, err := monolithic.BuildAll(monolithic.Options{ @@ -76,20 +91,15 @@ func (r *TempoMonolithicReconciler) Reconcile(ctx context.Context, req ctrl.Requ StorageParams: storageParams, }) if err != nil { - return ctrl.Result{}, fmt.Errorf("error building manifests: %w", err) + return 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 err } - return ctrl.Result{}, nil + return reconcileManagedObjects(ctx, r.Client, &tempo, r.Scheme, managedObjects, ownedObjects) } func (r *TempoMonolithicReconciler) getOwnedObjects(ctx context.Context, tempo v1alpha1.TempoMonolithic) (map[types.UID]client.Object, error) { diff --git a/controllers/tempo/tempostack_controller.go b/controllers/tempo/tempostack_controller.go index 6f0bd87ab..fa8eb714f 100644 --- a/controllers/tempo/tempostack_controller.go +++ b/controllers/tempo/tempostack_controller.go @@ -64,7 +64,8 @@ type TempoStackReconciler struct { // 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 *TempoStackReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - log := ctrl.LoggerFrom(ctx).WithName("tempostack-reconcile").WithValues("tempo", req.NamespacedName) + log := ctrl.LoggerFrom(ctx).WithName("tempostack-reconcile") + ctx = ctrl.LoggerInto(ctx, log) log.V(1).Info("starting reconcile loop") defer log.V(1).Info("finished reconcile loop") diff --git a/controllers/tempo/tempostack_create_or_update.go b/controllers/tempo/tempostack_create_or_update.go index 70a14afb4..646391968 100644 --- a/controllers/tempo/tempostack_create_or_update.go +++ b/controllers/tempo/tempostack_create_or_update.go @@ -102,7 +102,7 @@ func (r *TempoStackReconciler) createOrUpdate(ctx context.Context, log logr.Logg return err } - err = reconcileManagedObjects(ctx, log, r.Client, &tempo, r.Scheme, managedObjects, ownedObjects) + err = reconcileManagedObjects(ctx, r.Client, &tempo, r.Scheme, managedObjects, ownedObjects) if err != nil { return err } diff --git a/docs/operator/api.md b/docs/operator/api.md index f0a1d8bdb..619f3f5ac 100644 --- a/docs/operator/api.md +++ b/docs/operator/api.md @@ -1560,6 +1560,66 @@ using an in-process OpenPolicyAgent Rego authorizer.

+## MonolithicComponentStatus { #tempo-grafana-com-v1alpha1-MonolithicComponentStatus } + +

+ +(Appears on:TempoMonolithicStatus) + +

+ +
+ +

MonolithicComponentStatus defines the status of each component.

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+ +tempo
+ + + + + +PodStatusMap + + + + + +
+ +(Optional) + +

Tempo is a map of the pod status of the Tempo pods.

+ +
+ ## MonolithicIngestionOTLPProtocolsGRPCSpec { #tempo-grafana-com-v1alpha1-MonolithicIngestionOTLPProtocolsGRPCSpec }

@@ -3169,7 +3229,7 @@ GrafanaConfigSpec

-(Appears on:ComponentStatus) +(Appears on:ComponentStatus, MonolithicComponentStatus)

@@ -4860,6 +4920,75 @@ ExtraConfigSpec + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+ +components
+ + + + + +MonolithicComponentStatus + + + + + +
+ +

Components provides summary of all Tempo pod status, grouped per component.

+ +
+ +conditions
+ + + + + +[]Kubernetes meta/v1.Condition + + + + + +
+ +

Conditions of the Tempo deployment health.

+ +
+ ## TempoQueryFrontendSpec { #tempo-grafana-com-v1alpha1-TempoQueryFrontendSpec }

diff --git a/docs/spec/tempo.grafana.com_tempomonolithics.yaml b/docs/spec/tempo.grafana.com_tempomonolithics.yaml index 4b4c4ff49..8b4ab206d 100644 --- a/docs/spec/tempo.grafana.com_tempomonolithics.yaml +++ b/docs/spec/tempo.grafana.com_tempomonolithics.yaml @@ -70,3 +70,14 @@ spec: # TempoMonolithicSpec defines the desir minVersion: "" # MinVersion defines the minimum acceptable TLS version. size: "10Gi" # Size defines the size of the volume where traces are stored. For in-memory storage, this defines the size of the tmpfs volume. For persistent volume storage, this defines the size of the persistent volume. For object storage, this defines the size of the persistent volume containing the Write-Ahead Log (WAL) of Tempo. Default: 10Gi. status: # TempoMonolithicStatus defines the observed state of TempoMonolithic. + components: # Components provides summary of all Tempo pod status, grouped per component. + tempo: # Tempo is a map of the pod status of the Tempo pods. + "key": + - "" + conditions: # Conditions of the Tempo deployment health. + - lastTransitionTime: "" # lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + message: "" # message is a human readable message indicating details about the transition. This may be an empty string. + observedGeneration: 0 # observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. + reason: "" # reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. + status: "" # status of the condition, one of True, False, Unknown. + type: "" # type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) diff --git a/internal/status/conditions.go b/internal/status/conditions.go index 49ce91387..c5f9127f7 100644 --- a/internal/status/conditions.go +++ b/internal/status/conditions.go @@ -10,8 +10,8 @@ import ( const ( messageReady = "All components are operational" - messageFailed = "Some TempoStack components failed" - messagePending = "Some TempoStack components are pending on dependencies" + messageFailed = "Some Tempo components failed" + messagePending = "Some Tempo components are pending on dependencies" ) // ConfigurationError contains information about why the managed TempoStack has an invalid configuration. diff --git a/internal/status/metrics.go b/internal/status/metrics.go new file mode 100644 index 000000000..9d9537fcf --- /dev/null +++ b/internal/status/metrics.go @@ -0,0 +1,42 @@ +package status + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/metrics" + + "github.com/grafana/tempo-operator/apis/tempo/v1alpha1" +) + +var ( + metricTempoStackStatusCondition = promauto.With(metrics.Registry).NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "tempostack", + Name: "status_condition", + Help: "The status condition of a TempoStack instance.", + }, []string{"stack_namespace", "stack_name", "condition"}) + metricTempoMonolithicStatusCondition = promauto.With(metrics.Registry).NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "tempomonolithic", + Name: "status_condition", + Help: "The status condition of a TempoMonolithic instance.", + }, []string{"stack_namespace", "stack_name", "condition"}) +) + +func updateMetrics(metric *prometheus.GaugeVec, conditions []metav1.Condition, namespace string, name string) { + // Update all status condition metrics. + // In some cases not all status conditions are present in the status.Conditions list, for example: + // A TempoStack CR gets created with an invalid storage secret (creating an ConfigurationError status condition). + // Later this CR is deleted, a storage secret is created and a new TempoStack instance is created. + // Then this TempoStack instance doesn't have the ConfigurationError condition in the status.Conditions list. + activeConditions := map[string]float64{} + for _, cond := range conditions { + if cond.Status == metav1.ConditionTrue { + activeConditions[cond.Type] = 1 + } + } + for _, cond := range v1alpha1.AllStatusConditions { + condStr := string(cond) + isActive := activeConditions[condStr] // isActive will be 0 if the condition is not found in the map + metric.WithLabelValues(namespace, name, condStr).Set(isActive) + } +} diff --git a/internal/status/monolithic.go b/internal/status/monolithic.go new file mode 100644 index 000000000..085605949 --- /dev/null +++ b/internal/status/monolithic.go @@ -0,0 +1,216 @@ +package status + +import ( + "context" + "errors" + "fmt" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/grafana/tempo-operator/apis/tempo/v1alpha1" + "github.com/grafana/tempo-operator/internal/manifests/monolithic" +) + +func isPodReady(pod corev1.Pod) bool { + for _, c := range pod.Status.ContainerStatuses { + if !c.Ready { + return false + } + } + return true +} + +func getStatefulSetStatus(ctx context.Context, c client.Client, namespace string, name string) (v1alpha1.PodStatusMap, error) { + psm := v1alpha1.PodStatusMap{} + + opts := []client.ListOption{ + client.MatchingLabels(monolithic.Labels(name)), + client.InNamespace(namespace), + } + + // After creation of a StatefulSet, but before the Pods are created, the list of Pods is empty + // and therefore no Pod is in pending phase. However, this does not reflect the actual state, + // therefore we additionally check if the StatefulSet has the required number of readyReplicas. + // + // This additional check also helps with Pods in terminating state, which otherwise would show up + // as Pods with PodPhase = Running. + stss := &appsv1.StatefulSetList{} + err := c.List(ctx, stss, opts...) + if err != nil { + return nil, err + } + for _, sts := range stss.Items { + if sts.Status.ReadyReplicas < ptr.Deref(sts.Spec.Replicas, 1) { + psm[corev1.PodPending] = append(psm[corev1.PodPending], sts.Name) + return psm, nil + } + } + + pods := &corev1.PodList{} + err = c.List(ctx, pods, opts...) + if err != nil { + return nil, err + } + for _, pod := range pods.Items { + phase := pod.Status.Phase + if phase == corev1.PodRunning { + // for the component status consider running, but not ready, pods as pending + if !isPodReady(pod) { + phase = corev1.PodPending + } + } + psm[phase] = append(psm[phase], pod.Name) + } + + return psm, nil +} + +func getComponentsStatus(ctx context.Context, client client.Client, tempo v1alpha1.TempoMonolithic) (v1alpha1.MonolithicComponentStatus, error) { + var err error + components := v1alpha1.MonolithicComponentStatus{} + + components.Tempo, err = getStatefulSetStatus(ctx, client, tempo.Namespace, tempo.Name) + if err != nil { + return v1alpha1.MonolithicComponentStatus{}, fmt.Errorf("cannot get pod status: %w", err) + } + + return components, nil +} + +func conditionStatus(active bool) metav1.ConditionStatus { + if active { + return metav1.ConditionTrue + } else { + return metav1.ConditionFalse + } +} + +// resetCondition disables the condition if it exists already (without changing any other field of the condition), +// otherwise creates a new disabled condition with a specified reason. +func resetCondition(conditions []metav1.Condition, conditionType v1alpha1.ConditionStatus, defaultReason v1alpha1.ConditionReason) metav1.Condition { + existingCondition := meta.FindStatusCondition(conditions, string(conditionType)) + if existingCondition != nil { + // do not modify the condition struct of the slice, otherwise + // meta.SetStatusCondition() won't update the last transition time + condition := existingCondition.DeepCopy() + condition.Status = metav1.ConditionFalse + return *condition + } else { + return metav1.Condition{ + Type: string(conditionType), + Reason: string(defaultReason), + Status: metav1.ConditionFalse, + } + } +} + +func updateConditions(conditions *[]metav1.Condition, componentsStatus v1alpha1.MonolithicComponentStatus, reconcileError error) bool { + isTerminalError := false + + // set PendingComponents condition if any pod of any component is in pending phase (or running but not ready) + pending := metav1.Condition{ + Type: string(v1alpha1.ConditionPending), + Reason: string(v1alpha1.ReasonPendingComponents), + Message: messagePending, + Status: conditionStatus( + len(componentsStatus.Tempo[corev1.PodPending]) > 0, + ), + } + + // set ConfigurationError condition if the reconcile function returned a ConfigurationError + var configurationError metav1.Condition + var cerr *ConfigurationError + if errors.As(reconcileError, &cerr) { + configurationError = metav1.Condition{ + Type: string(v1alpha1.ConditionConfigurationError), + Reason: string(cerr.Reason), + Message: cerr.Message, + Status: metav1.ConditionTrue, + } + isTerminalError = true + } else { + configurationError = resetCondition(*conditions, v1alpha1.ConditionConfigurationError, v1alpha1.ReasonInvalidStorageConfig) + } + + // set Failed condition if the reconcile function returned any error other than ConfigurationError, + // or if any pod of any component is in failed phase + var failed metav1.Condition + if reconcileError != nil && cerr == nil { + failed = metav1.Condition{ + Type: string(v1alpha1.ConditionFailed), + Reason: string(v1alpha1.ReasonFailedReconciliation), + Message: reconcileError.Error(), + Status: metav1.ConditionTrue, + } + } else if len(componentsStatus.Tempo[corev1.PodFailed]) > 0 { + failed = metav1.Condition{ + Type: string(v1alpha1.ConditionFailed), + Reason: string(v1alpha1.ReasonFailedComponents), + Message: messageFailed, + Status: metav1.ConditionTrue, + } + } else { + failed = resetCondition(*conditions, v1alpha1.ConditionFailed, v1alpha1.ReasonFailedComponents) + } + + // set Ready condition if all above conditions are false + ready := metav1.Condition{ + Type: string(v1alpha1.ConditionReady), + Reason: string(v1alpha1.ReasonReady), + Message: messageReady, + Status: conditionStatus( + pending.Status == metav1.ConditionFalse && + failed.Status == metav1.ConditionFalse && + configurationError.Status == metav1.ConditionFalse, + ), + } + + meta.SetStatusCondition(conditions, pending) + meta.SetStatusCondition(conditions, configurationError) + meta.SetStatusCondition(conditions, failed) + meta.SetStatusCondition(conditions, ready) + return isTerminalError +} + +func patchStatus(ctx context.Context, c client.Client, original v1alpha1.TempoMonolithic, status v1alpha1.TempoMonolithicStatus) error { + patch := client.MergeFrom(&original) + updated := original.DeepCopy() + updated.Status = status + return c.Status().Patch(ctx, updated, patch) +} + +// HandleTempoMonolithicStatus updates the .status field of a TempoMonolithic CR +// Status Conditions API conventions: https://github.com/kubernetes/community/blob/c04227d209633696ad49d7f4546fc8cfd9c660ab/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties +func HandleTempoMonolithicStatus(ctx context.Context, client client.Client, tempo v1alpha1.TempoMonolithic, reconcileError error) error { + var err error + log := ctrl.LoggerFrom(ctx) + status := *tempo.Status.DeepCopy() + + status.Components, err = getComponentsStatus(ctx, client, tempo) + if err != nil { + log.Error(err, "could not get status of each component") + } + + isTerminalError := updateConditions(&status.Conditions, status.Components, reconcileError) + if isTerminalError { + // wrap error in reconcile.TerminalError to indicate human intervention is required + // and the request should not be requeued. + reconcileError = reconcile.TerminalError(reconcileError) + } + + updateMetrics(metricTempoMonolithicStatusCondition, status.Conditions, tempo.Namespace, tempo.Name) + + err = patchStatus(ctx, client, tempo, status) + if err != nil { + return err + } + + return reconcileError +} diff --git a/internal/status/monolithic_test.go b/internal/status/monolithic_test.go new file mode 100644 index 000000000..eef07455c --- /dev/null +++ b/internal/status/monolithic_test.go @@ -0,0 +1,404 @@ +package status + +import ( + "context" + "errors" + "fmt" + "testing" + + "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/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/grafana/tempo-operator/apis/tempo/v1alpha1" +) + +func TestGetStatefulSetStatus(t *testing.T) { + tests := []struct { + name string + client client.Client + expected v1alpha1.PodStatusMap + }{ + { + name: "sts rolling out", + client: &k8sFake{ + stss: &appsv1.StatefulSetList{ + Items: []appsv1.StatefulSet{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tempo", + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: ptr.To[int32](1), + }, + Status: appsv1.StatefulSetStatus{ + ReadyReplicas: 0, + }, + }}, + }, + }, + expected: map[corev1.PodPhase][]string{ + corev1.PodPending: {"tempo"}, + }, + }, + { + name: "pod pending", + client: &k8sFake{ + stss: &appsv1.StatefulSetList{ + Items: []appsv1.StatefulSet{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tempo", + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: ptr.To[int32](1), + }, + Status: appsv1.StatefulSetStatus{ + ReadyReplicas: 1, + }, + }}, + }, + pods: &corev1.PodList{ + Items: []corev1.Pod{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tempo-xyz", + }, + Status: corev1.PodStatus{ + Phase: corev1.PodPending, + }, + }}, + }, + }, + expected: map[corev1.PodPhase][]string{ + corev1.PodPending: {"tempo-xyz"}, + }, + }, + { + name: "pod running but not ready", + client: &k8sFake{ + stss: &appsv1.StatefulSetList{ + Items: []appsv1.StatefulSet{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tempo", + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: ptr.To[int32](1), + }, + Status: appsv1.StatefulSetStatus{ + ReadyReplicas: 1, + }, + }}, + }, + pods: &corev1.PodList{ + Items: []corev1.Pod{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tempo-xyz", + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + ContainerStatuses: []corev1.ContainerStatus{{ + Ready: false, + }}, + }, + }}, + }, + }, + expected: map[corev1.PodPhase][]string{ + corev1.PodPending: {"tempo-xyz"}, + }, + }, + { + name: "pod running and ready", + client: &k8sFake{ + stss: &appsv1.StatefulSetList{ + Items: []appsv1.StatefulSet{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tempo", + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: ptr.To[int32](1), + }, + Status: appsv1.StatefulSetStatus{ + ReadyReplicas: 1, + }, + }}, + }, + pods: &corev1.PodList{ + Items: []corev1.Pod{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tempo-xyz", + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + ContainerStatuses: []corev1.ContainerStatus{{ + Ready: true, + }}, + }, + }}, + }, + }, + expected: map[corev1.PodPhase][]string{ + corev1.PodRunning: {"tempo-xyz"}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + psm, err := getStatefulSetStatus(context.Background(), tc.client, "", "") + require.NoError(t, err) + require.Equal(t, tc.expected, psm) + }) + } +} + +func TestUpdateConditions(t *testing.T) { + tests := []struct { + name string + conditions []metav1.Condition + componentsStatus v1alpha1.MonolithicComponentStatus + reconcileError error + expectedConditions []metav1.Condition + expectedIsTerminalErr bool + }{ + { + name: "pod pending", + componentsStatus: v1alpha1.MonolithicComponentStatus{ + Tempo: v1alpha1.PodStatusMap{ + corev1.PodPending: []string{"tempo-1"}, + }, + }, + expectedConditions: []metav1.Condition{ + { + Type: string(v1alpha1.ConditionPending), + Reason: string(v1alpha1.ReasonPendingComponents), + Message: messagePending, + Status: metav1.ConditionTrue, + }, + { + Type: string(v1alpha1.ConditionConfigurationError), + Reason: string(v1alpha1.ReasonInvalidStorageConfig), + Status: metav1.ConditionFalse, + }, + { + Type: string(v1alpha1.ConditionFailed), + Reason: string(v1alpha1.ReasonFailedComponents), + Message: "", + Status: metav1.ConditionFalse, + }, + { + Type: string(v1alpha1.ConditionReady), + Reason: string(v1alpha1.ReasonReady), + Message: messageReady, + Status: metav1.ConditionFalse, + }, + }, + }, + { + name: "pod failed", + componentsStatus: v1alpha1.MonolithicComponentStatus{ + Tempo: v1alpha1.PodStatusMap{ + corev1.PodFailed: []string{"tempo-1"}, + }, + }, + expectedConditions: []metav1.Condition{ + { + Type: string(v1alpha1.ConditionPending), + Reason: string(v1alpha1.ReasonPendingComponents), + Message: messagePending, + Status: metav1.ConditionFalse, + }, + { + Type: string(v1alpha1.ConditionConfigurationError), + Reason: string(v1alpha1.ReasonInvalidStorageConfig), + Status: metav1.ConditionFalse, + }, + { + Type: string(v1alpha1.ConditionFailed), + Reason: string(v1alpha1.ReasonFailedComponents), + Message: messageFailed, + Status: metav1.ConditionTrue, + }, + { + Type: string(v1alpha1.ConditionReady), + Reason: string(v1alpha1.ReasonReady), + Message: messageReady, + Status: metav1.ConditionFalse, + }, + }, + }, + { + name: "configuration error", + componentsStatus: v1alpha1.MonolithicComponentStatus{ + Tempo: v1alpha1.PodStatusMap{ + corev1.PodRunning: []string{"tempo-1"}, + }, + }, + reconcileError: &ConfigurationError{ + Reason: v1alpha1.ReasonInvalidStorageConfig, + Message: "cannot get secret: abc", + }, + expectedConditions: []metav1.Condition{ + { + Type: string(v1alpha1.ConditionPending), + Reason: string(v1alpha1.ReasonPendingComponents), + Message: messagePending, + Status: metav1.ConditionFalse, + }, + { + Type: string(v1alpha1.ConditionConfigurationError), + Reason: string(v1alpha1.ReasonInvalidStorageConfig), + Message: "cannot get secret: abc", + Status: metav1.ConditionTrue, + }, + { + Type: string(v1alpha1.ConditionFailed), + Reason: string(v1alpha1.ReasonFailedComponents), + Message: "", + Status: metav1.ConditionFalse, + }, + { + Type: string(v1alpha1.ConditionReady), + Reason: string(v1alpha1.ReasonReady), + Message: messageReady, + Status: metav1.ConditionFalse, + }, + }, + expectedIsTerminalErr: true, + }, + { + name: "transition from configuration error to no error", + conditions: []metav1.Condition{ + { + Type: string(v1alpha1.ConditionPending), + Reason: string(v1alpha1.ReasonPendingComponents), + Message: messagePending, + Status: metav1.ConditionFalse, + }, + { + Type: string(v1alpha1.ConditionConfigurationError), + Reason: string(v1alpha1.ReasonInvalidStorageConfig), + Message: "cannot get secret: abc", + Status: metav1.ConditionTrue, + }, + { + Type: string(v1alpha1.ConditionFailed), + Reason: string(v1alpha1.ReasonFailedComponents), + Message: "", + Status: metav1.ConditionFalse, + }, + { + Type: string(v1alpha1.ConditionReady), + Reason: string(v1alpha1.ReasonReady), + Message: messageReady, + Status: metav1.ConditionTrue, + }, + }, + componentsStatus: v1alpha1.MonolithicComponentStatus{ + Tempo: v1alpha1.PodStatusMap{ + corev1.PodRunning: []string{"tempo-1"}, + }, + }, + expectedConditions: []metav1.Condition{ + { + Type: string(v1alpha1.ConditionPending), + Reason: string(v1alpha1.ReasonPendingComponents), + Message: messagePending, + Status: metav1.ConditionFalse, + }, + { + Type: string(v1alpha1.ConditionConfigurationError), + Reason: string(v1alpha1.ReasonInvalidStorageConfig), + Message: "cannot get secret: abc", + Status: metav1.ConditionFalse, + }, + { + Type: string(v1alpha1.ConditionFailed), + Reason: string(v1alpha1.ReasonFailedComponents), + Message: "", + Status: metav1.ConditionFalse, + }, + { + Type: string(v1alpha1.ConditionReady), + Reason: string(v1alpha1.ReasonReady), + Message: messageReady, + Status: metav1.ConditionTrue, + }, + }, + }, + { + name: "other reconcile error", + componentsStatus: v1alpha1.MonolithicComponentStatus{ + Tempo: v1alpha1.PodStatusMap{ + corev1.PodRunning: []string{"tempo-1"}, + }, + }, + reconcileError: errors.New("permission denied"), + expectedConditions: []metav1.Condition{ + { + Type: string(v1alpha1.ConditionPending), + Reason: string(v1alpha1.ReasonPendingComponents), + Message: messagePending, + Status: metav1.ConditionFalse, + }, + { + Type: string(v1alpha1.ConditionConfigurationError), + Reason: string(v1alpha1.ReasonInvalidStorageConfig), + Status: metav1.ConditionFalse, + }, + { + Type: string(v1alpha1.ConditionFailed), + Reason: string(v1alpha1.ReasonFailedReconciliation), + Message: "permission denied", + Status: metav1.ConditionTrue, + }, + { + Type: string(v1alpha1.ConditionReady), + Reason: string(v1alpha1.ReasonReady), + Message: messageReady, + Status: metav1.ConditionFalse, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + updatedConditions := make([]metav1.Condition, len(tc.conditions)) + _ = copy(updatedConditions, tc.conditions) + + isTerminalErr := updateConditions(&updatedConditions, tc.componentsStatus, tc.reconcileError) + require.Equal(t, tc.expectedIsTerminalErr, isTerminalErr) + + // ignore times + for i := range updatedConditions { + updatedConditions[i].LastTransitionTime = metav1.Time{} + } + + require.Equal(t, tc.expectedConditions, updatedConditions) + }) + } +} + +type k8sFake struct { + client.Client + stss *appsv1.StatefulSetList + pods *corev1.PodList +} + +func (k *k8sFake) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + switch typed := list.(type) { + case *appsv1.StatefulSetList: + if k.stss != nil { + k.stss.DeepCopyInto(typed) + return nil + } + case *corev1.PodList: + if k.pods != nil { + k.pods.DeepCopyInto(typed) + return nil + } + } + return fmt.Errorf("mock: not implemented") +} diff --git a/internal/status/status.go b/internal/status/status.go index 4e38a038f..04800099e 100644 --- a/internal/status/status.go +++ b/internal/status/status.go @@ -3,23 +3,10 @@ package status import ( "context" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/metrics" - "github.com/grafana/tempo-operator/apis/tempo/v1alpha1" "github.com/grafana/tempo-operator/internal/version" ) -var ( - metricTempoStackStatusCondition = promauto.With(metrics.Registry).NewGaugeVec(prometheus.GaugeOpts{ - Namespace: "tempostack", - Name: "status_condition", - Help: "The status condition of a TempoStack instance.", - }, []string{"stack_namespace", "stack_name", "condition"}) -) - // Refresh updates the status field with the Tempo versions and updates the tempostack_status_condition metric. func Refresh(ctx context.Context, k StatusClient, tempo v1alpha1.TempoStack, status *v1alpha1.TempoStackStatus) error { changed := tempo.DeepCopy() @@ -35,22 +22,7 @@ func Refresh(ctx context.Context, k StatusClient, tempo v1alpha1.TempoStack, sta changed.Status.TempoVersion = version.Get().TempoVersion } - // Update all status condition metrics. - // In some cases not all status conditions are present in the status.Conditions list, for example: - // A TempoStack CR gets created with an invalid storage secret (creating an ConfigurationError status condition). - // Later this CR is deleted, a storage secret is created and a new TempoStack instance is created. - // Then this TempoStack instance doesn't have the ConfigurationError condition in the status.Conditions list. - activeConditions := map[string]float64{} - for _, cond := range status.Conditions { - if cond.Status == metav1.ConditionTrue { - activeConditions[cond.Type] = 1 - } - } - for _, cond := range v1alpha1.AllStatusConditions { - condStr := string(cond) - isActive := activeConditions[condStr] // isActive will be 0 if the condition is not found in the map - metricTempoStackStatusCondition.WithLabelValues(tempo.Namespace, tempo.Name, condStr).Set(isActive) - } + updateMetrics(metricTempoStackStatusCondition, status.Conditions, tempo.Namespace, tempo.Name) err := k.PatchStatus(ctx, changed, &tempo) if err != nil { diff --git a/tests/e2e/monolithic-s3-tls/01-install-tempo.yaml b/tests/e2e/monolithic-s3-tls/01-install-tempo.yaml index 7e646b66f..6e90507d7 100644 --- a/tests/e2e/monolithic-s3-tls/01-install-tempo.yaml +++ b/tests/e2e/monolithic-s3-tls/01-install-tempo.yaml @@ -1,17 +1,3 @@ -apiVersion: tempo.grafana.com/v1alpha1 -kind: TempoMonolithic -metadata: - name: simplest -spec: - storage: - traces: - backend: s3 - s3: - secret: minio - tls: - enabled: true - caName: storage-ca ---- apiVersion: v1 kind: ConfigMap metadata: @@ -47,3 +33,17 @@ data: xtdKFBv1LLup2pr/hbmbP+0XHNFuUK36I7oantHhagXL/pGO3vcugppAd+YNzOW9 Qr67VgKeixOrNiK6W9FzuaWYadv0XOgrJbFhsvmtYqpEMDGZzfdb5dAzdA== -----END CERTIFICATE----- +--- +apiVersion: tempo.grafana.com/v1alpha1 +kind: TempoMonolithic +metadata: + name: simplest +spec: + storage: + traces: + backend: s3 + s3: + secret: minio + tls: + enabled: true + caName: storage-ca