diff --git a/config/crd/bases/postgres-operator.crunchydata.com_crunchybridgeclusters.yaml b/config/crd/bases/postgres-operator.crunchydata.com_crunchybridgeclusters.yaml index 9e22b24797..3ac46af410 100644 --- a/config/crd/bases/postgres-operator.crunchydata.com_crunchybridgeclusters.yaml +++ b/config/crd/bases/postgres-operator.crunchydata.com_crunchybridgeclusters.yaml @@ -103,17 +103,13 @@ spec: items: properties: name: - description: 'The name of this PostgreSQL role. The value may - contain only lowercase letters, numbers, and hyphen so that - it fits into Kubernetes metadata. The above is problematic - for us as Bridge has a role with an underscore. TODO: figure - out underscore dilemma' - pattern: ^[A-Za-z][A-Za-z0-9\-_ ]*[A-Za-z0-9]$ + description: 'Name of the role within Crunchy Bridge. More info: + https://docs.crunchybridge.com/concepts/users' type: string secretName: description: The name of the Secret that will hold the role credentials. - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string required: - name @@ -151,57 +147,8 @@ spec: description: CrunchyBridgeClusterStatus defines the observed state of CrunchyBridgeCluster properties: - clusterStatus: - description: The cluster as represented by Bridge - properties: - id: - type: string - isHa: - type: boolean - majorVersion: - type: integer - name: - type: string - planId: - type: string - postgresVersionId: - anyOf: - - type: integer - - type: string - x-kubernetes-int-or-string: true - providerId: - type: string - regionId: - type: string - state: - type: string - storage: - format: int64 - type: integer - teamId: - type: string - type: object - clusterUpgradeResponse: - description: The cluster upgrade as represented by Bridge - properties: - operations: - items: - properties: - flavor: - type: string - starting_from: - type: string - state: - type: string - required: - - flavor - - starting_from - - state - type: object - type: array - type: object conditions: - description: conditions represent the observations of postgrescluster's + description: conditions represent the observations of postgres cluster's current state. items: description: "Condition contains details for one aspect of the current @@ -273,16 +220,66 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map + host: + description: The Hostname of the postgres cluster in Bridge, provided + by Bridge API and null until then. + type: string id: - description: The ID of the postgrescluster in Bridge, provided by + description: The ID of the postgres cluster in Bridge, provided by Bridge API and null until then. type: string + isHa: + description: Whether the cluster is high availability, meaning that + it has a secondary it can fail over to quickly in case the primary + becomes unavailable. + type: boolean + isProtected: + description: Whether the cluster is protected. Protected clusters + can't be destroyed until their protected flag is removed + type: boolean + majorVersion: + description: The cluster's major Postgres version. + type: integer + name: + description: The name of the cluster in Bridge. + type: string observedGeneration: description: observedGeneration represents the .metadata.generation on which the status was based. format: int64 minimum: 0 type: integer + ongoingUpgrade: + description: The cluster upgrade as represented by Bridge + items: + properties: + flavor: + type: string + starting_from: + type: string + state: + type: string + required: + - flavor + - starting_from + - state + type: object + type: array + planId: + description: The ID of the cluster's plan. Determines instance, CPU, + and memory. + type: string + responses: + description: Most recent, raw responses from Bridge API + type: object + x-kubernetes-preserve-unknown-fields: true + state: + description: State of cluster in Bridge. + type: string + storage: + description: The amount of storage available to the cluster in gigabytes. + format: int64 + type: integer type: object type: object served: true diff --git a/internal/bridge/client.go b/internal/bridge/client.go index 5bdb7087a9..c77baa22ff 100644 --- a/internal/bridge/client.go +++ b/internal/bridge/client.go @@ -27,6 +27,7 @@ import ( "strconv" "time" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/uuid" "k8s.io/apimachinery/pkg/util/wait" @@ -45,21 +46,172 @@ type Client struct { Version string } -type ClusterRole struct { +// BRIDGE API RESPONSE OBJECTS + +// ClusterApiResource is used to hold cluster information received in Bridge API response. +type ClusterApiResource struct { + ID string `json:"id,omitempty"` + ClusterGroup *ClusterGroupApiResource `json:"cluster_group,omitempty"` + PrimaryClusterID string `json:"cluster_id,omitempty"` + CPU int64 `json:"cpu,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + DiskUsage *ClusterDiskUsageApiResource `json:"disk_usage,omitempty"` + Environment string `json:"environment,omitempty"` + Host string `json:"host,omitempty"` + IsHA bool `json:"is_ha,omitempty"` + IsProtected bool `json:"is_protected,omitempty"` + IsSuspended bool `json:"is_suspended,omitempty"` + Keychain string `json:"keychain_id,omitempty"` + MaintenanceWindowStart int64 `json:"maintenance_window_start,omitempty"` + MajorVersion int `json:"major_version,omitempty"` + Memory float64 `json:"memory,omitempty"` + ClusterName string `json:"name,omitempty"` + Network string `json:"network_id,omitempty"` + Parent string `json:"parent_id,omitempty"` + Plan string `json:"plan_id,omitempty"` + PostgresVersion intstr.IntOrString `json:"postgres_version_id,omitempty"` + Provider string `json:"provider_id,omitempty"` + Region string `json:"region_id,omitempty"` + Replicas []*ClusterApiResource `json:"replicas,omitempty"` + Storage int64 `json:"storage,omitempty"` + Tailscale bool `json:"tailscale_active,omitempty"` + Team string `json:"team_id,omitempty"` + LastUpdate string `json:"updated_at,omitempty"` + ResponsePayload v1beta1.SchemalessObject `json:""` +} + +func (c *ClusterApiResource) AddDataToClusterStatus(cluster *v1beta1.CrunchyBridgeCluster) { + cluster.Status.ClusterName = c.ClusterName + cluster.Status.Host = c.Host + cluster.Status.ID = c.ID + cluster.Status.IsHA = c.IsHA + cluster.Status.IsProtected = c.IsProtected + cluster.Status.MajorVersion = c.MajorVersion + cluster.Status.Plan = c.Plan + cluster.Status.Storage = c.Storage + cluster.Status.Responses.Cluster = c.ResponsePayload +} + +type ClusterList struct { + Clusters []*ClusterApiResource `json:"clusters"` +} + +// ClusterDiskUsageApiResource hold information on disk usage for a particular cluster. +type ClusterDiskUsageApiResource struct { + DiskAvailableMB int64 `json:"disk_available_mb,omitempty"` + DiskTotalSizeMB int64 `json:"disk_total_size_mb,omitempty"` + DiskUsedMB int64 `json:"disk_used_mb,omitempty"` +} + +// ClusterGroupApiResource holds information on a ClusterGroup +type ClusterGroupApiResource struct { + ID string `json:"id,omitempty"` + Clusters []*ClusterApiResource `json:"clusters,omitempty"` + Kind string `json:"kind,omitempty"` + Name string `json:"name,omitempty"` + Network string `json:"network_id,omitempty"` + Provider string `json:"provider_id,omitempty"` + Region string `json:"region_id,omitempty"` + Team string `json:"team_id,omitempty"` +} + +type ClusterStatusApiResource struct { + DiskUsage *ClusterDiskUsageApiResource `json:"disk_usage,omitempty"` + OldestBackup string `json:"oldest_backup_at,omitempty"` + OngoingUpgrade *ClusterUpgradeApiResource `json:"ongoing_upgrade,omitempty"` + State string `json:"state,omitempty"` + ResponsePayload v1beta1.SchemalessObject `json:""` +} + +func (c *ClusterStatusApiResource) AddDataToClusterStatus(cluster *v1beta1.CrunchyBridgeCluster) { + cluster.Status.State = c.State + cluster.Status.Responses.Status = c.ResponsePayload +} + +type ClusterUpgradeApiResource struct { + ClusterID string `json:"cluster_id,omitempty"` + Operations []*v1beta1.UpgradeOperation `json:"operations,omitempty"` + Team string `json:"team_id,omitempty"` + ResponsePayload v1beta1.SchemalessObject `json:""` +} + +func (c *ClusterUpgradeApiResource) AddDataToClusterStatus(cluster *v1beta1.CrunchyBridgeCluster) { + cluster.Status.OngoingUpgrade = c.Operations + cluster.Status.Responses.Upgrade = c.ResponsePayload +} + +type ClusterUpgradeOperationApiResource struct { + Flavor string `json:"flavor,omitempty"` + StartingFrom string `json:"starting_from,omitempty"` + State string `json:"state,omitempty"` +} + +// BRIDGE API REQUEST PAYLOADS + +// PatchClustersRequestPayload is used for updating various properties of an existing cluster. +type PatchClustersRequestPayload struct { + ClusterGroup string `json:"cluster_group_id,omitempty"` + // DashboardSettings *ClusterDashboardSettings `json:"dashboard_settings,omitempty"` + // TODO: (dsessler7) Find docs for DashboardSettings and create appropriate struct + Environment string `json:"environment,omitempty"` + IsProtected bool `json:"is_protected,omitempty"` + MaintenanceWindowStart int64 `json:"maintenance_window_start,omitempty"` + Name string `json:"name,omitempty"` +} + +// PostClustersRequestPayload is used for creating a new cluster. +type PostClustersRequestPayload struct { + Name string `json:"name"` + Plan string `json:"plan_id"` + Team string `json:"team_id"` + ClusterGroup string `json:"cluster_group_id,omitempty"` + Environment string `json:"environment,omitempty"` + IsHA bool `json:"is_ha,omitempty"` + Keychain string `json:"keychain_id,omitempty"` + Network string `json:"network_id,omitempty"` + PostgresVersion intstr.IntOrString `json:"postgres_version_id,omitempty"` + Provider string `json:"provider_id,omitempty"` + Region string `json:"region_id,omitempty"` + Storage int64 `json:"storage,omitempty"` +} + +// PostClustersUpgradeRequestPayload is used for creating a new cluster upgrade which may include +// changing its plan, upgrading its major version, or increasing its storage size. +type PostClustersUpgradeRequestPayload struct { + Plan string `json:"plan_id,omitempty"` + PostgresVersion intstr.IntOrString `json:"postgres_version_id,omitempty"` + UpgradeStartTime string `json:"starting_from,omitempty"` + Storage int64 `json:"storage,omitempty"` +} + +// PutClustersUpgradeRequestPayload is used for updating an ongoing or scheduled upgrade. +type PutClustersUpgradeRequestPayload struct { + Plan string `json:"plan_id,omitempty"` + PostgresVersion intstr.IntOrString `json:"postgres_version_id,omitempty"` + UpgradeStartTime string `json:"starting_from,omitempty"` + Storage int64 `json:"storage,omitempty"` + UseMaintenanceWindow bool `json:"use_cluster_maintenance_window,omitempty"` +} + +// ClusterRoleApiResource is used for retrieving details on ClusterRole from the Bridge API +type ClusterRoleApiResource struct { AccountEmail string `json:"account_email"` AccountId string `json:"account_id"` ClusterId string `json:"cluster_id"` Flavor string `json:"flavor"` Name string `json:"name"` Password string `json:"password"` - TeamId string `json:"team_id"` + Team string `json:"team_id"` URI string `json:"uri"` } +// ClusterRoleList holds a slice of ClusterRoleApiResource type ClusterRoleList struct { - Roles []*ClusterRole `json:"roles"` + Roles []*ClusterRoleApiResource `json:"roles"` } +// BRIDGE CLIENT FUNCTIONS AND METHODS + // NewClient creates a Client with backoff settings that amount to // ~10 attempts over ~2 minutes. A default is used when apiURL is not // an acceptable URL. @@ -265,11 +417,7 @@ func (c *Client) CreateInstallation(ctx context.Context) (Installation, error) { // TODO(crunchybridgecluster) Is this where we want CRUD for clusters functions? Or make client `do` funcs // directly callable? -type ClusterList struct { - Clusters []*v1beta1.ClusterDetails `json:"clusters"` -} - -func (c *Client) ListClusters(ctx context.Context, apiKey, teamId string) ([]*v1beta1.ClusterDetails, error) { +func (c *Client) ListClusters(ctx context.Context, apiKey, teamId string) ([]*ClusterApiResource, error) { result := &ClusterList{} params := url.Values{} @@ -301,10 +449,12 @@ func (c *Client) ListClusters(ctx context.Context, apiKey, teamId string) ([]*v1 return result.Clusters, err } -func (c *Client) CreateCluster(ctx context.Context, apiKey string, cluster *v1beta1.ClusterDetails) (*v1beta1.ClusterDetails, error) { - result := &v1beta1.ClusterDetails{} +func (c *Client) CreateCluster( + ctx context.Context, apiKey string, clusterRequestPayload *PostClustersRequestPayload, +) (*ClusterApiResource, error) { + result := &ClusterApiResource{} - clusterbyte, err := json.Marshal(cluster) + clusterbyte, err := json.Marshal(clusterRequestPayload) if err != nil { return result, err } @@ -323,6 +473,10 @@ func (c *Client) CreateCluster(ctx context.Context, apiKey string, cluster *v1be case response.StatusCode >= 200 && response.StatusCode < 300: if err = json.Unmarshal(body, &result); err != nil { err = fmt.Errorf("%w: %s", err, body) + return result, err + } + if err = json.Unmarshal(body, &result.ResponsePayload); err != nil { + err = fmt.Errorf("%w: %s", err, body) } default: @@ -339,8 +493,8 @@ func (c *Client) CreateCluster(ctx context.Context, apiKey string, cluster *v1be // the cluster, // whether the cluster is deleted already, // and an error. -func (c *Client) DeleteCluster(ctx context.Context, apiKey, id string) (*v1beta1.ClusterDetails, bool, error) { - result := &v1beta1.ClusterDetails{} +func (c *Client) DeleteCluster(ctx context.Context, apiKey, id string) (*ClusterApiResource, bool, error) { + result := &ClusterApiResource{} var deletedAlready bool response, err := c.doWithRetry(ctx, "DELETE", "/clusters/"+id, nil, nil, http.Header{ @@ -379,8 +533,8 @@ func (c *Client) DeleteCluster(ctx context.Context, apiKey, id string) (*v1beta1 return result, deletedAlready, err } -func (c *Client) GetCluster(ctx context.Context, apiKey, id string) (*v1beta1.ClusterDetails, error) { - result := &v1beta1.ClusterDetails{} +func (c *Client) GetCluster(ctx context.Context, apiKey, id string) (*ClusterApiResource, error) { + result := &ClusterApiResource{} response, err := c.doWithRetry(ctx, "GET", "/clusters/"+id, nil, nil, http.Header{ "Accept": []string{"application/json"}, @@ -396,6 +550,10 @@ func (c *Client) GetCluster(ctx context.Context, apiKey, id string) (*v1beta1.Cl case response.StatusCode >= 200 && response.StatusCode < 300: if err = json.Unmarshal(body, &result); err != nil { err = fmt.Errorf("%w: %s", err, body) + return result, err + } + if err = json.Unmarshal(body, &result.ResponsePayload); err != nil { + err = fmt.Errorf("%w: %s", err, body) } default: @@ -407,9 +565,8 @@ func (c *Client) GetCluster(ctx context.Context, apiKey, id string) (*v1beta1.Cl return result, err } -// TODO (dsessler7) We should use a ClusterStatus struct here -func (c *Client) GetClusterStatus(ctx context.Context, apiKey, id string) (string, error) { - result := "" +func (c *Client) GetClusterStatus(ctx context.Context, apiKey, id string) (*ClusterStatusApiResource, error) { + result := &ClusterStatusApiResource{} response, err := c.doWithRetry(ctx, "GET", "/clusters/"+id+"/status", nil, nil, http.Header{ "Accept": []string{"application/json"}, @@ -425,6 +582,10 @@ func (c *Client) GetClusterStatus(ctx context.Context, apiKey, id string) (strin case response.StatusCode >= 200 && response.StatusCode < 300: if err = json.Unmarshal(body, &result); err != nil { err = fmt.Errorf("%w: %s", err, body) + return result, err + } + if err = json.Unmarshal(body, &result.ResponsePayload); err != nil { + err = fmt.Errorf("%w: %s", err, body) } default: @@ -436,8 +597,8 @@ func (c *Client) GetClusterStatus(ctx context.Context, apiKey, id string) (strin return result, err } -func (c *Client) GetClusterUpgrade(ctx context.Context, apiKey, id string) (*v1beta1.ClusterUpgrade, error) { - result := &v1beta1.ClusterUpgrade{} +func (c *Client) GetClusterUpgrade(ctx context.Context, apiKey, id string) (*ClusterUpgradeApiResource, error) { + result := &ClusterUpgradeApiResource{} response, err := c.doWithRetry(ctx, "GET", "/clusters/"+id+"/upgrade", nil, nil, http.Header{ "Accept": []string{"application/json"}, @@ -453,6 +614,10 @@ func (c *Client) GetClusterUpgrade(ctx context.Context, apiKey, id string) (*v1b case response.StatusCode >= 200 && response.StatusCode < 300: if err = json.Unmarshal(body, &result); err != nil { err = fmt.Errorf("%w: %s", err, body) + return result, err + } + if err = json.Unmarshal(body, &result.ResponsePayload); err != nil { + err = fmt.Errorf("%w: %s", err, body) } default: @@ -464,10 +629,12 @@ func (c *Client) GetClusterUpgrade(ctx context.Context, apiKey, id string) (*v1b return result, err } -func (c *Client) UpgradeCluster(ctx context.Context, apiKey, id string, cluster *v1beta1.ClusterDetails) (*v1beta1.ClusterUpgrade, error) { - result := &v1beta1.ClusterUpgrade{} +func (c *Client) UpgradeCluster( + ctx context.Context, apiKey, id string, clusterRequestPayload *PostClustersUpgradeRequestPayload, +) (*ClusterUpgradeApiResource, error) { + result := &ClusterUpgradeApiResource{} - clusterbyte, err := json.Marshal(cluster) + clusterbyte, err := json.Marshal(clusterRequestPayload) if err != nil { return result, err } @@ -486,6 +653,10 @@ func (c *Client) UpgradeCluster(ctx context.Context, apiKey, id string, cluster case response.StatusCode >= 200 && response.StatusCode < 300: if err = json.Unmarshal(body, &result); err != nil { err = fmt.Errorf("%w: %s", err, body) + return result, err + } + if err = json.Unmarshal(body, &result.ResponsePayload); err != nil { + err = fmt.Errorf("%w: %s", err, body) } default: @@ -497,8 +668,8 @@ func (c *Client) UpgradeCluster(ctx context.Context, apiKey, id string, cluster return result, err } -func (c *Client) UpgradeClusterHA(ctx context.Context, apiKey, id, action string) (*v1beta1.ClusterUpgrade, error) { - result := &v1beta1.ClusterUpgrade{} +func (c *Client) UpgradeClusterHA(ctx context.Context, apiKey, id, action string) (*ClusterUpgradeApiResource, error) { + result := &ClusterUpgradeApiResource{} response, err := c.doWithRetry(ctx, "PUT", "/clusters/"+id+"/actions/"+action, nil, nil, http.Header{ "Accept": []string{"application/json"}, @@ -514,6 +685,10 @@ func (c *Client) UpgradeClusterHA(ctx context.Context, apiKey, id, action string case response.StatusCode >= 200 && response.StatusCode < 300: if err = json.Unmarshal(body, &result); err != nil { err = fmt.Errorf("%w: %s", err, body) + return result, err + } + if err = json.Unmarshal(body, &result.ResponsePayload); err != nil { + err = fmt.Errorf("%w: %s", err, body) } default: @@ -525,8 +700,8 @@ func (c *Client) UpgradeClusterHA(ctx context.Context, apiKey, id, action string return result, err } -func (c *Client) GetClusterRole(ctx context.Context, apiKey, clusterId, roleName string) (*ClusterRole, error) { - result := &ClusterRole{} +func (c *Client) GetClusterRole(ctx context.Context, apiKey, clusterId, roleName string) (*ClusterRoleApiResource, error) { + result := &ClusterRoleApiResource{} response, err := c.doWithRetry(ctx, "GET", "/clusters/"+clusterId+"/roles/"+roleName, nil, nil, http.Header{ "Accept": []string{"application/json"}, @@ -553,7 +728,7 @@ func (c *Client) GetClusterRole(ctx context.Context, apiKey, clusterId, roleName return result, err } -func (c *Client) ListClusterRoles(ctx context.Context, apiKey, id string) ([]*ClusterRole, error) { +func (c *Client) ListClusterRoles(ctx context.Context, apiKey, id string) ([]*ClusterRoleApiResource, error) { result := ClusterRoleList{} response, err := c.doWithRetry(ctx, "GET", "/clusters/"+id+"/roles", nil, nil, http.Header{ diff --git a/internal/bridge/crunchybridgecluster/crunchybridgecluster_controller.go b/internal/bridge/crunchybridgecluster/crunchybridgecluster_controller.go index 9bab67d30c..10729e4ebd 100644 --- a/internal/bridge/crunchybridgecluster/crunchybridgecluster_controller.go +++ b/internal/bridge/crunchybridgecluster/crunchybridgecluster_controller.go @@ -346,7 +346,7 @@ func (r *CrunchyBridgeClusterReconciler) Reconcile(ctx context.Context, req ctrl } for _, cluster := range clusters { - if crunchybridgecluster.Spec.ClusterName == cluster.Name { + if crunchybridgecluster.Spec.ClusterName == cluster.ClusterName { // Cluster with the same name exists so check for adoption annotation adoptionID, annotationExists := crunchybridgecluster.Annotations[naming.CrunchyBridgeClusterAdoptionAnnotation] if annotationExists && strings.EqualFold(adoptionID, cluster.ID) { @@ -387,7 +387,7 @@ func (r *CrunchyBridgeClusterReconciler) Reconcile(ctx context.Context, req ctrl // TODO(crunchybridgecluster) Can almost just use the crunchybridgecluster.Spec... except for the team, // which we don't want users to set on the spec. Do we? - clusterReq := &v1beta1.ClusterDetails{ + createClusterRequestPayload := &bridge.PostClustersRequestPayload{ IsHA: crunchybridgecluster.Spec.IsHA, Name: crunchybridgecluster.Spec.ClusterName, Plan: crunchybridgecluster.Spec.Plan, @@ -397,7 +397,7 @@ func (r *CrunchyBridgeClusterReconciler) Reconcile(ctx context.Context, req ctrl Storage: storageVal, Team: team, } - cluster, err := r.NewClient().CreateCluster(ctx, key, clusterReq) + cluster, err := r.NewClient().CreateCluster(ctx, key, createClusterRequestPayload) if err != nil { log.Error(err, "whoops, cluster creating issue") // TODO(crunchybridgecluster): probably shouldn't set this condition unless response from Bridge @@ -420,40 +420,35 @@ func (r *CrunchyBridgeClusterReconciler) Reconcile(ctx context.Context, req ctrl return ctrl.Result{RequeueAfter: 3 * time.Minute}, nil } - // If we reach this point, our CrunchyBridgeCluster object has an ID - // so we want to fill in the details for the cluster and cluster upgrades from the Bridge API - // Consider cluster details as a separate func. + // If we reach this point, our CrunchyBridgeCluster object has an ID, so we want + // to fill in the details for the cluster, cluster status, and cluster upgrades + // from the Bridge API. + // Get Cluster clusterDetails, err := r.NewClient().GetCluster(ctx, key, crunchybridgecluster.Status.ID) if err != nil { - log.Error(err, "whoops, cluster getting issue") + log.Error(err, "whoops, issue getting cluster") return ctrl.Result{}, err } + clusterDetails.AddDataToClusterStatus(crunchybridgecluster) - clusterStatus := &v1beta1.ClusterStatus{ - ID: clusterDetails.ID, - IsHA: clusterDetails.IsHA, - Name: clusterDetails.Name, - Plan: clusterDetails.Plan, - MajorVersion: clusterDetails.MajorVersion, - PostgresVersion: clusterDetails.PostgresVersion, - Provider: clusterDetails.Provider, - Region: clusterDetails.Region, - Storage: clusterDetails.Storage, - Team: clusterDetails.Team, - State: clusterDetails.State, + // Get Cluster Status + clusterStatus, err := r.NewClient().GetClusterStatus(ctx, key, crunchybridgecluster.Status.ID) + if err != nil { + log.Error(err, "whoops, issue getting cluster status") + return ctrl.Result{}, err } + clusterStatus.AddDataToClusterStatus(crunchybridgecluster) - crunchybridgecluster.Status.Cluster = clusterStatus - + // Get Cluster Upgrade clusterUpgradeDetails, err := r.NewClient().GetClusterUpgrade(ctx, key, crunchybridgecluster.Status.ID) if err != nil { - log.Error(err, "whoops, cluster upgrade getting issue") + log.Error(err, "whoops, issue getting cluster upgrade") return ctrl.Result{}, err } - crunchybridgecluster.Status.ClusterUpgrade = clusterUpgradeDetails + clusterUpgradeDetails.AddDataToClusterStatus(crunchybridgecluster) - // reconcile roles and their secrets + // Reconcile roles and their secrets err = r.reconcilePostgresRoles(ctx, key, crunchybridgecluster) // For now, we skip updating until the upgrade status is cleared. @@ -465,23 +460,23 @@ func (r *CrunchyBridgeClusterReconciler) Reconcile(ctx context.Context, req ctrl // then we will requeue and wait for it to be done. // TODO(crunchybridgecluster): Do we want the operator to interrupt // upgrades created through the GUI/API? - if len(crunchybridgecluster.Status.ClusterUpgrade.Operations) != 0 { + if len(crunchybridgecluster.Status.OngoingUpgrade) != 0 { return ctrl.Result{RequeueAfter: 3 * time.Minute}, nil } // Check if there's an upgrade difference for the three upgradeable fields that hit the upgrade endpoint // Why PostgresVersion and MajorVersion? Because MajorVersion in the Status is sure to be - // an int of the major version, whereas Status.Cluster.PostgresVersion might be the ID - if (storageVal != crunchybridgecluster.Status.Cluster.Storage) || - crunchybridgecluster.Spec.Plan != crunchybridgecluster.Status.Cluster.Plan || - crunchybridgecluster.Spec.PostgresVersion != crunchybridgecluster.Status.Cluster.MajorVersion { + // an int of the major version, whereas Status.Responses.Cluster.PostgresVersion might be the ID + if (storageVal != crunchybridgecluster.Status.Storage) || + crunchybridgecluster.Spec.Plan != crunchybridgecluster.Status.Plan || + crunchybridgecluster.Spec.PostgresVersion != crunchybridgecluster.Status.MajorVersion { return r.handleUpgrade(ctx, key, crunchybridgecluster, storageVal) } // Are there diffs between the cluster response from the Bridge API and the spec? // HA diffs are sent to /clusters/{cluster_id}/actions/[enable|disable]-ha // so have to know (a) to send and (b) which to send to - if crunchybridgecluster.Spec.IsHA != crunchybridgecluster.Status.Cluster.IsHA { + if crunchybridgecluster.Spec.IsHA != crunchybridgecluster.Status.IsHA { return r.handleUpgradeHA(ctx, key, crunchybridgecluster) } @@ -518,7 +513,7 @@ func (r *CrunchyBridgeClusterReconciler) handleUpgrade(ctx context.Context, log.Info("Handling upgrade request") - upgradeRequest := &v1beta1.ClusterDetails{ + upgradeRequest := &bridge.PostClustersUpgradeRequestPayload{ Plan: crunchybridgecluster.Spec.Plan, PostgresVersion: intstr.FromInt(crunchybridgecluster.Spec.PostgresVersion), Storage: storageVal, @@ -533,7 +528,8 @@ func (r *CrunchyBridgeClusterReconciler) handleUpgrade(ctx context.Context, log.Error(err, "Error while attempting cluster upgrade") return ctrl.Result{}, nil } - crunchybridgecluster.Status.ClusterUpgrade = clusterUpgrade + clusterUpgrade.AddDataToClusterStatus(crunchybridgecluster) + return ctrl.Result{RequeueAfter: 3 * time.Minute}, nil } @@ -560,7 +556,8 @@ func (r *CrunchyBridgeClusterReconciler) handleUpgradeHA(ctx context.Context, log.Error(err, "Error while attempting cluster HA change") return ctrl.Result{}, nil } - crunchybridgecluster.Status.ClusterUpgrade = clusterUpgrade + clusterUpgrade.AddDataToClusterStatus(crunchybridgecluster) + return ctrl.Result{RequeueAfter: 3 * time.Minute}, nil } diff --git a/internal/bridge/crunchybridgecluster/postgres.go b/internal/bridge/crunchybridgecluster/postgres.go index 90a64b0e2e..8e8638e0a8 100644 --- a/internal/bridge/crunchybridgecluster/postgres.go +++ b/internal/bridge/crunchybridgecluster/postgres.go @@ -34,7 +34,7 @@ import ( // connection details for the appropriate database. func (r *CrunchyBridgeClusterReconciler) generatePostgresRoleSecret( cluster *v1beta1.CrunchyBridgeCluster, roleSpec *v1beta1.CrunchyBridgeClusterRoleSpec, - clusterRole *bridge.ClusterRole, + clusterRole *bridge.ClusterRoleApiResource, ) (*corev1.Secret, error) { roleName := roleSpec.Name secretName := roleSpec.SecretName @@ -69,23 +69,17 @@ func (r *CrunchyBridgeClusterReconciler) reconcilePostgresRoles( ctx context.Context, apiKey string, cluster *v1beta1.CrunchyBridgeCluster, ) error { _, _, err := r.reconcilePostgresRoleSecrets(ctx, apiKey, cluster) - // if err == nil { - // err = r.reconcilePostgresUsersInPostgreSQL(ctx, cluster, instances, users, secrets) - // } - // if err == nil { - // // Copy PostgreSQL users and passwords into pgAdmin. This is here because - // // reconcilePostgresRoleSecrets is building a (default) PostgresUserSpec - // // that is not in the PostgresClusterSpec. The freshly generated Secrets - // // are available here, too. - // err = r.reconcilePGAdminUsers(ctx, cluster, users, secrets) - // } + + // TODO: If we ever add a PgAdmin feature to CrunchyBridgeCluster, we will + // want to add the role credentials to PgAdmin here + return err } func (r *CrunchyBridgeClusterReconciler) reconcilePostgresRoleSecrets( ctx context.Context, apiKey string, cluster *v1beta1.CrunchyBridgeCluster, ) ( - []v1beta1.CrunchyBridgeClusterRoleSpec, map[string]*corev1.Secret, error, + []*v1beta1.CrunchyBridgeClusterRoleSpec, map[string]*corev1.Secret, error, ) { log := ctrl.LoggerFrom(ctx) specRoles := cluster.Spec.Roles @@ -93,7 +87,7 @@ func (r *CrunchyBridgeClusterReconciler) reconcilePostgresRoleSecrets( // Index role specifications by PostgreSQL role name. roleSpecs := make(map[string]*v1beta1.CrunchyBridgeClusterRoleSpec, len(specRoles)) for i := range specRoles { - roleSpecs[string(specRoles[i].Name)] = &specRoles[i] + roleSpecs[specRoles[i].Name] = specRoles[i] } // Gather existing role secrets diff --git a/pkg/apis/postgres-operator.crunchydata.com/v1beta1/crunchy_bridgecluster_types.go b/pkg/apis/postgres-operator.crunchydata.com/v1beta1/crunchy_bridgecluster_types.go index fdc9c26d6e..0e4e5941f2 100644 --- a/pkg/apis/postgres-operator.crunchydata.com/v1beta1/crunchy_bridgecluster_types.go +++ b/pkg/apis/postgres-operator.crunchydata.com/v1beta1/crunchy_bridgecluster_types.go @@ -18,7 +18,6 @@ package v1beta1 import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" ) // CrunchyBridgeClusterSpec defines the desired state of CrunchyBridgeCluster @@ -73,7 +72,7 @@ type CrunchyBridgeClusterSpec struct { // +listType=map // +listMapKey=name // +optional - Roles []CrunchyBridgeClusterRoleSpec `json:"roles,omitempty"` + Roles []*CrunchyBridgeClusterRoleSpec `json:"roles,omitempty"` // The name of the secret containing the API key and team id // +kubebuilder:validation:Required @@ -89,84 +88,91 @@ type CrunchyBridgeClusterSpec struct { } type CrunchyBridgeClusterRoleSpec struct { - // The name of this PostgreSQL role. The value may contain only lowercase - // letters, numbers, and hyphen so that it fits into Kubernetes metadata. - // The above is problematic for us as Bridge has a role with an underscore. - // TODO: figure out underscore dilemma - // +kubebuilder:validation:Pattern=`^[A-Za-z][A-Za-z0-9\-_ ]*[A-Za-z0-9]$` - // +kubebuilder:validation:Type=string + // Name of the role within Crunchy Bridge. + // More info: https://docs.crunchybridge.com/concepts/users Name string `json:"name"` // The name of the Secret that will hold the role credentials. - // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$` + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$` // +kubebuilder:validation:Type=string SecretName string `json:"secretName"` } // CrunchyBridgeClusterStatus defines the observed state of CrunchyBridgeCluster type CrunchyBridgeClusterStatus struct { - // The ID of the postgrescluster in Bridge, provided by Bridge API and null until then. + // The name of the cluster in Bridge. + // +optional + ClusterName string `json:"name,omitempty"` + + // conditions represent the observations of postgres cluster's current state. + // +optional + // +listType=map + // +listMapKey=type + // +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors={"urn:alm:descriptor:io.kubernetes.conditions"} + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // The Hostname of the postgres cluster in Bridge, provided by Bridge API and null until then. + // +optional + Host string `json:"host,omitempty"` + + // The ID of the postgres cluster in Bridge, provided by Bridge API and null until then. // +optional ID string `json:"id,omitempty"` + // Whether the cluster is high availability, meaning that it has a secondary it can fail + // over to quickly in case the primary becomes unavailable. + // +optional + IsHA bool `json:"isHa"` + + // Whether the cluster is protected. Protected clusters can't be destroyed until + // their protected flag is removed + // +optional + IsProtected bool `json:"isProtected,omitempty"` + + // The cluster's major Postgres version. + // +optional + MajorVersion int `json:"majorVersion"` + // observedGeneration represents the .metadata.generation on which the status was based. // +optional // +kubebuilder:validation:Minimum=0 ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // conditions represent the observations of postgrescluster's current state. + // The cluster upgrade as represented by Bridge // +optional - // +listType=map - // +listMapKey=type - // +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors={"urn:alm:descriptor:io.kubernetes.conditions"} - Conditions []metav1.Condition `json:"conditions,omitempty"` + OngoingUpgrade []*UpgradeOperation `json:"ongoingUpgrade,omitempty"` - // The cluster as represented by Bridge + // The ID of the cluster's plan. Determines instance, CPU, and memory. // +optional - Cluster *ClusterStatus `json:"clusterStatus,omitempty"` + Plan string `json:"planId"` - // The cluster upgrade as represented by Bridge + // Most recent, raw responses from Bridge API // +optional - ClusterUpgrade *ClusterUpgrade `json:"clusterUpgradeResponse,omitempty"` -} + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:validation:Schemaless + // +kubebuilder:validation:Type=object + Responses APIResponses `json:"responses"` -// Right now used for cluster create requests and cluster get responses -type ClusterDetails struct { - ID string `json:"id,omitempty"` - IsHA bool `json:"is_ha,omitempty"` - Name string `json:"name,omitempty"` - Plan string `json:"plan_id,omitempty"` - MajorVersion int `json:"major_version,omitempty"` - PostgresVersion intstr.IntOrString `json:"postgres_version_id,omitempty"` - Provider string `json:"provider_id,omitempty"` - Region string `json:"region_id,omitempty"` - Storage int64 `json:"storage,omitempty"` - Team string `json:"team_id,omitempty"` - State string `json:"state,omitempty"` - // TODO(crunchybridgecluster): add other fields, DiskUsage, Host, IsProtected, IsSuspended, CPU, Memory, etc. + // State of cluster in Bridge. + // +optional + State string `json:"state,omitempty"` + + // The amount of storage available to the cluster in gigabytes. + // +optional + Storage int64 `json:"storage"` } -// Used to make the cluster status look kubey -type ClusterStatus struct { - ID string `json:"id,omitempty"` - IsHA bool `json:"isHa,omitempty"` - Name string `json:"name,omitempty"` - Plan string `json:"planId,omitempty"` - MajorVersion int `json:"majorVersion,omitempty"` - PostgresVersion intstr.IntOrString `json:"postgresVersionId,omitempty"` - Provider string `json:"providerId,omitempty"` - Region string `json:"regionId,omitempty"` - Storage int64 `json:"storage,omitempty"` - Team string `json:"teamId,omitempty"` - State string `json:"state,omitempty"` - // TODO(crunchybridgecluster): add other fields, DiskUsage, Host, IsProtected, IsSuspended, CPU, Memory, etc. +type APIResponses struct { + Cluster SchemalessObject `json:"cluster,omitempty"` + Status SchemalessObject `json:"status,omitempty"` + Upgrade SchemalessObject `json:"upgrade,omitempty"` } type ClusterUpgrade struct { - Operations []*Operation `json:"operations,omitempty"` + Operations []*UpgradeOperation `json:"operations,omitempty"` } -type Operation struct { +type UpgradeOperation struct { Flavor string `json:"flavor"` StartingFrom string `json:"starting_from"` State string `json:"state"` diff --git a/pkg/apis/postgres-operator.crunchydata.com/v1beta1/zz_generated.deepcopy.go b/pkg/apis/postgres-operator.crunchydata.com/v1beta1/zz_generated.deepcopy.go index 35746fc6b0..d98e7ea378 100644 --- a/pkg/apis/postgres-operator.crunchydata.com/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/postgres-operator.crunchydata.com/v1beta1/zz_generated.deepcopy.go @@ -27,6 +27,24 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIResponses) DeepCopyInto(out *APIResponses) { + *out = *in + in.Cluster.DeepCopyInto(&out.Cluster) + in.Status.DeepCopyInto(&out.Status) + in.Upgrade.DeepCopyInto(&out.Upgrade) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIResponses. +func (in *APIResponses) DeepCopy() *APIResponses { + if in == nil { + return nil + } + out := new(APIResponses) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackupJobs) DeepCopyInto(out *BackupJobs) { *out = *in @@ -81,48 +99,16 @@ func (in *Backups) DeepCopy() *Backups { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ClusterDetails) DeepCopyInto(out *ClusterDetails) { - *out = *in - out.PostgresVersion = in.PostgresVersion -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterDetails. -func (in *ClusterDetails) DeepCopy() *ClusterDetails { - if in == nil { - return nil - } - out := new(ClusterDetails) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ClusterStatus) DeepCopyInto(out *ClusterStatus) { - *out = *in - out.PostgresVersion = in.PostgresVersion -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterStatus. -func (in *ClusterStatus) DeepCopy() *ClusterStatus { - if in == nil { - return nil - } - out := new(ClusterStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterUpgrade) DeepCopyInto(out *ClusterUpgrade) { *out = *in if in.Operations != nil { in, out := &in.Operations, &out.Operations - *out = make([]*Operation, len(*in)) + *out = make([]*UpgradeOperation, len(*in)) for i := range *in { if (*in)[i] != nil { in, out := &(*in)[i], &(*out)[i] - *out = new(Operation) + *out = new(UpgradeOperation) **out = **in } } @@ -223,8 +209,14 @@ func (in *CrunchyBridgeClusterSpec) DeepCopyInto(out *CrunchyBridgeClusterSpec) } if in.Roles != nil { in, out := &in.Roles, &out.Roles - *out = make([]CrunchyBridgeClusterRoleSpec, len(*in)) - copy(*out, *in) + *out = make([]*CrunchyBridgeClusterRoleSpec, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(CrunchyBridgeClusterRoleSpec) + **out = **in + } + } } out.Storage = in.Storage.DeepCopy() } @@ -249,16 +241,18 @@ func (in *CrunchyBridgeClusterStatus) DeepCopyInto(out *CrunchyBridgeClusterStat (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.Cluster != nil { - in, out := &in.Cluster, &out.Cluster - *out = new(ClusterStatus) - **out = **in - } - if in.ClusterUpgrade != nil { - in, out := &in.ClusterUpgrade, &out.ClusterUpgrade - *out = new(ClusterUpgrade) - (*in).DeepCopyInto(*out) + if in.OngoingUpgrade != nil { + in, out := &in.OngoingUpgrade, &out.OngoingUpgrade + *out = make([]*UpgradeOperation, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(UpgradeOperation) + **out = **in + } + } } + in.Responses.DeepCopyInto(&out.Responses) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CrunchyBridgeClusterStatus. @@ -473,21 +467,6 @@ func (in *MonitoringStatus) DeepCopy() *MonitoringStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Operation) DeepCopyInto(out *Operation) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Operation. -func (in *Operation) DeepCopy() *Operation { - if in == nil { - return nil - } - out := new(Operation) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PGAdmin) DeepCopyInto(out *PGAdmin) { *out = *in @@ -2232,6 +2211,21 @@ func (in *TablespaceVolume) DeepCopy() *TablespaceVolume { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UpgradeOperation) DeepCopyInto(out *UpgradeOperation) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpgradeOperation. +func (in *UpgradeOperation) DeepCopy() *UpgradeOperation { + if in == nil { + return nil + } + out := new(UpgradeOperation) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UserInterfaceSpec) DeepCopyInto(out *UserInterfaceSpec) { *out = *in