diff --git a/apis/datadoghq/v1alpha1/datadogagentprofile_types.go b/apis/datadoghq/v1alpha1/datadogagentprofile_types.go index 4e30a6232..8f51d0706 100644 --- a/apis/datadoghq/v1alpha1/datadogagentprofile_types.go +++ b/apis/datadoghq/v1alpha1/datadogagentprofile_types.go @@ -53,16 +53,39 @@ type Container struct { } // DatadogAgentProfileStatus defines the observed state of DatadogAgentProfile +// +k8s:openapi-gen=true type DatadogAgentProfileStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file -} + // LastUpdate is the last time the status was updated. + // +optional + LastUpdate *metav1.Time `json:"lastUpdate,omitempty"` -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status -//+kubebuilder:resource:path=datadogagentprofiles,shortName=dap + // CurrentHash is the stored hash of the DatadogAgentProfile. + // +optional + CurrentHash string `json:"currentHash,omitempty"` + + // Conditions represents the latest available observations of a DatadogAgentProfile's current state. + // +optional + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions"` + + // Valid shows if the DatadogAgentProfile has a valid config spec. + // +optional + Valid metav1.ConditionStatus `json:"valid,omitempty"` + + // Applied shows whether the DatadogAgentProfile conflicts with an existing DatadogAgentProfile. + // +optional + Applied metav1.ConditionStatus `json:"applied,omitempty"` +} // DatadogAgentProfile is the Schema for the datadogagentprofiles API +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:path=datadogagentprofiles,shortName=dap +// +kubebuilder:printcolumn:name="valid",type="string",JSONPath=".status.valid" +// +kubebuilder:printcolumn:name="applied",type="string",JSONPath=".status.applied" +// +kubebuilder:printcolumn:name="age",type="date",JSONPath=".metadata.creationTimestamp" +// +k8s:openapi-gen=true type DatadogAgentProfile struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/apis/datadoghq/v1alpha1/zz_generated.deepcopy.go b/apis/datadoghq/v1alpha1/zz_generated.deepcopy.go index 009f1e571..218535daf 100644 --- a/apis/datadoghq/v1alpha1/zz_generated.deepcopy.go +++ b/apis/datadoghq/v1alpha1/zz_generated.deepcopy.go @@ -677,7 +677,7 @@ func (in *DatadogAgentProfile) DeepCopyInto(out *DatadogAgentProfile) { 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 DatadogAgentProfile. @@ -758,6 +758,17 @@ func (in *DatadogAgentProfileSpec) DeepCopy() *DatadogAgentProfileSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DatadogAgentProfileStatus) DeepCopyInto(out *DatadogAgentProfileStatus) { *out = *in + if in.LastUpdate != nil { + in, out := &in.LastUpdate, &out.LastUpdate + *out = (*in).DeepCopy() + } + 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 DatadogAgentProfileStatus. diff --git a/apis/datadoghq/v1alpha1/zz_generated.openapi.go b/apis/datadoghq/v1alpha1/zz_generated.openapi.go index 67826d412..121e5bb14 100644 --- a/apis/datadoghq/v1alpha1/zz_generated.openapi.go +++ b/apis/datadoghq/v1alpha1/zz_generated.openapi.go @@ -35,6 +35,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "./apis/datadoghq/v1alpha1.DaemonSetRollingUpdateSpec": schema__apis_datadoghq_v1alpha1_DaemonSetRollingUpdateSpec(ref), "./apis/datadoghq/v1alpha1.DatadogAgent": schema__apis_datadoghq_v1alpha1_DatadogAgent(ref), "./apis/datadoghq/v1alpha1.DatadogAgentCondition": schema__apis_datadoghq_v1alpha1_DatadogAgentCondition(ref), + "./apis/datadoghq/v1alpha1.DatadogAgentProfile": schema__apis_datadoghq_v1alpha1_DatadogAgentProfile(ref), + "./apis/datadoghq/v1alpha1.DatadogAgentProfileStatus": schema__apis_datadoghq_v1alpha1_DatadogAgentProfileStatus(ref), "./apis/datadoghq/v1alpha1.DatadogAgentSpec": schema__apis_datadoghq_v1alpha1_DatadogAgentSpec(ref), "./apis/datadoghq/v1alpha1.DatadogAgentSpecAgentSpec": schema__apis_datadoghq_v1alpha1_DatadogAgentSpecAgentSpec(ref), "./apis/datadoghq/v1alpha1.DatadogAgentSpecClusterAgentSpec": schema__apis_datadoghq_v1alpha1_DatadogAgentSpecClusterAgentSpec(ref), @@ -1071,6 +1073,117 @@ func schema__apis_datadoghq_v1alpha1_DatadogAgentCondition(ref common.ReferenceC } } +func schema__apis_datadoghq_v1alpha1_DatadogAgentProfile(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DatadogAgentProfile is the Schema for the datadogagentprofiles API", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("./apis/datadoghq/v1alpha1.DatadogAgentProfileSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("./apis/datadoghq/v1alpha1.DatadogAgentProfileStatus"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "./apis/datadoghq/v1alpha1.DatadogAgentProfileSpec", "./apis/datadoghq/v1alpha1.DatadogAgentProfileStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema__apis_datadoghq_v1alpha1_DatadogAgentProfileStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DatadogAgentProfileStatus defines the observed state of DatadogAgentProfile", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "lastUpdate": { + SchemaProps: spec.SchemaProps{ + Description: "LastUpdate is the last time the status was updated.", + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), + }, + }, + "currentHash": { + SchemaProps: spec.SchemaProps{ + Description: "CurrentHash is the stored hash of the DatadogAgentProfile.", + Type: []string{"string"}, + Format: "", + }, + }, + "conditions": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "type", + }, + "x-kubernetes-list-type": "map", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Conditions represents the latest available observations of a DatadogAgentProfile's current state.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Condition"), + }, + }, + }, + }, + }, + "valid": { + SchemaProps: spec.SchemaProps{ + Description: "Valid shows if the DatadogAgentProfile has a valid config spec.", + Type: []string{"string"}, + Format: "", + }, + }, + "applied": { + SchemaProps: spec.SchemaProps{ + Description: "Applied shows whether the DatadogAgentProfile conflicts with an existing DatadogAgentProfile.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/apis/meta/v1.Condition", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, + } +} + func schema__apis_datadoghq_v1alpha1_DatadogAgentSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/config/crd/bases/v1/datadoghq.com_datadogagentprofiles.yaml b/config/crd/bases/v1/datadoghq.com_datadogagentprofiles.yaml index e32536727..4ce95b8dc 100644 --- a/config/crd/bases/v1/datadoghq.com_datadogagentprofiles.yaml +++ b/config/crd/bases/v1/datadoghq.com_datadogagentprofiles.yaml @@ -18,7 +18,17 @@ spec: singular: datadogagentprofile scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .status.valid + name: valid + type: string + - jsonPath: .status.applied + name: applied + type: string + - jsonPath: .metadata.creationTimestamp + name: age + type: date + name: v1alpha1 schema: openAPIV3Schema: description: DatadogAgentProfile is the Schema for the datadogagentprofiles API @@ -99,6 +109,67 @@ spec: type: object status: description: DatadogAgentProfileStatus defines the observed state of DatadogAgentProfile + properties: + applied: + description: Applied shows whether the DatadogAgentProfile conflicts with an existing DatadogAgentProfile. + type: string + conditions: + description: Conditions represents the latest available observations of a DatadogAgentProfile's current state. + 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, 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + currentHash: + description: CurrentHash is the stored hash of the DatadogAgentProfile. + type: string + lastUpdate: + description: LastUpdate is the last time the status was updated. + format: date-time + type: string + valid: + description: Valid shows if the DatadogAgentProfile has a valid config spec. + type: string type: object type: object served: true diff --git a/config/crd/bases/v1beta1/datadoghq.com_datadogagentprofiles.yaml b/config/crd/bases/v1beta1/datadoghq.com_datadogagentprofiles.yaml index c8f58e95b..8bdddb8ac 100644 --- a/config/crd/bases/v1beta1/datadoghq.com_datadogagentprofiles.yaml +++ b/config/crd/bases/v1beta1/datadoghq.com_datadogagentprofiles.yaml @@ -8,6 +8,16 @@ metadata: creationTimestamp: null name: datadogagentprofiles.datadoghq.com spec: + additionalPrinterColumns: + - JSONPath: .status.valid + name: valid + type: string + - JSONPath: .status.applied + name: applied + type: string + - JSONPath: .metadata.creationTimestamp + name: age + type: date group: datadoghq.com names: kind: DatadogAgentProfile @@ -100,6 +110,67 @@ spec: type: object status: description: DatadogAgentProfileStatus defines the observed state of DatadogAgentProfile + properties: + applied: + description: Applied shows whether the DatadogAgentProfile conflicts with an existing DatadogAgentProfile. + type: string + conditions: + description: Conditions represents the latest available observations of a DatadogAgentProfile's current state. + 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, 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + currentHash: + description: CurrentHash is the stored hash of the DatadogAgentProfile. + type: string + lastUpdate: + description: LastUpdate is the last time the status was updated. + format: date-time + type: string + valid: + description: Valid shows if the DatadogAgentProfile has a valid config spec. + type: string type: object type: object version: v1alpha1 diff --git a/controllers/datadogagent/controller_reconcile_v2.go b/controllers/datadogagent/controller_reconcile_v2.go index b2ee2d567..6eb7827e7 100644 --- a/controllers/datadogagent/controller_reconcile_v2.go +++ b/controllers/datadogagent/controller_reconcile_v2.go @@ -125,13 +125,13 @@ func (r *Reconciler) reconcileInstanceV2(ctx context.Context, logger logr.Logger } } if len(errs) > 0 { - return r.updateStatusIfNeededV2(logger, instance, newStatus, result, errors.NewAggregate(errs)) + return r.updateStatusIfNeededV2(logger, instance, newStatus, result, errors.NewAggregate(errs), now) } // Examine user configuration to override any external dependencies (e.g. RBACs) errs = override.Dependencies(logger, resourceManagers, instance) if len(errs) > 0 { - return r.updateStatusIfNeededV2(logger, instance, newStatus, result, errors.NewAggregate(errs)) + return r.updateStatusIfNeededV2(logger, instance, newStatus, result, errors.NewAggregate(errs), now) } userSpecifiedClusterAgentToken := instance.Spec.Global.ClusterAgentToken != nil || instance.Spec.Global.ClusterAgentTokenSecret != nil @@ -147,7 +147,7 @@ func (r *Reconciler) reconcileInstanceV2(ctx context.Context, logger logr.Logger result, err = r.reconcileV2ClusterAgent(logger, requiredComponents, features, instance, resourceManagers, newStatus) if utils.ShouldReturn(result, err) { - return r.updateStatusIfNeededV2(logger, instance, newStatus, result, err) + return r.updateStatusIfNeededV2(logger, instance, newStatus, result, err, now) } else { // Update the status to make it the ClusterAgentReconcileConditionType successful datadoghqv2alpha1.UpdateDatadogAgentStatusConditions(newStatus, now, datadoghqv2alpha1.ClusterAgentReconcileConditionType, metav1.ConditionTrue, "reconcile_succeed", "reconcile succeed", false) @@ -163,7 +163,7 @@ func (r *Reconciler) reconcileInstanceV2(ctx context.Context, logger logr.Logger // Get a node list for profiles and introspection nodeList, e := r.getNodeList(ctx) if e != nil { - return r.updateStatusIfNeededV2(logger, instance, newStatus, result, e) + return r.updateStatusIfNeededV2(logger, instance, newStatus, result, e, now) } if r.options.IntrospectionEnabled { @@ -171,14 +171,14 @@ func (r *Reconciler) reconcileInstanceV2(ctx context.Context, logger logr.Logger } if r.options.DatadogAgentProfileEnabled { - profileList, profilesByNode, e := r.profilesToApply(ctx, logger, nodeList) - if e != nil { - return r.updateStatusIfNeededV2(logger, instance, newStatus, result, e) + var profilesByNode map[string]types.NamespacedName + profiles, profilesByNode, e = r.profilesToApply(ctx, logger, nodeList, now) + if err != nil { + return r.updateStatusIfNeededV2(logger, instance, newStatus, result, e, now) } - profiles = profileList if err = r.handleProfiles(ctx, profilesByNode, instance.Namespace); err != nil { - return r.updateStatusIfNeededV2(logger, instance, newStatus, result, err) + return r.updateStatusIfNeededV2(logger, instance, newStatus, result, err, now) } } } @@ -198,7 +198,7 @@ func (r *Reconciler) reconcileInstanceV2(ctx context.Context, logger logr.Logger logger.Error(err, "Error cleaning up old DaemonSets") } if utils.ShouldReturn(result, errors.NewAggregate(errs)) { - return r.updateStatusIfNeededV2(logger, instance, newStatus, result, errors.NewAggregate(errs)) + return r.updateStatusIfNeededV2(logger, instance, newStatus, result, errors.NewAggregate(errs), now) } else { // Update the status to set AgentReconcileConditionType to successful datadoghqv2alpha1.UpdateDatadogAgentStatusConditions(newStatus, now, datadoghqv2alpha1.AgentReconcileConditionType, metav1.ConditionTrue, "reconcile_succeed", "reconcile succeed", false) @@ -206,7 +206,7 @@ func (r *Reconciler) reconcileInstanceV2(ctx context.Context, logger logr.Logger result, err = r.reconcileV2ClusterChecksRunner(logger, requiredComponents, features, instance, resourceManagers, newStatus) if utils.ShouldReturn(result, err) { - return r.updateStatusIfNeededV2(logger, instance, newStatus, result, err) + return r.updateStatusIfNeededV2(logger, instance, newStatus, result, err, now) } else { // Update the status to set ClusterChecksRunnerReconcileConditionType to successful datadoghqv2alpha1.UpdateDatadogAgentStatusConditions(newStatus, now, datadoghqv2alpha1.ClusterChecksRunnerReconcileConditionType, metav1.ConditionTrue, "reconcile_succeed", "reconcile succeed", false) @@ -218,7 +218,7 @@ func (r *Reconciler) reconcileInstanceV2(ctx context.Context, logger logr.Logger errs = append(errs, depsStore.Apply(ctx, r.client)...) if len(errs) > 0 { logger.V(2).Info("Dependencies apply error", "errs", errs) - return r.updateStatusIfNeededV2(logger, instance, newStatus, result, errors.NewAggregate(errs)) + return r.updateStatusIfNeededV2(logger, instance, newStatus, result, errors.NewAggregate(errs), now) } // ----------------------------- @@ -226,18 +226,17 @@ func (r *Reconciler) reconcileInstanceV2(ctx context.Context, logger logr.Logger // ----------------------------- // Run it after the deployments reconcile if errs = depsStore.Cleanup(ctx, r.client); len(errs) > 0 { - return r.updateStatusIfNeededV2(logger, instance, newStatus, result, errors.NewAggregate(errs)) + return r.updateStatusIfNeededV2(logger, instance, newStatus, result, errors.NewAggregate(errs), now) } // Always requeue if !result.Requeue && result.RequeueAfter == 0 { result.RequeueAfter = defaultRequeuePeriod } - return r.updateStatusIfNeededV2(logger, instance, newStatus, result, err) + return r.updateStatusIfNeededV2(logger, instance, newStatus, result, err, now) } -func (r *Reconciler) updateStatusIfNeededV2(logger logr.Logger, agentdeployment *datadoghqv2alpha1.DatadogAgent, newStatus *datadoghqv2alpha1.DatadogAgentStatus, result reconcile.Result, currentError error) (reconcile.Result, error) { - now := metav1.NewTime(time.Now()) +func (r *Reconciler) updateStatusIfNeededV2(logger logr.Logger, agentdeployment *datadoghqv2alpha1.DatadogAgent, newStatus *datadoghqv2alpha1.DatadogAgentStatus, result reconcile.Result, currentError error, now metav1.Time) (reconcile.Result, error) { if currentError == nil { datadoghqv2alpha1.UpdateDatadogAgentStatusConditions(newStatus, now, datadoghqv2alpha1.DatadogAgentReconcileErrorConditionType, metav1.ConditionFalse, "DatadogAgent_reconcile_ok", "DatadogAgent reconcile ok", false) } else { @@ -262,6 +261,15 @@ func (r *Reconciler) updateStatusIfNeededV2(logger logr.Logger, agentdeployment return result, currentError } +func (r *Reconciler) updateDAPStatus(logger logr.Logger, profile *datadoghqv1alpha1.DatadogAgentProfile) { + if err := r.client.Status().Update(context.TODO(), profile); err != nil { + if apierrors.IsConflict(err) { + logger.V(1).Info("unable to update DatadogAgentProfile status due to update conflict") + } + logger.Error(err, "unable to update DatadogAgentProfile status") + } +} + // setMetricsForwarderStatus sets the metrics forwarder status condition if enabled func (r *Reconciler) setMetricsForwarderStatusV2(logger logr.Logger, agentdeployment *datadoghqv2alpha1.DatadogAgent, newStatus *datadoghqv2alpha1.DatadogAgentStatus) { if r.options.OperatorMetricsEnabled { @@ -313,14 +321,44 @@ func (r *Reconciler) updateMetricsForwardersFeatures(dda *datadoghqv2alpha1.Data } } -func (r *Reconciler) profilesToApply(ctx context.Context, logger logr.Logger, nodeList []corev1.Node) ([]datadoghqv1alpha1.DatadogAgentProfile, map[string]types.NamespacedName, error) { +// profilesToApply gets a list of profiles and returns the ones that should be +// applied in the cluster. +// - If there are no profiles, it returns the default profile. +// - If there are no conflicting profiles, it returns all the profiles plus the default one. +// - If there are conflicting profiles, it returns a subset that does not +// conflict plus the default one. When there are conflicting profiles, the +// oldest one is the one that takes precedence. When two profiles share an +// identical creation timestamp, the profile whose name is alphabetically first +// is considered to have priority. +// This function also returns a map that maps each node name to the profile that +// should be applied to it. +func (r *Reconciler) profilesToApply(ctx context.Context, logger logr.Logger, nodeList []corev1.Node, now metav1.Time) ([]datadoghqv1alpha1.DatadogAgentProfile, map[string]types.NamespacedName, error) { profilesList := datadoghqv1alpha1.DatadogAgentProfileList{} err := r.client.List(ctx, &profilesList) if err != nil { return nil, nil, err } - return agentprofile.ProfilesToApply(profilesList.Items, nodeList, logger) + var profileListToApply []datadoghqv1alpha1.DatadogAgentProfile + profileAppliedByNode := make(map[string]types.NamespacedName, len(nodeList)) + + sortedProfiles := agentprofile.SortProfiles(profilesList.Items) + for _, profile := range sortedProfiles { + + profileAppliedByNode, err = agentprofile.ProfileToApply(logger, &profile, nodeList, profileAppliedByNode, now) + r.updateDAPStatus(logger, &profile) + if err != nil { + // profile is invalid or conflicts + logger.Error(err, "profile cannot be applied", "name", profile.Name, "namespace", profile.Namespace) + continue + } + profileListToApply = append(profileListToApply, profile) + } + + // add default profile + profileListToApply = agentprofile.ApplyDefaultProfile(profileListToApply, profileAppliedByNode, nodeList) + + return profileListToApply, profileAppliedByNode, nil } func (r *Reconciler) getNodeList(ctx context.Context) ([]corev1.Node, error) { diff --git a/controllers/datadogagent/controller_reconcile_v2_test.go b/controllers/datadogagent/controller_reconcile_v2_test.go new file mode 100644 index 000000000..b6f1885f6 --- /dev/null +++ b/controllers/datadogagent/controller_reconcile_v2_test.go @@ -0,0 +1,639 @@ +package datadogagent + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/DataDog/datadog-operator/apis/datadoghq/common/v1" + "github.com/DataDog/datadog-operator/apis/datadoghq/v1alpha1" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +const testNamespace = "foo" + +func Test_profilesToApply(t *testing.T) { + t1 := time.Now() + t2 := t1.Add(time.Minute) + t3 := t2.Add(time.Minute) + now := metav1.NewTime(t1) + + sch := runtime.NewScheme() + _ = scheme.AddToScheme(sch) + _ = v1alpha1.AddToScheme(sch) + ctx := context.Background() + + testCases := []struct { + name string + nodeList []corev1.Node + profileList []client.Object + wantProfilesToApply func() []v1alpha1.DatadogAgentProfile + wantProfileAppliedByNode map[string]types.NamespacedName + wantError error + }{ + { + name: "no user-created profiles to apply", + nodeList: []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Labels: map[string]string{ + "1": "1", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + Labels: map[string]string{ + "2": "1", + }, + }, + }, + }, + profileList: []client.Object{}, + wantProfilesToApply: func() []v1alpha1.DatadogAgentProfile { + return []v1alpha1.DatadogAgentProfile{defaultProfile()} + }, + wantProfileAppliedByNode: map[string]types.NamespacedName{ + "node1": { + Namespace: "", + Name: "default", + }, + "node2": { + Namespace: "", + Name: "default", + }, + }, + }, + { + name: "no nodes, no profiles", + nodeList: []corev1.Node{}, + profileList: []client.Object{}, + wantProfilesToApply: func() []v1alpha1.DatadogAgentProfile { + return []v1alpha1.DatadogAgentProfile{defaultProfile()} + }, wantProfileAppliedByNode: map[string]types.NamespacedName{}, + }, + { + name: "no nodes", + nodeList: []corev1.Node{}, + profileList: generateObjectList([]string{"1"}, []time.Time{t1}), + wantProfilesToApply: func() []v1alpha1.DatadogAgentProfile { + profileList := generateProfileList([]string{"1"}, []time.Time{t1}) + profileList[0].Status = v1alpha1.DatadogAgentProfileStatus{ + LastUpdate: &now, + CurrentHash: "36a4d655a44a0ca07780fff47dd96c6a", + Conditions: nil, + Valid: "Unknown", + Applied: "Unknown", + } + profileList[0].ResourceVersion = "1000" + return profileList + }, + wantProfileAppliedByNode: map[string]types.NamespacedName{}, + }, + { + name: "one profile", + nodeList: []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Labels: map[string]string{ + "1": "1", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + Labels: map[string]string{ + "2": "1", + }, + }, + }, + }, + profileList: generateObjectList([]string{"1"}, []time.Time{t1}), + wantProfilesToApply: func() []v1alpha1.DatadogAgentProfile { + profileList := generateProfileList([]string{"1"}, []time.Time{t1}) + profileList[0].Status = v1alpha1.DatadogAgentProfileStatus{ + LastUpdate: &now, + CurrentHash: "36a4d655a44a0ca07780fff47dd96c6a", + Conditions: []metav1.Condition{ + { + Type: "Valid", + Status: "True", + LastTransitionTime: now, + Reason: "Valid", + Message: "Valid manifest", + }, + { + Type: "Applied", + Status: "True", + LastTransitionTime: now, + Reason: "Applied", + Message: "Profile applied", + }, + }, + Valid: "True", + Applied: "True", + } + profileList[0].ResourceVersion = "1000" + return profileList + }, + wantProfileAppliedByNode: map[string]types.NamespacedName{ + "node1": { + Namespace: testNamespace, + Name: "1", + }, + "node2": { + Namespace: "", + Name: "default", + }, + }, + }, + { + name: "several non-conflicting profiles", + nodeList: []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Labels: map[string]string{ + "1": "1", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + Labels: map[string]string{ + "2": "1", + }, + }, + }, + }, + profileList: generateObjectList([]string{"1", "2"}, []time.Time{t1, t2}), + wantProfilesToApply: func() []v1alpha1.DatadogAgentProfile { + profileList := generateProfileList([]string{"1", "2"}, []time.Time{t1, t2}) + profileList[0].Status = v1alpha1.DatadogAgentProfileStatus{ + LastUpdate: &now, + CurrentHash: "36a4d655a44a0ca07780fff47dd96c6a", + Conditions: []metav1.Condition{ + { + Type: "Valid", + Status: "True", + LastTransitionTime: now, + Reason: "Valid", + Message: "Valid manifest", + }, + { + Type: "Applied", + Status: "True", + LastTransitionTime: now, + Reason: "Applied", + Message: "Profile applied", + }, + }, + Valid: "True", + Applied: "True", + } + profileList[0].ResourceVersion = "1000" + profileList[1].Status = v1alpha1.DatadogAgentProfileStatus{ + LastUpdate: &now, + CurrentHash: "e7eda6755e8a98d127140e2169204312", + Conditions: []metav1.Condition{ + { + Type: "Valid", + Status: "True", + LastTransitionTime: now, + Reason: "Valid", + Message: "Valid manifest", + }, + { + Type: "Applied", + Status: "True", + LastTransitionTime: now, + Reason: "Applied", + Message: "Profile applied", + }, + }, + Valid: "True", + Applied: "True", + } + profileList[1].ResourceVersion = "1000" + return profileList + }, + wantProfileAppliedByNode: map[string]types.NamespacedName{ + "node1": { + Namespace: testNamespace, + Name: "1", + }, + "node2": { + Namespace: testNamespace, + Name: "2", + }, + }, + }, + { + // This test defines 3 profiles created in this order: profile-2, + // profile-1, profile-3 (not sorted here to make sure that the code does). + // - profile-1 and profile-2 conflict, but profile-2 is the oldest, + // so it wins. + // - profile-1 and profile-3 conflict, but profile-1 is not applied + // because of the conflict with profile-2, so profile-3 should be. + // So in this case, the returned profiles should be profile-2, + // profile-3 and a default one. + name: "several conflicting profiles with different creation timestamps", + nodeList: []corev1.Node{ + // node1 matches profile-1 and profile-3 + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Labels: map[string]string{ + "1": "1", + "3": "1", + }, + }, + }, + // node2 matches profile-2 + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + Labels: map[string]string{ + "2": "1", + }, + }, + }, + // node3 matches profile-1 and profile-2 + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node3", + Labels: map[string]string{ + "1": "1", + "2": "1", + }, + }, + }, + }, + profileList: generateObjectList([]string{"1", "2", "3"}, []time.Time{t2, t1, t3}), + wantProfilesToApply: func() []v1alpha1.DatadogAgentProfile { + profileList := generateProfileList([]string{"2", "3"}, []time.Time{t1, t3}) + profileList[0].Status = v1alpha1.DatadogAgentProfileStatus{ + LastUpdate: &now, + CurrentHash: "e7eda6755e8a98d127140e2169204312", + Conditions: []metav1.Condition{ + { + Type: "Valid", + Status: "True", + LastTransitionTime: now, + Reason: "Valid", + Message: "Valid manifest", + }, + { + Type: "Applied", + Status: "True", + LastTransitionTime: now, + Reason: "Applied", + Message: "Profile applied", + }, + }, + Valid: "True", + Applied: "True", + } + profileList[0].ResourceVersion = "1000" + profileList[1].Status = v1alpha1.DatadogAgentProfileStatus{ + LastUpdate: &now, + CurrentHash: "6cc0746a51b8e52da6e4e625d3181686", + Conditions: []metav1.Condition{ + { + Type: "Valid", + Status: "True", + LastTransitionTime: now, + Reason: "Valid", + Message: "Valid manifest", + }, + { + Type: "Applied", + Status: "True", + LastTransitionTime: now, + Reason: "Applied", + Message: "Profile applied", + }, + }, + Valid: "True", + Applied: "True", + } + profileList[1].ResourceVersion = "1000" + return profileList + }, + wantProfileAppliedByNode: map[string]types.NamespacedName{ + "node1": { + Namespace: testNamespace, + Name: "3", + }, + "node2": { + Namespace: testNamespace, + Name: "2", + }, + "node3": { + Namespace: testNamespace, + Name: "2", + }, + }, + }, + { + // This test defines 3 profiles with the same creation timestamp: + // profile-2, profile-1, profile-3 (not sorted alphabetically here + // to make sure that the code does). + // The 3 profiles conflict and only profile-1 should apply because + // it's the first one alphabetically. + name: "conflicting profiles with the same creation timestamp", + nodeList: []corev1.Node{ + // matches all profiles + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Labels: map[string]string{ + "1": "1", + "2": "1", + "3": "1", + }, + }, + }, + }, + profileList: generateObjectList([]string{"2", "1", "3"}, []time.Time{t1, t1, t1}), + wantProfilesToApply: func() []v1alpha1.DatadogAgentProfile { + profileList := generateProfileList([]string{"1"}, []time.Time{t1}) + profileList[0].Status = v1alpha1.DatadogAgentProfileStatus{ + LastUpdate: &now, + CurrentHash: "36a4d655a44a0ca07780fff47dd96c6a", + Conditions: []metav1.Condition{ + { + Type: "Valid", + Status: "True", + LastTransitionTime: now, + Reason: "Valid", + Message: "Valid manifest", + }, + { + Type: "Applied", + Status: "True", + LastTransitionTime: now, + Reason: "Applied", + Message: "Profile applied", + }, + }, + Valid: "True", + Applied: "True", + } + profileList[0].ResourceVersion = "1000" + return profileList + }, + wantProfileAppliedByNode: map[string]types.NamespacedName{ + "node1": { + Namespace: testNamespace, + Name: "1", + }, + }, + }, + { + name: "invalid profile", + nodeList: []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Labels: map[string]string{ + "1": "1", + }, + }, + }, + }, + profileList: []client.Object{ + &v1alpha1.DatadogAgentProfile{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: "invalid", + }, + Spec: v1alpha1.DatadogAgentProfileSpec{}, + }, + }, + wantProfilesToApply: func() []v1alpha1.DatadogAgentProfile { + return generateProfileList([]string{}, []time.Time{}) + }, + wantProfileAppliedByNode: map[string]types.NamespacedName{ + "node1": { + Namespace: "", + Name: "default", + }, + }, + }, + { + // Profile 1 matches node1 and should be applied. + // Profile 2 doesn't conflict with Profile 1 but doesn't apply + // to any nodes since there are no matching nodes. + name: "invalid profiles + valid profiles", + nodeList: []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Labels: map[string]string{ + "1": "1", + }, + }, + }, + }, + profileList: append(generateObjectList([]string{"1", "2"}, []time.Time{t1, t2}), []client.Object{ + &v1alpha1.DatadogAgentProfile{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: "invalid-no-affinity", + }, + Spec: v1alpha1.DatadogAgentProfileSpec{ + Config: &v1alpha1.Config{ + Override: map[v1alpha1.ComponentName]*v1alpha1.Override{ + v1alpha1.NodeAgentComponentName: { + Containers: map[common.AgentContainerName]*v1alpha1.Container{ + common.CoreAgentContainerName: { + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + &v1alpha1.DatadogAgentProfile{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: "invalid-no-config", + }, + Spec: v1alpha1.DatadogAgentProfileSpec{ + ProfileAffinity: &v1alpha1.ProfileAffinity{ + ProfileNodeAffinity: []corev1.NodeSelectorRequirement{ + { + Key: "os", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"linux"}, + }, + }, + }, + }, + }, + }...), + wantProfilesToApply: func() []v1alpha1.DatadogAgentProfile { + profileList := generateProfileList([]string{"1", "2"}, []time.Time{t1, t2}) + profileList[0].Status = v1alpha1.DatadogAgentProfileStatus{ + LastUpdate: &now, + CurrentHash: "36a4d655a44a0ca07780fff47dd96c6a", + Conditions: []metav1.Condition{ + { + Type: "Valid", + Status: "True", + LastTransitionTime: now, + Reason: "Valid", + Message: "Valid manifest", + }, + { + Type: "Applied", + Status: "True", + LastTransitionTime: now, + Reason: "Applied", + Message: "Profile applied", + }, + }, + Valid: "True", + Applied: "True", + } + profileList[0].ResourceVersion = "1000" + profileList[1].Status = v1alpha1.DatadogAgentProfileStatus{ + LastUpdate: &now, + CurrentHash: "e7eda6755e8a98d127140e2169204312", + Conditions: []metav1.Condition{ + { + Type: "Valid", + Status: "True", + LastTransitionTime: now, + Reason: "Valid", + Message: "Valid manifest", + }, + }, + Valid: "True", + Applied: "Unknown", + } + profileList[1].ResourceVersion = "1000" + return profileList + }, + wantProfileAppliedByNode: map[string]types.NamespacedName{ + "node1": { + Namespace: testNamespace, + Name: "1", + }, + }, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + fakeClient := fake.NewClientBuilder().WithScheme(sch).WithObjects(tt.profileList...).Build() + logger := logf.Log.WithName("Test_profilesToApply") + eventBroadcaster := record.NewBroadcaster() + recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "Test_profilesToApply"}) + + r := &Reconciler{ + client: fakeClient, + log: logger, + recorder: recorder, + options: ReconcilerOptions{ + DatadogAgentProfileEnabled: true, + }, + } + + profilesToApply, profileAppliedByNode, err := r.profilesToApply(ctx, logger, tt.nodeList, metav1.NewTime(t1)) + require.NoError(t, err) + + wantProfilesToApply := tt.wantProfilesToApply() + assert.Equal(t, wantProfilesToApply, profilesToApply) + // assert.ElementsMatch(t, wantProfilesToApply, profilesToApply) + assert.Equal(t, tt.wantProfileAppliedByNode, profileAppliedByNode) + }) + } +} + +func generateObjectList(profileIdentifiers []string, creationTimes []time.Time) []client.Object { + objectList := []client.Object{} + for i, j := range profileIdentifiers { + profile := exampleProfile(j, creationTimes[i]) + objectList = append(objectList, &profile) + } + return objectList +} + +func generateProfileList(profileIdentifiers []string, creationTimes []time.Time) []v1alpha1.DatadogAgentProfile { + profileList := []v1alpha1.DatadogAgentProfile{} + for i, j := range profileIdentifiers { + profileList = append(profileList, exampleProfile(j, creationTimes[i])) + } + profileList = append(profileList, defaultProfile()) + return profileList +} + +func exampleProfile(i string, creationTime time.Time) v1alpha1.DatadogAgentProfile { + return v1alpha1.DatadogAgentProfile{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: i, + CreationTimestamp: metav1.NewTime(creationTime.Truncate(time.Second)), + }, + Spec: v1alpha1.DatadogAgentProfileSpec{ + ProfileAffinity: &v1alpha1.ProfileAffinity{ + ProfileNodeAffinity: []corev1.NodeSelectorRequirement{ + { + Key: i, + Operator: corev1.NodeSelectorOpIn, + Values: []string{"1"}, + }, + }, + }, + Config: &v1alpha1.Config{ + Override: map[v1alpha1.ComponentName]*v1alpha1.Override{ + v1alpha1.NodeAgentComponentName: { + Containers: map[common.AgentContainerName]*v1alpha1.Container{ + common.CoreAgentContainerName: { + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse(fmt.Sprintf("%s00m", i)), + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func defaultProfile() v1alpha1.DatadogAgentProfile { + return v1alpha1.DatadogAgentProfile{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "", + Name: "default", + }, + } +} diff --git a/pkg/agentprofile/agent_profile.go b/pkg/agentprofile/agent_profile.go index d8ae53079..78f96bade 100644 --- a/pkg/agentprofile/agent_profile.go +++ b/pkg/agentprofile/agent_profile.go @@ -9,17 +9,18 @@ import ( "fmt" "sort" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/selection" - "k8s.io/apimachinery/pkg/types" - apicommon "github.com/DataDog/datadog-operator/apis/datadoghq/common" "github.com/DataDog/datadog-operator/apis/datadoghq/common/v1" datadoghqv1alpha1 "github.com/DataDog/datadog-operator/apis/datadoghq/v1alpha1" "github.com/DataDog/datadog-operator/apis/datadoghq/v2alpha1" + "github.com/DataDog/datadog-operator/pkg/controller/utils/comparison" + "github.com/go-logr/logr" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/types" ) const ( @@ -28,76 +29,80 @@ const ( daemonSetNamePrefix = "datadog-agent-with-profile-" ) -// ProfilesToApply given a list of profiles, returns the ones that should be -// applied in the cluster. -// - If there are no profiles, it returns the default profile. -// - If there are no conflicting profiles, it returns all the profiles plus the default one. -// - If there are conflicting profiles, it returns a subset that does not -// conflict plus the default one. When there are conflicting profiles, the -// oldest one is the one that takes precedence. When two profiles share an -// identical creation timestamp, the profile whose name is alphabetically first -// is considered to have priority. -// This function also returns a map that maps each node name to the profile that -// should be applied to it. -func ProfilesToApply(profiles []datadoghqv1alpha1.DatadogAgentProfile, nodes []v1.Node, logger logr.Logger) ([]datadoghqv1alpha1.DatadogAgentProfile, map[string]types.NamespacedName, error) { - var profilesToApply []datadoghqv1alpha1.DatadogAgentProfile - profileAppliedPerNode := make(map[string]types.NamespacedName, len(nodes)) - - sortedProfiles := sortProfiles(profiles) - - for _, profile := range sortedProfiles { - conflicts := false - nodesThatMatchProfile := map[string]bool{} - - if err := datadoghqv1alpha1.ValidateDatadogAgentProfileSpec(&profile.Spec); err != nil { - logger.Error(err, "profile spec is invalid, skipping", "name", profile.Name, "namespace", profile.Namespace) - continue - } +// ProfileToApply validates a profile spec and returns a map that maps each +// node name to the profile that should be applied to it. +func ProfileToApply(logger logr.Logger, profile *datadoghqv1alpha1.DatadogAgentProfile, nodes []v1.Node, profileAppliedByNode map[string]types.NamespacedName, + now metav1.Time) (map[string]types.NamespacedName, error) { + nodesThatMatchProfile := map[string]bool{} + profileStatus := datadoghqv1alpha1.DatadogAgentProfileStatus{} + + if hash, err := comparison.GenerateMD5ForSpec(profile.Spec); err != nil { + logger.Error(err, "couldn't generate hash for profile", "name", profile.Name, "namespace", profile.Namespace) + } else { + profileStatus.CurrentHash = hash + } - for _, node := range nodes { - matchesNode, err := profileMatchesNode(&profile, node.Labels) - if err != nil { - return nil, nil, err - } + if err := datadoghqv1alpha1.ValidateDatadogAgentProfileSpec(&profile.Spec); err != nil { + logger.Error(err, "profile spec is invalid, skipping", "name", profile.Name, "namespace", profile.Namespace) + profileStatus.Conditions = SetDatadogAgentProfileCondition(profileStatus.Conditions, NewDatadogAgentProfileCondition(ValidConditionType, metav1.ConditionFalse, now, InvalidConditionReason, err.Error())) + profileStatus.Valid = metav1.ConditionFalse + UpdateProfileStatus(profile, profileStatus, now) + return profileAppliedByNode, err + } - if matchesNode { - if existingProfile, found := profileAppliedPerNode[node.Name]; found { - // Conflict. This profile should not be applied. - conflicts = true - logger.Info("conflict with existing profile, skipping", "conflicting profile", profile.Namespace+"/"+profile.Name, "existing profile", existingProfile.String()) - break - } else { - nodesThatMatchProfile[node.Name] = true - } + for _, node := range nodes { + matchesNode, err := profileMatchesNode(profile, node.Labels) + if err != nil { + logger.Error(err, "profile selector is invalid, skipping", "name", profile.Name, "namespace", profile.Namespace) + profileStatus.Conditions = SetDatadogAgentProfileCondition(profileStatus.Conditions, NewDatadogAgentProfileCondition(ValidConditionType, metav1.ConditionFalse, now, InvalidConditionReason, err.Error())) + profileStatus.Valid = metav1.ConditionFalse + UpdateProfileStatus(profile, profileStatus, now) + return profileAppliedByNode, err + } + profileStatus.Valid = metav1.ConditionTrue + profileStatus.Conditions = SetDatadogAgentProfileCondition(profileStatus.Conditions, NewDatadogAgentProfileCondition(ValidConditionType, metav1.ConditionTrue, now, ValidConditionReason, "Valid manifest")) + + if matchesNode { + if existingProfile, found := profileAppliedByNode[node.Name]; found { + // Conflict. This profile should not be applied. + logger.Info("conflict with existing profile, skipping", "conflicting profile", profile.Namespace+"/"+profile.Name, "existing profile", existingProfile.String()) + profileStatus.Conditions = SetDatadogAgentProfileCondition(profileStatus.Conditions, NewDatadogAgentProfileCondition(AppliedConditionType, metav1.ConditionFalse, now, ConflictConditionReason, "Conflict with existing profile")) + profileStatus.Applied = metav1.ConditionFalse + UpdateProfileStatus(profile, profileStatus, now) + return profileAppliedByNode, fmt.Errorf("conflict with existing profile") + } else { + nodesThatMatchProfile[node.Name] = true + profileStatus.Conditions = SetDatadogAgentProfileCondition(profileStatus.Conditions, NewDatadogAgentProfileCondition(AppliedConditionType, metav1.ConditionTrue, now, AppliedConditionReason, "Profile applied")) + profileStatus.Applied = metav1.ConditionTrue } } + } - if conflicts { - continue + for node := range nodesThatMatchProfile { + profileAppliedByNode[node] = types.NamespacedName{ + Namespace: profile.Namespace, + Name: profile.Name, } + } - for node := range nodesThatMatchProfile { - profileAppliedPerNode[node] = types.NamespacedName{ - Namespace: profile.Namespace, - Name: profile.Name, - } - } + UpdateProfileStatus(profile, profileStatus, now) - profilesToApply = append(profilesToApply, profile) - } + return profileAppliedByNode, nil +} +func ApplyDefaultProfile(profilesToApply []datadoghqv1alpha1.DatadogAgentProfile, profileAppliedByNode map[string]types.NamespacedName, nodes []v1.Node) []datadoghqv1alpha1.DatadogAgentProfile { profilesToApply = append(profilesToApply, defaultProfile()) // Apply the default profile to all nodes that don't have a profile applied for _, node := range nodes { - if _, found := profileAppliedPerNode[node.Name]; !found { - profileAppliedPerNode[node.Name] = types.NamespacedName{ + if _, found := profileAppliedByNode[node.Name]; !found { + profileAppliedByNode[node.Name] = types.NamespacedName{ Name: defaultProfileName, } } } - return profilesToApply, profileAppliedPerNode, nil + return profilesToApply } // OverrideFromProfile returns the component override that should be @@ -287,9 +292,9 @@ func priorityClassNameOverride(profile *datadoghqv1alpha1.DatadogAgentProfile) * return nodeAgentOverride.PriorityClassName } -// sortProfiles sorts the profiles by creation timestamp. If two profiles have +// SortProfiles sorts the profiles by creation timestamp. If two profiles have // the same creation timestamp, it sorts them by name. -func sortProfiles(profiles []datadoghqv1alpha1.DatadogAgentProfile) []datadoghqv1alpha1.DatadogAgentProfile { +func SortProfiles(profiles []datadoghqv1alpha1.DatadogAgentProfile) []datadoghqv1alpha1.DatadogAgentProfile { sortedProfiles := make([]datadoghqv1alpha1.DatadogAgentProfile, len(profiles)) copy(sortedProfiles, profiles) diff --git a/pkg/agentprofile/agent_profile_test.go b/pkg/agentprofile/agent_profile_test.go index ed9efc30e..a569a7038 100644 --- a/pkg/agentprofile/agent_profile_test.go +++ b/pkg/agentprofile/agent_profile_test.go @@ -11,7 +11,6 @@ import ( "time" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -27,21 +26,18 @@ import ( const testNamespace = "default" -func TestProfilesToApply(t *testing.T) { - t1 := time.Now() - t2 := t1.Add(time.Minute) - t3 := t2.Add(time.Minute) - +func TestProfileToApply(t *testing.T) { tests := []struct { name string - profiles []v1alpha1.DatadogAgentProfile + profile v1alpha1.DatadogAgentProfile nodes []v1.Node - expectedProfiles []v1alpha1.DatadogAgentProfile + profileAppliedByNode map[string]types.NamespacedName expectedProfilesAppliedPerNode map[string]types.NamespacedName + expectedErr error }{ { - name: "no profiles", - profiles: []v1alpha1.DatadogAgentProfile{}, + name: "empty profile, empty profileAppliedByNode", + profile: v1alpha1.DatadogAgentProfile{}, nodes: []v1.Node{ { ObjectMeta: metav1.ObjectMeta{ @@ -52,30 +48,58 @@ func TestProfilesToApply(t *testing.T) { }, }, }, - expectedProfiles: []v1alpha1.DatadogAgentProfile{ + profileAppliedByNode: map[string]types.NamespacedName{}, + expectedProfilesAppliedPerNode: map[string]types.NamespacedName{}, + expectedErr: fmt.Errorf("profileAffinity must be defined"), + }, + { + name: "empty profile, non-empty profileAppliedByNode", + profile: v1alpha1.DatadogAgentProfile{}, + nodes: []v1.Node{ { ObjectMeta: metav1.ObjectMeta{ - Name: "default", - }, - Spec: v1alpha1.DatadogAgentProfileSpec{ - ProfileAffinity: nil, // Applies to all nodes - Config: nil, // No overrides + Name: "node1", + Labels: map[string]string{ + "os": "linux", + }, }, }, }, + profileAppliedByNode: map[string]types.NamespacedName{ + "node1": { + Namespace: testNamespace, + Name: "linux", + }, + }, expectedProfilesAppliedPerNode: map[string]types.NamespacedName{ "node1": { - Namespace: "", - Name: "default", + Namespace: testNamespace, + Name: "linux", }, }, + expectedErr: fmt.Errorf("profileAffinity must be defined"), }, { - name: "several non-conflicting profiles", - profiles: []v1alpha1.DatadogAgentProfile{ - exampleProfileForLinux(), - exampleProfileForWindows(), + name: "empty profile, , non-empty profileAppliedByNode, no nodes", + profile: v1alpha1.DatadogAgentProfile{}, + nodes: []v1.Node{}, + profileAppliedByNode: map[string]types.NamespacedName{ + "node1": { + Namespace: testNamespace, + Name: "linux", + }, + }, + expectedProfilesAppliedPerNode: map[string]types.NamespacedName{ + "node1": { + Namespace: testNamespace, + Name: "linux", + }, }, + expectedErr: fmt.Errorf("profileAffinity must be defined"), + }, + { + name: "non-conflicting profile, empty profileAppliedByNode", + profile: exampleProfileForLinux(), nodes: []v1.Node{ { ObjectMeta: metav1.ObjectMeta{ @@ -86,99 +110,27 @@ func TestProfilesToApply(t *testing.T) { }, }, }, - expectedProfiles: []v1alpha1.DatadogAgentProfile{ - exampleProfileForLinux(), - exampleProfileForWindows(), - defaultProfile(), - }, + profileAppliedByNode: map[string]types.NamespacedName{}, expectedProfilesAppliedPerNode: map[string]types.NamespacedName{ "node1": { Namespace: testNamespace, Name: "linux", }, }, + expectedErr: nil, }, { - // This test defines 3 profiles created in this order: profile-2, - // profile-1, profile-3 (not sorted here to make sure that the code does). - // - profile-1 and profile-2 conflict, but profile-2 is the oldest, - // so it wins. - // - profile-1 and profile-3 conflict, but profile-1 is not applied - // because of the conflict with profile-2, so profile-3 should be. - // So in this case, the returned profiles should be profile-2, - // profile-3 and a default one. - name: "several conflicting profiles with different creation timestamps", - profiles: []v1alpha1.DatadogAgentProfile{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: "profile-1", - CreationTimestamp: metav1.NewTime(t2), - }, - Spec: v1alpha1.DatadogAgentProfileSpec{ - ProfileAffinity: &v1alpha1.ProfileAffinity{ - ProfileNodeAffinity: []v1.NodeSelectorRequirement{ - { - Key: "a", - Operator: v1.NodeSelectorOpIn, - Values: []string{"1"}, - }, - }, - }, - Config: configWithCPURequestOverrideForCoreAgent("100m"), - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: "profile-2", - CreationTimestamp: metav1.NewTime(t1), - }, - Spec: v1alpha1.DatadogAgentProfileSpec{ - ProfileAffinity: &v1alpha1.ProfileAffinity{ - ProfileNodeAffinity: []v1.NodeSelectorRequirement{ - { - Key: "b", - Operator: v1.NodeSelectorOpIn, - Values: []string{"1"}, - }, - }, - }, - Config: configWithCPURequestOverrideForCoreAgent("200m"), - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: "profile-3", - CreationTimestamp: metav1.NewTime(t3), - }, - Spec: v1alpha1.DatadogAgentProfileSpec{ - ProfileAffinity: &v1alpha1.ProfileAffinity{ - ProfileNodeAffinity: []v1.NodeSelectorRequirement{ - { - Key: "c", - Operator: v1.NodeSelectorOpIn, - Values: []string{"1"}, - }, - }, - }, - Config: configWithCPURequestOverrideForCoreAgent("300m"), - }, - }, - }, + name: "non-conflicting profile, non-empty profileAppliedByNode", + profile: exampleProfileForLinux(), nodes: []v1.Node{ - // node1 matches profile-1 and profile-3 { ObjectMeta: metav1.ObjectMeta{ Name: "node1", Labels: map[string]string{ - "a": "1", - "c": "1", + "os": "linux", }, }, }, - // node2 matches profile-2 { ObjectMeta: metav1.ObjectMeta{ Name: "node2", @@ -187,194 +139,80 @@ func TestProfilesToApply(t *testing.T) { }, }, }, - // node3 matches profile-1 and profile-2 - { - ObjectMeta: metav1.ObjectMeta{ - Name: "node3", - Labels: map[string]string{ - "a": "1", - "b": "1", - }, - }, - }, }, - expectedProfiles: []v1alpha1.DatadogAgentProfile{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: "profile-2", - CreationTimestamp: metav1.NewTime(t1), - }, - Spec: v1alpha1.DatadogAgentProfileSpec{ - ProfileAffinity: &v1alpha1.ProfileAffinity{ - ProfileNodeAffinity: []v1.NodeSelectorRequirement{ - { - Key: "b", - Operator: v1.NodeSelectorOpIn, - Values: []string{"1"}, - }, - }, - }, - Config: configWithCPURequestOverrideForCoreAgent("200m"), - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: "profile-3", - CreationTimestamp: metav1.NewTime(t3), - }, - Spec: v1alpha1.DatadogAgentProfileSpec{ - ProfileAffinity: &v1alpha1.ProfileAffinity{ - ProfileNodeAffinity: []v1.NodeSelectorRequirement{ - { - Key: "c", - Operator: v1.NodeSelectorOpIn, - Values: []string{"1"}, - }, - }, - }, - Config: configWithCPURequestOverrideForCoreAgent("300m"), - }, + profileAppliedByNode: map[string]types.NamespacedName{ + "node2": { + Namespace: testNamespace, + Name: "windows", }, - defaultProfile(), }, expectedProfilesAppliedPerNode: map[string]types.NamespacedName{ "node1": { Namespace: testNamespace, - Name: "profile-3", + Name: "linux", }, "node2": { Namespace: testNamespace, - Name: "profile-2", - }, - "node3": { - Namespace: testNamespace, - Name: "profile-2", + Name: "windows", }, }, + expectedErr: nil, }, { - // This test defines 3 profiles with the same creation timestamp: - // profile-2, profile-1, profile-3 (not sorted alphabetically here - // to make sure that the code does). - // The 3 profiles conflict and only profile-1 should apply because - // it's the first one alphabetically. - name: "conflicting profiles with the same creation timestamp", - profiles: []v1alpha1.DatadogAgentProfile{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: "profile-2", - CreationTimestamp: metav1.NewTime(t1), - }, - Spec: v1alpha1.DatadogAgentProfileSpec{ - ProfileAffinity: &v1alpha1.ProfileAffinity{ - ProfileNodeAffinity: []v1.NodeSelectorRequirement{ - { - Key: "a", - Operator: v1.NodeSelectorOpIn, - Values: []string{"1"}, - }, - }, - }, - Config: configWithCPURequestOverrideForCoreAgent("100m"), - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: "profile-1", - CreationTimestamp: metav1.NewTime(t1), - }, - Spec: v1alpha1.DatadogAgentProfileSpec{ - ProfileAffinity: &v1alpha1.ProfileAffinity{ - ProfileNodeAffinity: []v1.NodeSelectorRequirement{ - { - Key: "b", - Operator: v1.NodeSelectorOpIn, - Values: []string{"1"}, - }, - }, - }, - Config: configWithCPURequestOverrideForCoreAgent("200m"), - }, + name: "non-conflicting profile, non-empty profileAppliedByNode, no nodes", + profile: exampleProfileForLinux(), + nodes: []v1.Node{}, + profileAppliedByNode: map[string]types.NamespacedName{ + "node2": { + Namespace: testNamespace, + Name: "windows", }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: "profile-3", - CreationTimestamp: metav1.NewTime(t1), - }, - Spec: v1alpha1.DatadogAgentProfileSpec{ - ProfileAffinity: &v1alpha1.ProfileAffinity{ - ProfileNodeAffinity: []v1.NodeSelectorRequirement{ - { - Key: "c", - Operator: v1.NodeSelectorOpIn, - Values: []string{"1"}, - }, - }, - }, - Config: configWithCPURequestOverrideForCoreAgent("300m"), - }, + }, + expectedProfilesAppliedPerNode: map[string]types.NamespacedName{ + "node2": { + Namespace: testNamespace, + Name: "windows", }, }, + expectedErr: nil, + }, + { + name: "conflicting profile", + profile: exampleProfileForLinux(), nodes: []v1.Node{ - // matches all profiles { ObjectMeta: metav1.ObjectMeta{ Name: "node1", Labels: map[string]string{ - "a": "1", - "b": "1", - "c": "1", + "os": "linux", }, }, }, - }, - expectedProfiles: []v1alpha1.DatadogAgentProfile{ { ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: "profile-1", - CreationTimestamp: metav1.NewTime(t1), - }, - Spec: v1alpha1.DatadogAgentProfileSpec{ - ProfileAffinity: &v1alpha1.ProfileAffinity{ - ProfileNodeAffinity: []v1.NodeSelectorRequirement{ - { - Key: "b", - Operator: v1.NodeSelectorOpIn, - Values: []string{"1"}, - }, - }, + Name: "node2", + Labels: map[string]string{ + "os": "windows", }, - Config: configWithCPURequestOverrideForCoreAgent("200m"), }, }, - defaultProfile(), }, - expectedProfilesAppliedPerNode: map[string]types.NamespacedName{ + profileAppliedByNode: map[string]types.NamespacedName{ "node1": { Namespace: testNamespace, - Name: "profile-1", + Name: "linux", + }, + }, expectedProfilesAppliedPerNode: map[string]types.NamespacedName{ + "node1": { + Namespace: testNamespace, + Name: "linux", }, }, + expectedErr: fmt.Errorf("conflict with existing profile"), }, { - name: "invalid profile", - profiles: []v1alpha1.DatadogAgentProfile{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: "invalid", - }, - Spec: v1alpha1.DatadogAgentProfileSpec{ - Config: configWithCPURequestOverrideForCoreAgent("100m"), - }, - }, - }, + name: "invalid profile", + profile: exampleInvalidProfile(), nodes: []v1.Node{ { ObjectMeta: metav1.ObjectMeta{ @@ -384,80 +222,37 @@ func TestProfilesToApply(t *testing.T) { }, }, }, - }, - expectedProfiles: []v1alpha1.DatadogAgentProfile{ - defaultProfile(), - }, - expectedProfilesAppliedPerNode: map[string]types.NamespacedName{ - "node1": { - Namespace: "", - Name: "default", - }, - }, - }, - { - name: "invalid profiles + valid profiles", - profiles: []v1alpha1.DatadogAgentProfile{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: "invalid-no-affinity", - }, - Spec: v1alpha1.DatadogAgentProfileSpec{ - Config: configWithCPURequestOverrideForCoreAgent("100m"), - }, - }, { ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: "invalid-no-config", - }, - Spec: v1alpha1.DatadogAgentProfileSpec{ - ProfileAffinity: &v1alpha1.ProfileAffinity{ - ProfileNodeAffinity: []v1.NodeSelectorRequirement{ - { - Key: "os", - Operator: v1.NodeSelectorOpIn, - Values: []string{"linux"}, - }, - }, - }, - }, - }, - exampleProfileForLinux(), - exampleProfileForWindows(), - }, - nodes: []v1.Node{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "node1", + Name: "node2", Labels: map[string]string{ - "os": "linux", + "os": "windows", }, }, }, }, - expectedProfiles: []v1alpha1.DatadogAgentProfile{ - exampleProfileForLinux(), - exampleProfileForWindows(), - defaultProfile(), - }, - expectedProfilesAppliedPerNode: map[string]types.NamespacedName{ + profileAppliedByNode: map[string]types.NamespacedName{ + "node1": { + Namespace: testNamespace, + Name: "linux", + }, + }, expectedProfilesAppliedPerNode: map[string]types.NamespacedName{ "node1": { Namespace: testNamespace, Name: "linux", }, }, + expectedErr: fmt.Errorf("profileAffinity must be defined"), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { testLogger := zap.New(zap.UseDevMode(true)) - profilesToApply, profileAppliedPerNode, err := ProfilesToApply(test.profiles, test.nodes, testLogger) - require.NoError(t, err) - assert.ElementsMatch(t, test.expectedProfiles, profilesToApply) - assert.Equal(t, test.expectedProfilesAppliedPerNode, profileAppliedPerNode) + now := metav1.NewTime(time.Now()) + profileAppliedByNode, err := ProfileToApply(testLogger, &test.profile, test.nodes, test.profileAppliedByNode, now) + assert.Equal(t, test.expectedErr, err) + assert.Equal(t, test.expectedProfilesAppliedPerNode, profileAppliedByNode) }) } } @@ -722,6 +517,19 @@ func exampleProfileForWindows() v1alpha1.DatadogAgentProfile { } } +func exampleInvalidProfile() v1alpha1.DatadogAgentProfile { + return v1alpha1.DatadogAgentProfile{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: "windows", + }, + Spec: v1alpha1.DatadogAgentProfileSpec{ + // missing ProfileAffinity + Config: configWithCPURequestOverrideForCoreAgent("200m"), + }, + } +} + // configWithCPURequestOverrideForCoreAgent returns a config with a CPU request // for the core agent container. func configWithCPURequestOverrideForCoreAgent(cpuRequest string) *v1alpha1.Config { diff --git a/pkg/agentprofile/status.go b/pkg/agentprofile/status.go new file mode 100644 index 000000000..b3438c311 --- /dev/null +++ b/pkg/agentprofile/status.go @@ -0,0 +1,83 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +package agentprofile + +import ( + datadoghqv1alpha1 "github.com/DataDog/datadog-operator/apis/datadoghq/v1alpha1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // ValidConditionType is a type of condition for a DatadogAgentProfile + ValidConditionType = "Valid" + // ValidConditionType is a type of condition for a DatadogAgentProfile + AppliedConditionType = "Applied" + + // ValidConditionReason is for DatadogAgentProfiles with a valid manifest + ValidConditionReason = "Valid" + // InvalidConditionReason is for DatadogAgentProfiles with an invalid manifest + InvalidConditionReason = "Invalid" + // AppliedConditionReason is for DatadogAgentProfiles that are applied to at least one node + AppliedConditionReason = "Applied" + // ConflictConditionReason is for DatadogAgentProfiles that conflict with an existing DatadogAgentProfile + ConflictConditionReason = "Conflict" +) + +func UpdateProfileStatus(profile *datadoghqv1alpha1.DatadogAgentProfile, profileStatus datadoghqv1alpha1.DatadogAgentProfileStatus, now metav1.Time) { + if profile == nil { + profile.Status = datadoghqv1alpha1.DatadogAgentProfileStatus{ + LastUpdate: &now, + CurrentHash: "", + Conditions: []metav1.Condition{ + NewDatadogAgentProfileCondition(ValidConditionType, metav1.ConditionFalse, now, InvalidConditionReason, "Profile is empty"), + }, + } + return + } + + profileStatus.LastUpdate = &now + if profileStatus.Valid == "" { + profileStatus.Valid = metav1.ConditionUnknown + } + if profileStatus.Applied == "" { + profileStatus.Applied = metav1.ConditionUnknown + } + + profile.Status = profileStatus +} + +// NewDatadogAgentProfileCondition returns a new metav1.Condition instance +func NewDatadogAgentProfileCondition(conditionType string, conditionStatus metav1.ConditionStatus, now metav1.Time, reason, message string) metav1.Condition { + return metav1.Condition{ + Type: conditionType, + Status: conditionStatus, + LastTransitionTime: now, + Reason: reason, + Message: message, + } +} + +// SetDatadogAgentProfileCondition is used to update a condition +func SetDatadogAgentProfileCondition(conditionsList []metav1.Condition, newCondition metav1.Condition) []metav1.Condition { + if newCondition.Type == "" { + return conditionsList + } + + found := false + for i, condition := range conditionsList { + if newCondition.Type == condition.Type { + found = true + conditionsList[i] = newCondition + } + } + + if !found { + conditionsList = append(conditionsList, newCondition) + } + + return conditionsList +} diff --git a/pkg/agentprofile/status_test.go b/pkg/agentprofile/status_test.go new file mode 100644 index 000000000..e169aacdc --- /dev/null +++ b/pkg/agentprofile/status_test.go @@ -0,0 +1,134 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +package agentprofile + +import ( + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestSetDatadogAgentProfileCondition(t *testing.T) { + now := metav1.Now() + tests := []struct { + name string + existingConditionsList []metav1.Condition + condition metav1.Condition + expectedConditionsList []metav1.Condition + }{ + { + name: "empty existingConditionsList, empty condition", + existingConditionsList: []metav1.Condition{}, + condition: metav1.Condition{}, + expectedConditionsList: []metav1.Condition{}, + }, + { + name: "empty existingConditionsList, non-empty condition", + existingConditionsList: []metav1.Condition{}, + condition: metav1.Condition{ + Type: "foo-type", + Status: metav1.ConditionTrue, + LastTransitionTime: now, + Reason: "foo-reason", + Message: "foo-message", + }, + expectedConditionsList: []metav1.Condition{ + { + Type: "foo-type", + Status: metav1.ConditionTrue, + LastTransitionTime: now, + Reason: "foo-reason", + Message: "foo-message", + }, + }, + }, + { + name: "non-empty existingConditionsList, non-empty condition, different types", + existingConditionsList: []metav1.Condition{ + { + Type: "bar-type", + Status: metav1.ConditionFalse, + LastTransitionTime: now, + Reason: "bar-reason", + Message: "bar-message", + }, + }, + condition: metav1.Condition{ + Type: "foo-type", + Status: metav1.ConditionTrue, + LastTransitionTime: now, + Reason: "foo-reason", + Message: "foo-message", + }, + expectedConditionsList: []metav1.Condition{ + { + Type: "bar-type", + Status: metav1.ConditionFalse, + LastTransitionTime: now, + Reason: "bar-reason", + Message: "bar-message", + }, + { + Type: "foo-type", + Status: metav1.ConditionTrue, + LastTransitionTime: now, + Reason: "foo-reason", + Message: "foo-message", + }, + }, + }, + { + name: "non-empty existingConditionsList, non-empty condition, same types", + existingConditionsList: []metav1.Condition{ + { + Type: "foo-type", + Status: metav1.ConditionTrue, + LastTransitionTime: now, + Reason: "foo-reason", + Message: "foo-message", + }, + { + Type: "bar-type", + Status: metav1.ConditionFalse, + LastTransitionTime: now, + Reason: "bar-reason", + Message: "bar-message", + }, + }, + condition: metav1.Condition{ + Type: "foo-type", + Status: metav1.ConditionUnknown, + LastTransitionTime: now, + Reason: "foo2-reason", + Message: "foo2-message", + }, + expectedConditionsList: []metav1.Condition{ + { + Type: "foo-type", + Status: metav1.ConditionUnknown, + LastTransitionTime: now, + Reason: "foo2-reason", + Message: "foo2-message", + }, + { + Type: "bar-type", + Status: metav1.ConditionFalse, + LastTransitionTime: now, + Reason: "bar-reason", + Message: "bar-message", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conditionsList := SetDatadogAgentProfileCondition(tt.existingConditionsList, tt.condition) + assert.Equal(t, tt.expectedConditionsList, conditionsList) + }) + } +}