diff --git a/manifests/minimal-postgres-manifest.yaml b/manifests/minimal-postgres-manifest.yaml index d22327905..f3ed3768d 100644 --- a/manifests/minimal-postgres-manifest.yaml +++ b/manifests/minimal-postgres-manifest.yaml @@ -2,6 +2,8 @@ apiVersion: "acid.zalan.do/v1" kind: postgresql metadata: name: acid-minimal-cluster + labels: + cluster-name: acid-minimal-cluster spec: teamId: "acid" volume: diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index 4bd757f38..e9ae3acf6 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -20,6 +20,10 @@ spec: storage: true subresources: status: {} + scale: + specReplicasPath: .spec.numberOfInstances + statusReplicasPath: .status.numberOfInstances + labelSelectorPath: .status.labelSelector additionalPrinterColumns: - name: Team type: string @@ -51,7 +55,7 @@ spec: - name: Status type: string description: Current sync status of postgresql resource - jsonPath: .status.PostgresClusterStatus + jsonPath: .status.postgresClusterStatus schema: openAPIV3Schema: type: object @@ -677,5 +681,30 @@ spec: type: integer status: type: object - additionalProperties: - type: string + properties: + postgresClusterStatus: + type: string + numberOfInstances: + format: int32 + type: integer + labelSelector: + type: string + observedGeneration: + format: int64 + type: integer + conditions: + type: array + items: + type: object + properties: + type: + type: string + status: + type: string + lastTransitionTime: + type: string + format: date-time + reason: + type: string + message: + type: string diff --git a/pkg/apis/acid.zalan.do/v1/crds.go b/pkg/apis/acid.zalan.do/v1/crds.go index 9e65869e7..f56bd0a89 100644 --- a/pkg/apis/acid.zalan.do/v1/crds.go +++ b/pkg/apis/acid.zalan.do/v1/crds.go @@ -23,6 +23,10 @@ const ( OperatorConfigCRDResourceList = OperatorConfigCRDResouceKind + "List" OperatorConfigCRDResourceName = OperatorConfigCRDResourcePlural + "." + acidzalando.GroupName OperatorConfigCRDResourceShort = "opconfig" + + specReplicasPath = ".spec.numberOfInstances" + statusReplicasPath = ".status.numberOfInstances" + labelSelectorPath = ".status.labelSelector" ) // PostgresCRDResourceColumns definition of AdditionalPrinterColumns for postgresql CRD @@ -72,7 +76,7 @@ var PostgresCRDResourceColumns = []apiextv1.CustomResourceColumnDefinition{ Name: "Status", Type: "string", Description: "Current sync status of postgresql resource", - JSONPath: ".status.PostgresClusterStatus", + JSONPath: ".status.postgresClusterStatus", }, } @@ -1106,10 +1110,47 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{ }, "status": { Type: "object", - AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{ - Schema: &apiextv1.JSONSchemaProps{ + Properties: map[string]apiextv1.JSONSchemaProps{ + "postgresClusterStatus": { + Type: "string", + }, + "numberOfInstances": { + Type: "integer", + Format: "int32", + }, + "labelSelector": { Type: "string", }, + "observedGeneration": { + Type: "integer", + Format: "int64", + }, + "conditions": { + Type: "array", + Items: &apiextv1.JSONSchemaPropsOrArray{ + Schema: &apiextv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextv1.JSONSchemaProps{ + "type": { + Type: "string", + }, + "status": { + Type: "string", + }, + "lastTransitionTime": { + Type: "string", + Format: "date-time", + }, + "reason": { + Type: "string", + }, + "message": { + Type: "string", + }, + }, + }, + }, + }, }, }, }, @@ -1983,7 +2024,7 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{ func buildCRD(name, kind, plural, list, short string, categories []string, columns []apiextv1.CustomResourceColumnDefinition, - validation apiextv1.CustomResourceValidation) *apiextv1.CustomResourceDefinition { + validation apiextv1.CustomResourceValidation, specReplicasPath string, statusReplicasPath string, labelSelectorPath string) *apiextv1.CustomResourceDefinition { return &apiextv1.CustomResourceDefinition{ TypeMeta: metav1.TypeMeta{ APIVersion: fmt.Sprintf("%s/%s", apiextv1.GroupName, apiextv1.SchemeGroupVersion.Version), @@ -2010,6 +2051,11 @@ func buildCRD(name, kind, plural, list, short string, Storage: true, Subresources: &apiextv1.CustomResourceSubresources{ Status: &apiextv1.CustomResourceSubresourceStatus{}, + Scale: &apiextv1.CustomResourceSubresourceScale{ + SpecReplicasPath: specReplicasPath, + StatusReplicasPath: statusReplicasPath, + LabelSelectorPath: &labelSelectorPath, + }, }, AdditionalPrinterColumns: columns, Schema: &validation, @@ -2028,7 +2074,10 @@ func PostgresCRD(crdCategories []string) *apiextv1.CustomResourceDefinition { PostgresCRDResourceShort, crdCategories, PostgresCRDResourceColumns, - PostgresCRDResourceValidation) + PostgresCRDResourceValidation, + specReplicasPath, + statusReplicasPath, + labelSelectorPath) } // ConfigurationCRD returns CustomResourceDefinition built from OperatorConfigCRDResource @@ -2040,5 +2089,8 @@ func ConfigurationCRD(crdCategories []string) *apiextv1.CustomResourceDefinition OperatorConfigCRDResourceShort, crdCategories, OperatorConfigCRDResourceColumns, - OperatorConfigCRDResourceValidation) + OperatorConfigCRDResourceValidation, + specReplicasPath, + statusReplicasPath, + labelSelectorPath) } diff --git a/pkg/apis/acid.zalan.do/v1/postgresql_type.go b/pkg/apis/acid.zalan.do/v1/postgresql_type.go index 612cf7041..8576e4c2f 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql_type.go +++ b/pkg/apis/acid.zalan.do/v1/postgresql_type.go @@ -3,6 +3,7 @@ package v1 // Postgres CRD definition, please use CamelCase for field names. import ( + "k8s.io/apimachinery/pkg/api/equality" "time" v1 "k8s.io/api/core/v1" @@ -225,9 +226,48 @@ type Sidecar struct { // UserFlags defines flags (such as superuser, nologin) that could be assigned to individual users type UserFlags []string +type Conditions []Condition + +type PostgresqlConditionType string +type VolatileTime struct { + Inner metav1.Time `json:",inline"` +} + +// MarshalJSON implements the json.Marshaler interface. +func (t VolatileTime) MarshalJSON() ([]byte, error) { + return t.Inner.MarshalJSON() +} + +// UnmarshalJSON implements the json.Unmarshaller interface. +func (t *VolatileTime) UnmarshalJSON(b []byte) error { + return t.Inner.UnmarshalJSON(b) +} + +func init() { + equality.Semantic.AddFunc( + // Always treat VolatileTime fields as equivalent. + func(VolatileTime, VolatileTime) bool { + return true + }, + ) +} + +// Condition contains the conditions of the PostgreSQL cluster +type Condition struct { + Type PostgresqlConditionType `json:"type" description:"type of status condition"` + Status v1.ConditionStatus `json:"status" description:"status of the condition, one of True, False, Unknown"` + LastTransitionTime VolatileTime `json:"lastTransitionTime,omitempty" description:"last time the condition transit from one status to another"` + Reason string `json:"reason,omitempty" description:"one-word CamelCase reason for the condition's last transition"` + Message string `json:"message,omitempty" description:"human-readable message indicating details about last transition"` +} + // PostgresStatus contains status of the PostgreSQL cluster (running, creation failed etc.) type PostgresStatus struct { - PostgresClusterStatus string `json:"PostgresClusterStatus"` + PostgresClusterStatus string `json:"postgresClusterStatus"` + NumberOfInstances int32 `json:"numberOfInstances"` + LabelSelector string `json:"labelSelector"` + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + Conditions Conditions `json:"conditions,omitempty"` } // ConnectionPooler Options for connection pooler diff --git a/pkg/apis/acid.zalan.do/v1/util.go b/pkg/apis/acid.zalan.do/v1/util.go index 719defe93..3f22f9f44 100644 --- a/pkg/apis/acid.zalan.do/v1/util.go +++ b/pkg/apis/acid.zalan.do/v1/util.go @@ -100,7 +100,3 @@ func (postgresStatus PostgresStatus) Running() bool { func (postgresStatus PostgresStatus) Creating() bool { return postgresStatus.PostgresClusterStatus == ClusterStatusCreating } - -func (postgresStatus PostgresStatus) String() string { - return postgresStatus.PostgresClusterStatus -} diff --git a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go index 80bc7b34d..bddf4de90 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -98,6 +98,45 @@ func (in *CloneDescription) DeepCopy() *CloneDescription { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in Conditions) DeepCopyInto(out *Conditions) { + { + in := &in + *out = make(Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Conditions. +func (in Conditions) DeepCopy() Conditions { + if in == nil { + return nil + } + out := new(Conditions) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConnectionPooler) DeepCopyInto(out *ConnectionPooler) { *out = *in @@ -892,6 +931,13 @@ func (in *PostgresSpec) DeepCopy() *PostgresSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PostgresStatus) DeepCopyInto(out *PostgresStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -1053,7 +1099,7 @@ func (in *Postgresql) DeepCopyInto(out *Postgresql) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -1444,6 +1490,23 @@ func (in UserFlags) DeepCopy() UserFlags { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VolatileTime) DeepCopyInto(out *VolatileTime) { + *out = *in + in.Inner.DeepCopyInto(&out.Inner) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VolatileTime. +func (in *VolatileTime) DeepCopy() *VolatileTime { + if in == nil { + return nil + } + out := new(VolatileTime) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Volume) DeepCopyInto(out *Volume) { *out = *in diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 86aaa4788..8d212da4f 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -255,16 +255,33 @@ func (c *Cluster) Create() (err error) { ss *appsv1.StatefulSet ) + //Even though its possible to propogate other CR labels to the pods, picking the default label here since its propogated to all the pods by default. But this means that in order for the scale subresource to work properly, user must set the "cluster-name" key in their CRs with value matching the CR name. + labelstring := fmt.Sprintf("%s=%s", c.OpConfig.ClusterNameLabel, c.Postgresql.ObjectMeta.Labels[c.OpConfig.ClusterNameLabel]) + defer func() { var ( pgUpdatedStatus *acidv1.Postgresql errStatus error ) if err == nil { - pgUpdatedStatus, errStatus = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusRunning) //TODO: are you sure it's running? + ClusterStatus := acidv1.PostgresStatus{ + PostgresClusterStatus: acidv1.ClusterStatusRunning, + NumberOfInstances: c.Postgresql.Spec.NumberOfInstances, + LabelSelector: labelstring, + ObservedGeneration: c.Postgresql.Generation, + Conditions: c.Postgresql.Status.Conditions, + } + pgUpdatedStatus, errStatus = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), ClusterStatus, "") //TODO: are you sure it's running? } else { c.logger.Warningf("cluster created failed: %v", err) - pgUpdatedStatus, errStatus = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusAddFailed) + ClusterStatus := acidv1.PostgresStatus{ + PostgresClusterStatus: acidv1.ClusterStatusAddFailed, + NumberOfInstances: 0, + LabelSelector: labelstring, + ObservedGeneration: 0, + Conditions: c.Postgresql.Status.Conditions, + } + pgUpdatedStatus, errStatus = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), ClusterStatus, err.Error()) } if errStatus != nil { c.logger.Warningf("could not set cluster status: %v", errStatus) @@ -274,7 +291,14 @@ func (c *Cluster) Create() (err error) { } }() - pgCreateStatus, err = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusCreating) + ClusterStatus := acidv1.PostgresStatus{ + PostgresClusterStatus: acidv1.ClusterStatusCreating, + NumberOfInstances: 0, + LabelSelector: labelstring, + ObservedGeneration: 0, + Conditions: c.Postgresql.Status.Conditions, + } + pgCreateStatus, err = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), ClusterStatus, "") if err != nil { return fmt.Errorf("could not set cluster status: %v", err) } @@ -927,7 +951,15 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { c.mu.Lock() defer c.mu.Unlock() - c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusUpdating) + labelstring := fmt.Sprintf("%s=%s", c.OpConfig.ClusterNameLabel, c.Postgresql.ObjectMeta.Labels[c.OpConfig.ClusterNameLabel]) + ClusterStatus := acidv1.PostgresStatus{ + PostgresClusterStatus: acidv1.ClusterStatusUpdating, + NumberOfInstances: c.Postgresql.Status.NumberOfInstances, + LabelSelector: labelstring, + ObservedGeneration: c.Postgresql.Status.ObservedGeneration, + Conditions: c.Postgresql.Status.Conditions, + } + c.KubeClient.SetPostgresCRDStatus(c.clusterName(), ClusterStatus, "") c.setSpec(newSpec) defer func() { @@ -936,9 +968,23 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { err error ) if updateFailed { - pgUpdatedStatus, err = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusUpdateFailed) + ClusterStatus := acidv1.PostgresStatus{ + PostgresClusterStatus: acidv1.ClusterStatusUpdateFailed, + NumberOfInstances: c.Postgresql.Status.NumberOfInstances, + LabelSelector: labelstring, + ObservedGeneration: c.Postgresql.Status.ObservedGeneration, + Conditions: c.Postgresql.Status.Conditions, + } + pgUpdatedStatus, err = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), ClusterStatus, err.Error()) } else { - pgUpdatedStatus, err = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusRunning) + ClusterStatus := acidv1.PostgresStatus{ + PostgresClusterStatus: acidv1.ClusterStatusRunning, + NumberOfInstances: newSpec.Spec.NumberOfInstances, + LabelSelector: labelstring, + ObservedGeneration: c.Postgresql.Generation, + Conditions: c.Postgresql.Status.Conditions, + } + pgUpdatedStatus, err = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), ClusterStatus, "") } if err != nil { c.logger.Warningf("could not set cluster status: %v", err) diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index b106fc722..f038cb03c 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -46,11 +46,26 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error { pgUpdatedStatus *acidv1.Postgresql errStatus error ) + labelstring := fmt.Sprintf("%s=%s", c.OpConfig.ClusterNameLabel, c.Postgresql.ObjectMeta.Labels[c.OpConfig.ClusterNameLabel]) if err != nil { c.logger.Warningf("error while syncing cluster state: %v", err) - pgUpdatedStatus, errStatus = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusSyncFailed) + ClusterStatus := acidv1.PostgresStatus{ + PostgresClusterStatus: acidv1.ClusterStatusSyncFailed, + NumberOfInstances: newSpec.Status.NumberOfInstances, + LabelSelector: labelstring, + ObservedGeneration: c.Postgresql.Status.ObservedGeneration, + Conditions: c.Postgresql.Status.Conditions, + } + pgUpdatedStatus, errStatus = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), ClusterStatus, errStatus.Error()) } else if !c.Status.Running() { - pgUpdatedStatus, errStatus = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusRunning) + ClusterStatus := acidv1.PostgresStatus{ + PostgresClusterStatus: acidv1.ClusterStatusRunning, + NumberOfInstances: newSpec.Spec.NumberOfInstances, + LabelSelector: labelstring, + ObservedGeneration: c.Postgresql.Generation, + Conditions: c.Postgresql.Status.Conditions, + } + pgUpdatedStatus, errStatus = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), ClusterStatus, "") } if errStatus != nil { c.logger.Warningf("could not set cluster status: %v", errStatus) diff --git a/pkg/controller/postgresql.go b/pkg/controller/postgresql.go index accc345ad..77e9e1585 100644 --- a/pkg/controller/postgresql.go +++ b/pkg/controller/postgresql.go @@ -161,7 +161,15 @@ func (c *Controller) acquireInitialListOfClusters() error { func (c *Controller) addCluster(lg *logrus.Entry, clusterName spec.NamespacedName, pgSpec *acidv1.Postgresql) (*cluster.Cluster, error) { if c.opConfig.EnableTeamIdClusternamePrefix { if _, err := acidv1.ExtractClusterName(clusterName.Name, pgSpec.Spec.TeamID); err != nil { - c.KubeClient.SetPostgresCRDStatus(clusterName, acidv1.ClusterStatusInvalid) + labelstring := fmt.Sprintf("%s=%s", c.opConfig.ClusterNameLabel, pgSpec.ObjectMeta.Labels[c.opConfig.ClusterNameLabel]) + ClusterStatus := acidv1.PostgresStatus{ + PostgresClusterStatus: acidv1.ClusterStatusInvalid, + NumberOfInstances: pgSpec.Status.NumberOfInstances, + LabelSelector: labelstring, + ObservedGeneration: pgSpec.Status.ObservedGeneration, + Conditions: pgSpec.Status.Conditions, + } + c.KubeClient.SetPostgresCRDStatus(clusterName, ClusterStatus, err.Error()) return nil, err } } @@ -207,10 +215,10 @@ func (c *Controller) processEvent(event ClusterEvent) { if event.EventType == EventRepair { runRepair, lastOperationStatus := cl.NeedsRepair() if !runRepair { - lg.Debugf("observed cluster status %s, repair is not required", lastOperationStatus) + lg.Debugf("observed cluster status %#v, repair is not required", lastOperationStatus) return } - lg.Debugf("observed cluster status %s, running sync scan to repair the cluster", lastOperationStatus) + lg.Debugf("observed cluster status %#v, running sync scan to repair the cluster", lastOperationStatus) event.EventType = EventSync } @@ -472,16 +480,26 @@ func (c *Controller) queueClusterEvent(informerOldSpec, informerNewSpec *acidv1. if clusterError != "" && eventType != EventDelete { c.logger.WithField("cluster-name", clusterName).Debugf("skipping %q event for the invalid cluster: %s", eventType, clusterError) + labelstring := fmt.Sprintf("%s=%s", c.opConfig.ClusterNameLabel, informerNewSpec.ObjectMeta.Labels[c.opConfig.ClusterNameLabel]) + ClusterStatus := acidv1.PostgresStatus{ + NumberOfInstances: informerNewSpec.Status.NumberOfInstances, + LabelSelector: labelstring, + ObservedGeneration: informerNewSpec.Status.ObservedGeneration, + Conditions: informerNewSpec.Status.Conditions, + } switch eventType { case EventAdd: - c.KubeClient.SetPostgresCRDStatus(clusterName, acidv1.ClusterStatusAddFailed) + ClusterStatus.PostgresClusterStatus = acidv1.ClusterStatusAddFailed + c.KubeClient.SetPostgresCRDStatus(clusterName, ClusterStatus, clusterError) c.eventRecorder.Eventf(c.GetReference(informerNewSpec), v1.EventTypeWarning, "Create", "%v", clusterError) case EventUpdate: - c.KubeClient.SetPostgresCRDStatus(clusterName, acidv1.ClusterStatusUpdateFailed) + ClusterStatus.PostgresClusterStatus = acidv1.ClusterStatusUpdateFailed + c.KubeClient.SetPostgresCRDStatus(clusterName, ClusterStatus, clusterError) c.eventRecorder.Eventf(c.GetReference(informerNewSpec), v1.EventTypeWarning, "Update", "%v", clusterError) default: - c.KubeClient.SetPostgresCRDStatus(clusterName, acidv1.ClusterStatusSyncFailed) + ClusterStatus.PostgresClusterStatus = acidv1.ClusterStatusSyncFailed + c.KubeClient.SetPostgresCRDStatus(clusterName, ClusterStatus, clusterError) c.eventRecorder.Eventf(c.GetReference(informerNewSpec), v1.EventTypeWarning, "Sync", "%v", clusterError) } diff --git a/pkg/util/k8sutil/k8sutil.go b/pkg/util/k8sutil/k8sutil.go index 7ae402fe3..48d31b77a 100644 --- a/pkg/util/k8sutil/k8sutil.go +++ b/pkg/util/k8sutil/k8sutil.go @@ -3,6 +3,7 @@ package k8sutil import ( "context" "fmt" + "time" b64 "encoding/base64" "encoding/json" @@ -192,10 +193,11 @@ func NewFromConfig(cfg *rest.Config) (KubernetesClient, error) { } // SetPostgresCRDStatus of Postgres cluster -func (client *KubernetesClient) SetPostgresCRDStatus(clusterName spec.NamespacedName, status string) (*apiacidv1.Postgresql, error) { +func (client *KubernetesClient) SetPostgresCRDStatus(clusterName spec.NamespacedName, pgStatus apiacidv1.PostgresStatus, message string) (*apiacidv1.Postgresql, error) { var pg *apiacidv1.Postgresql - var pgStatus apiacidv1.PostgresStatus - pgStatus.PostgresClusterStatus = status + + newConditions := updateConditions(pgStatus.Conditions, pgStatus.PostgresClusterStatus, message) + pgStatus.Conditions = newConditions patch, err := json.Marshal(struct { PgStatus interface{} `json:"status"` @@ -217,6 +219,88 @@ func (client *KubernetesClient) SetPostgresCRDStatus(clusterName spec.Namespaced return pg, nil } +func updateConditions(existingConditions apiacidv1.Conditions, currentStatus string, message string) apiacidv1.Conditions { + now := apiacidv1.VolatileTime{Inner: metav1.NewTime(time.Now())} + var readyCondition, reconciliationCondition *apiacidv1.Condition + + // Find existing conditions + for i := range existingConditions { + if existingConditions[i].Type == "Ready" { + readyCondition = &existingConditions[i] + } else if existingConditions[i].Type == "ReconciliationSuccessful" { + reconciliationCondition = &existingConditions[i] + } + } + + // Initialize conditions if not present + switch currentStatus { + case "Creating": + if reconciliationCondition == nil { + existingConditions = append(existingConditions, apiacidv1.Condition{Type: "ReconciliationSuccessful"}) + reconciliationCondition = &existingConditions[len(existingConditions)-1] + + } + default: + if readyCondition == nil { + existingConditions = append(existingConditions, apiacidv1.Condition{Type: "Ready"}) + readyCondition = &existingConditions[len(existingConditions)-1] + } + } + + // Safety checks to avoid nil pointer dereference + if readyCondition == nil { + readyCondition = &apiacidv1.Condition{Type: "Ready"} + existingConditions = append(existingConditions, *readyCondition) + } + + if reconciliationCondition == nil { + reconciliationCondition = &apiacidv1.Condition{Type: "ReconciliationSuccessful"} + existingConditions = append(existingConditions, *reconciliationCondition) + } + + // Update Ready condition + switch currentStatus { + case "Running": + readyCondition.Status = v1.ConditionTrue + readyCondition.LastTransitionTime = now + case "CreateFailed": + readyCondition.Status = v1.ConditionFalse + readyCondition.LastTransitionTime = now + case "UpdateFailed", "SyncFailed", "Invalid": + if readyCondition.Status == v1.ConditionFalse { + readyCondition.LastTransitionTime = now + } + case "Updating": + // not updatinf time, just setting the status + if readyCondition.Status == v1.ConditionFalse { + readyCondition.Status = v1.ConditionFalse + } else { + readyCondition.Status = v1.ConditionTrue + } + } + + // Update ReconciliationSuccessful condition + reconciliationCondition.LastTransitionTime = now + reconciliationCondition.Message = message + if currentStatus == "Running" { + reconciliationCondition.Status = v1.ConditionTrue + reconciliationCondition.Reason = "" + } else { + reconciliationCondition.Status = v1.ConditionFalse + reconciliationCondition.Reason = currentStatus + } + // Directly modify elements in the existingConditions slice + for i := range existingConditions { + if existingConditions[i].Type == "Ready" && readyCondition != nil { + existingConditions[i] = *readyCondition + } else if existingConditions[i].Type == "ReconciliationSuccessful" && reconciliationCondition != nil { + existingConditions[i] = *reconciliationCondition + } + } + + return existingConditions +} + // SetFinalizer of Postgres cluster func (client *KubernetesClient) SetFinalizer(clusterName spec.NamespacedName, pg *apiacidv1.Postgresql, finalizers []string) (*apiacidv1.Postgresql, error) { var (