From 82d1c0e406e241dd93e3a93c25fda92c9179e65d Mon Sep 17 00:00:00 2001 From: Leonardo Cecchi Date: Mon, 29 Jan 2024 14:14:49 +0100 Subject: [PATCH] feat: implement CNPG-I plugin infrastructure (#3719) Introduce foundational changes to support dynamic plugins in adherence to the CloudNativePG Interface (CNPG-I). This commit establishes the necessary infrastructure for seamless plugin management. Partially closes: #3699 Signed-off-by: Leonardo Cecchi Signed-off-by: Armando Ruocco Co-authored-by: Armando Ruocco --- .wordlist-en-custom.txt | 6 + api/v1/cluster_plugins.go | 39 +++ api/v1/cluster_types.go | 39 +++ api/v1/cluster_webhook.go | 61 +++++ api/v1/zz_generated.deepcopy.go | 82 ++++++ .../bases/postgresql.cnpg.io_clusters.yaml | 48 ++++ controllers/cluster_controller.go | 5 + controllers/cluster_create.go | 17 ++ controllers/cluster_plugins.go | 56 ++++ docs/src/cloudnative-pg.v1.md | 63 +++++ .../cluster-example-bis-restore-cr.yaml | 26 ++ .../samples/cluster-example-bis-restore.yaml | 43 +++ docs/src/samples/cluster-example-bis.yaml | 29 ++ go.mod | 8 +- go.sum | 30 ++- internal/cnpi/plugin/client/cluster.go | 204 ++++++++++++++ internal/cnpi/plugin/client/connection.go | 251 ++++++++++++++++++ internal/cnpi/plugin/client/contracts.go | 90 +++++++ internal/cnpi/plugin/client/doc.go | 19 ++ internal/cnpi/plugin/client/pod.go | 106 ++++++++ internal/configuration/configuration.go | 8 + 21 files changed, 1223 insertions(+), 7 deletions(-) create mode 100644 api/v1/cluster_plugins.go create mode 100644 controllers/cluster_plugins.go create mode 100644 docs/src/samples/cluster-example-bis-restore-cr.yaml create mode 100644 docs/src/samples/cluster-example-bis-restore.yaml create mode 100644 docs/src/samples/cluster-example-bis.yaml create mode 100644 internal/cnpi/plugin/client/cluster.go create mode 100644 internal/cnpi/plugin/client/connection.go create mode 100644 internal/cnpi/plugin/client/contracts.go create mode 100644 internal/cnpi/plugin/client/doc.go create mode 100644 internal/cnpi/plugin/client/pod.go diff --git a/.wordlist-en-custom.txt b/.wordlist-en-custom.txt index ced7ed7542..c47435d198 100644 --- a/.wordlist-en-custom.txt +++ b/.wordlist-en-custom.txt @@ -246,6 +246,7 @@ OnlineUpgrading OpenSSL OpenShift Openshift +OperatorCapabilities OperatorGroup OperatorHub PGAudit @@ -272,6 +273,8 @@ PgBouncerSecrets PgBouncerSecretsVersions PgBouncerSpec Philippe +PluginConfigurationList +PluginStatus PoLA PodAffinity PodAntiAffinity @@ -893,6 +896,7 @@ openldap openshift operability operativity +operatorCapabilities operatorframework operatorgorup operatorgroup @@ -931,6 +935,7 @@ pid pitr plpgsql pluggable +pluginStatus png podAffinityTerm podAntiAffinity @@ -990,6 +995,7 @@ readService readinessProbe readthedocs readyInstances +reconciler reconciliationLoop recoverability recoveredCluster diff --git a/api/v1/cluster_plugins.go b/api/v1/cluster_plugins.go new file mode 100644 index 0000000000..2b6c726b59 --- /dev/null +++ b/api/v1/cluster_plugins.go @@ -0,0 +1,39 @@ +/* +Copyright The CloudNativePG Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + + "github.com/cloudnative-pg/cloudnative-pg/internal/cnpi/plugin/client" + "github.com/cloudnative-pg/cloudnative-pg/internal/configuration" +) + +// NewLoader creates a new plugin client, loading the plugins that are required +// by this cluster +func (plugins PluginConfigurationList) NewLoader(ctx context.Context) (client.Client, error) { + pluginLoader := client.NewUnixSocketClient(configuration.Current.PluginSocketDir) + + // Load the plugins + for _, pluginDeclaration := range plugins { + if err := pluginLoader.Load(ctx, pluginDeclaration.Name); err != nil { + return nil, err + } + } + + return pluginLoader, nil +} diff --git a/api/v1/cluster_types.go b/api/v1/cluster_types.go index 20b6de00e2..99571895f1 100644 --- a/api/v1/cluster_types.go +++ b/api/v1/cluster_types.go @@ -489,8 +489,16 @@ type ClusterSpec struct { // The tablespaces configuration // +optional Tablespaces []TablespaceConfiguration `json:"tablespaces,omitempty"` + + // The plugins configuration, containing + // any plugin to be loaded with the corresponding configuration + Plugins PluginConfigurationList `json:"plugins,omitempty"` } +// PluginConfigurationList represent a set of plugin with their +// configuration parameters +type PluginConfigurationList []PluginConfiguration + const ( // PhaseSwitchover when a cluster is changing the primary node PhaseSwitchover = "Switchover in progress" @@ -896,6 +904,9 @@ type ClusterStatus struct { // Image contains the image name used by the pods // +optional Image string `json:"image,omitempty"` + + // PluginStatus is the status of the loaded plugins + PluginStatus []PluginStatus `json:"pluginStatus,omitempty"` } // InstanceReportedState describes the last reported state of an instance during a reconciliation loop @@ -2343,6 +2354,34 @@ type ManagedConfiguration struct { Roles []RoleConfiguration `json:"roles,omitempty"` } +// PluginConfiguration specifies a plugin that need to be loaded for this +// cluster to be reconciled +type PluginConfiguration struct { + // Name is the plugin name + Name string `json:"name"` + + // Parameters is the configuration of the plugin + Parameters map[string]string `json:"parameters,omitempty"` +} + +// PluginStatus is the status of a loaded plugin +type PluginStatus struct { + // Name is the name of the plugin + Name string `json:"name"` + + // Version is the version of the plugin loaded by the + // latest reconciliation loop + Version string `json:"version"` + + // Capabilities are the list of capabilities of the + // plugin + Capabilities []string `json:"capabilities,omitempty"` + + // OperatorCapabilities are the list of capabilities of the + // plugin regarding the reconciler + OperatorCapabilities []string `json:"operatorCapabilities,omitempty"` +} + // RoleConfiguration is the representation, in Kubernetes, of a PostgreSQL role // with the additional field Ensure specifying whether to ensure the presence or // absence of the role in the database diff --git a/api/v1/cluster_webhook.go b/api/v1/cluster_webhook.go index 26e2352668..7aefe1c3ea 100644 --- a/api/v1/cluster_webhook.go +++ b/api/v1/cluster_webhook.go @@ -17,6 +17,7 @@ limitations under the License. package v1 import ( + "context" "encoding/json" "fmt" "strconv" @@ -162,6 +163,27 @@ func (r *Cluster) setDefaults(preserveUserSettings bool) { if len(r.Spec.Tablespaces) > 0 { r.defaultTablespaces() } + + ctx := context.Background() + + // Call the plugins to help with defaulting this cluster + contextLogger := log.FromContext(ctx) + pluginClient, err := r.Spec.Plugins.NewLoader(ctx) + if err != nil { + contextLogger.Error(err, "Error invoking plugin in the defaulting webhook, skipping") + return + } + defer func() { + pluginClient.Close(ctx) + }() + + var mutatedCluster Cluster + if err := pluginClient.MutateCluster(ctx, r, &mutatedCluster); err != nil { + contextLogger.Error(err, "Error invoking plugin in the defaulting webhook, skipping") + return + } + + mutatedCluster.DeepCopyInto(r) } // defaultTablespaces adds the tablespace owner where the @@ -286,6 +308,26 @@ var _ webhook.Validator = &Cluster{} func (r *Cluster) ValidateCreate() (admission.Warnings, error) { clusterLog.Info("validate create", "name", r.Name, "namespace", r.Namespace) allErrs := r.Validate() + + // Call the plugins to help validating this cluster creation + ctx := context.Background() + contextLogger := log.FromContext(ctx) + pluginClient, err := r.Spec.Plugins.NewLoader(ctx) + if err != nil { + contextLogger.Error(err, "Error invoking plugin in the validate/create webhook") + return nil, err + } + defer func() { + pluginClient.Close(ctx) + }() + + pluginValidationResult, err := pluginClient.ValidateClusterCreate(ctx, r) + if err != nil { + contextLogger.Error(err, "Error invoking plugin in the validate/update webhook") + return nil, err + } + allErrs = append(allErrs, pluginValidationResult...) + if len(allErrs) == 0 { return nil, nil } @@ -356,6 +398,25 @@ func (r *Cluster) ValidateUpdate(old runtime.Object) (admission.Warnings, error) r.ValidateChanges(oldCluster)..., ) + // Call the plugins to help validating this cluster update + ctx := context.Background() + contextLogger := log.FromContext(ctx) + pluginClient, err := r.Spec.Plugins.NewLoader(ctx) + if err != nil { + contextLogger.Error(err, "Error invoking plugin in the validate/create webhook") + return nil, err + } + defer func() { + pluginClient.Close(ctx) + }() + + pluginValidationResult, err := pluginClient.ValidateClusterUpdate(ctx, oldCluster, r) + if err != nil { + contextLogger.Error(err, "Error invoking plugin in the validate/update webhook") + return nil, err + } + allErrs = append(allErrs, pluginValidationResult...) + if len(allErrs) == 0 { return nil, nil } diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 3b197d5af4..bcbae22f0e 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -871,6 +871,13 @@ func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Plugins != nil { + in, out := &in.Plugins, &out.Plugins + *out = make(PluginConfigurationList, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterSpec. @@ -980,6 +987,13 @@ func (in *ClusterStatus) DeepCopyInto(out *ClusterStatus) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.PluginStatus != nil { + in, out := &in.PluginStatus, &out.PluginStatus + *out = make([]PluginStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterStatus. @@ -1754,6 +1768,74 @@ func (in *PgBouncerSpec) DeepCopy() *PgBouncerSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PluginConfiguration) DeepCopyInto(out *PluginConfiguration) { + *out = *in + if in.Parameters != nil { + in, out := &in.Parameters, &out.Parameters + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginConfiguration. +func (in *PluginConfiguration) DeepCopy() *PluginConfiguration { + if in == nil { + return nil + } + out := new(PluginConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in PluginConfigurationList) DeepCopyInto(out *PluginConfigurationList) { + { + in := &in + *out = make(PluginConfigurationList, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginConfigurationList. +func (in PluginConfigurationList) DeepCopy() PluginConfigurationList { + if in == nil { + return nil + } + out := new(PluginConfigurationList) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PluginStatus) DeepCopyInto(out *PluginStatus) { + *out = *in + if in.Capabilities != nil { + in, out := &in.Capabilities, &out.Capabilities + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.OperatorCapabilities != nil { + in, out := &in.OperatorCapabilities, &out.OperatorCapabilities + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginStatus. +func (in *PluginStatus) DeepCopy() *PluginStatus { + if in == nil { + return nil + } + out := new(PluginStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PodTemplateSpec) DeepCopyInto(out *PodTemplateSpec) { *out = *in diff --git a/config/crd/bases/postgresql.cnpg.io_clusters.yaml b/config/crd/bases/postgresql.cnpg.io_clusters.yaml index 2ae6fc75ca..633daab84f 100644 --- a/config/crd/bases/postgresql.cnpg.io_clusters.yaml +++ b/config/crd/bases/postgresql.cnpg.io_clusters.yaml @@ -3210,6 +3210,25 @@ spec: up again) or not (recreate it elsewhere - when `instances` >1) type: boolean type: object + plugins: + description: The plugins configuration, containing any plugin to be + loaded with the corresponding configuration + items: + description: PluginConfiguration specifies a plugin that need to + be loaded for this cluster to be reconciled + properties: + name: + description: Name is the plugin name + type: string + parameters: + additionalProperties: + type: string + description: Parameters is the configuration of the plugin + type: object + required: + - name + type: object + type: array postgresGID: default: 26 description: The GID of the `postgres` user inside the image, defaults @@ -5141,6 +5160,35 @@ spec: phaseReason: description: Reason for the current phase type: string + pluginStatus: + description: PluginStatus is the status of the loaded plugins + items: + description: PluginStatus is the status of a loaded plugin + properties: + capabilities: + description: Capabilities are the list of capabilities of the + plugin + items: + type: string + type: array + name: + description: Name is the name of the plugin + type: string + operatorCapabilities: + description: OperatorCapabilities are the list of capabilities + of the plugin regarding the reconciler + items: + type: string + type: array + version: + description: Version is the version of the plugin loaded by + the latest reconciliation loop + type: string + required: + - name + - version + type: object + type: array poolerIntegrations: description: The integration needed by poolers referencing the cluster properties: diff --git a/controllers/cluster_controller.go b/controllers/cluster_controller.go index b6a083bcc7..8f78046006 100644 --- a/controllers/cluster_controller.go +++ b/controllers/cluster_controller.go @@ -195,6 +195,11 @@ func (r *ClusterReconciler) reconcile(ctx context.Context, cluster *apiv1.Cluste return ctrl.Result{}, fmt.Errorf("cannot set image name: %w", err) } + // Ensure we load all the plugins that are required to reconcile this cluster + if err := r.preReconcilePlugins(ctx, cluster); err != nil { + return ctrl.Result{}, fmt.Errorf("cannot reconciled required plugins: %w", err) + } + // Ensure we reconcile the orphan resources if present when we reconcile for the first time a cluster if err := r.reconcileRestoredCluster(ctx, cluster); err != nil { return ctrl.Result{}, fmt.Errorf("cannot reconcile restored Cluster: %w", err) diff --git a/controllers/cluster_create.go b/controllers/cluster_create.go index 8d5bf666eb..e532d16646 100644 --- a/controllers/cluster_create.go +++ b/controllers/cluster_create.go @@ -1271,6 +1271,23 @@ func (r *ClusterReconciler) ensureInstancesAreCreated( utils.InheritLabels(&instanceToCreate.ObjectMeta, cluster.Labels, cluster.GetFixedInheritedLabels(), configuration.Current) + // Call the plugins to enrich this Pod definition + pluginClient, err := cluster.Spec.Plugins.NewLoader(ctx) + if err != nil { + contextLogger.Error(err, "Error invoking plugin meanwhile creating Pods") + return ctrl.Result{}, err + } + defer func() { + pluginClient.Close(ctx) + }() + + var mutatedPod corev1.Pod + if err := pluginClient.MutatePod(ctx, cluster, instanceToCreate, &mutatedPod); err != nil { + contextLogger.Error(err, "Error invoking plugin in the defaulting webhook, skipping") + return ctrl.Result{}, err + } + mutatedPod.DeepCopyInto(instanceToCreate) + if err := r.Create(ctx, instanceToCreate); err != nil { if apierrs.IsAlreadyExists(err) { // This Pod was already created, maybe the cache is stale. diff --git a/controllers/cluster_plugins.go b/controllers/cluster_plugins.go new file mode 100644 index 0000000000..82db27ea38 --- /dev/null +++ b/controllers/cluster_plugins.go @@ -0,0 +1,56 @@ +/* +Copyright The CloudNativePG Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package controllers contains the controller of the CRD +package controllers + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/client" + + apiv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1" + "github.com/cloudnative-pg/cloudnative-pg/pkg/management/log" +) + +// reconcilePlugins ensures that we load the plugins that are required to reconcile +// this cluster +func (r *ClusterReconciler) preReconcilePlugins(ctx context.Context, cluster *apiv1.Cluster) error { + contextLogger := log.FromContext(ctx) + + // Load the plugins + pluginClient, err := cluster.Spec.Plugins.NewLoader(ctx) + if err != nil { + contextLogger.Error(err, "Error loading plugins, retrying") + return err + } + defer func() { + pluginClient.Close(ctx) + }() + + // Get the status of the plugins and store it inside the status section + oldCluster := cluster.DeepCopy() + metadataList := pluginClient.MetadataList() + cluster.Status.PluginStatus = make([]apiv1.PluginStatus, len(metadataList)) + for i, entry := range metadataList { + cluster.Status.PluginStatus[i].Name = entry.Name + cluster.Status.PluginStatus[i].Version = entry.Version + cluster.Status.PluginStatus[i].Capabilities = entry.Capabilities + cluster.Status.PluginStatus[i].OperatorCapabilities = entry.OperatorCapabilities + } + + return r.Client.Status().Patch(ctx, cluster, client.MergeFrom(oldCluster)) +} diff --git a/docs/src/cloudnative-pg.v1.md b/docs/src/cloudnative-pg.v1.md index b5478083e5..fa1fcb9ad5 100644 --- a/docs/src/cloudnative-pg.v1.md +++ b/docs/src/cloudnative-pg.v1.md @@ -1846,6 +1846,14 @@ Defaults to: RuntimeDefault

The tablespaces configuration

+plugins [Required]
+PluginConfigurationList + + +

The plugins configuration, containing +any plugin to be loaded with the corresponding configuration

+ + @@ -2168,6 +2176,13 @@ This field is reported when .spec.failoverDelay is populated or dur

Image contains the image name used by the pods

+pluginStatus [Required]
+[]PluginStatus + + +

PluginStatus is the status of the loaded plugins

+ + @@ -3406,6 +3421,54 @@ the operator calls PgBouncer's PAUSE and RESUME comman +## PluginStatus {#postgresql-cnpg-io-v1-PluginStatus} + + +**Appears in:** + +- [ClusterStatus](#postgresql-cnpg-io-v1-ClusterStatus) + + +

PluginStatus is the status of a loaded plugin

+ + + + + + + + + + + + + + + + + + +
FieldDescription
name [Required]
+string +
+

Name is the name of the plugin

+
version [Required]
+string +
+

Version is the version of the plugin loaded by the +latest reconciliation loop

+
capabilities [Required]
+[]string +
+

Capabilities are the list of capabilities of the +plugin

+
operatorCapabilities [Required]
+[]string +
+

OperatorCapabilities are the list of capabilities of the +plugin regarding the reconciler

+
+ ## PodTemplateSpec {#postgresql-cnpg-io-v1-PodTemplateSpec} diff --git a/docs/src/samples/cluster-example-bis-restore-cr.yaml b/docs/src/samples/cluster-example-bis-restore-cr.yaml new file mode 100644 index 0000000000..4958cadc06 --- /dev/null +++ b/docs/src/samples/cluster-example-bis-restore-cr.yaml @@ -0,0 +1,26 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: cluster-restore +spec: + instances: 3 + + storage: + size: 1Gi + storageClass: csi-hostpath-sc + walStorage: + size: 1Gi + storageClass: csi-hostpath-sc + + bootstrap: + recovery: + volumeSnapshots: + storage: + name: cluster-example-20231031161103 + kind: VolumeSnapshot + apiGroup: snapshot.storage.k8s.io + walStorage: + name: cluster-example-20231031161103-wal + kind: VolumeSnapshot + apiGroup: snapshot.storage.k8s.io + diff --git a/docs/src/samples/cluster-example-bis-restore.yaml b/docs/src/samples/cluster-example-bis-restore.yaml new file mode 100644 index 0000000000..7f814a89fe --- /dev/null +++ b/docs/src/samples/cluster-example-bis-restore.yaml @@ -0,0 +1,43 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: cluster-restore +spec: + instances: 3 + imageName: registry.dev:5000/postgresql:16 + + storage: + size: 1Gi + storageClass: csi-hostpath-sc + walStorage: + size: 1Gi + storageClass: csi-hostpath-sc + + # Backup properties + # This assumes a local minio setup +# backup: +# barmanObjectStore: +# destinationPath: s3://backups/ +# endpointURL: http://minio:9000 +# s3Credentials: +# accessKeyId: +# name: minio +# key: ACCESS_KEY_ID +# secretAccessKey: +# name: minio +# key: ACCESS_SECRET_KEY +# wal: +# compression: gzip + + bootstrap: + recovery: + volumeSnapshots: + storage: + name: snapshot-0bc6095db42768c7a1fe897494a966f541ef5fb29b2eb8e9399d80bd0a32408a-2023-11-13-7.41.53 + kind: VolumeSnapshot + apiGroup: snapshot.storage.k8s.io + walStorage: + name: snapshot-a67084ba08097fd8c3e34c6afef8110091da67e5895f0379fd2df5b9f73ff524-2023-11-13-7.41.53 + kind: VolumeSnapshot + apiGroup: snapshot.storage.k8s.io + diff --git a/docs/src/samples/cluster-example-bis.yaml b/docs/src/samples/cluster-example-bis.yaml new file mode 100644 index 0000000000..a99b205f1b --- /dev/null +++ b/docs/src/samples/cluster-example-bis.yaml @@ -0,0 +1,29 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: cluster-example +spec: + instances: 3 + imageName: registry.dev:5000/postgresql:16 + + backup: + volumeSnapshot: + className: csi-hostpath-groupsnapclass + #className: csi-hostpath-snapclass + groupSnapshot: true + + storage: + storageClass: csi-hostpath-sc + size: 1Gi + walStorage: + storageClass: csi-hostpath-sc + size: 1Gi + # tablespaces: + # first: + # storage: + # storageClass: csi-hostpath-sc + # size: 1Gi + # second: + # storage: + # storageClass: csi-hostpath-sc + # size: 1Gi diff --git a/go.mod b/go.mod index 784cb360f8..2599e03d94 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,13 @@ require ( github.com/avast/retry-go/v4 v4.5.1 github.com/blang/semver v3.5.1+incompatible github.com/cheynewallace/tabby v1.1.1 + github.com/cloudnative-pg/cnpg-i v0.0.0-20240122164555-5215ff219c8f github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc + github.com/evanphx/json-patch/v5 v5.8.0 github.com/go-logr/logr v1.4.1 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.6.0 + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.1 github.com/jackc/pgx/v5 v5.5.4 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kubernetes-csi/external-snapshotter/client/v7 v7.0.0 @@ -31,6 +34,7 @@ require ( go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 golang.org/x/sys v0.18.0 + google.golang.org/grpc v1.60.1 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.29.2 k8s.io/apiextensions-apiserver v0.29.2 @@ -49,7 +53,6 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect - github.com/evanphx/json-patch/v5 v5.8.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect @@ -100,7 +103,8 @@ require ( golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.18.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/appengine v1.6.7 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect google.golang.org/protobuf v1.32.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 2915f952db..17a55f2194 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudnative-pg/cnpg-i v0.0.0-20240122164555-5215ff219c8f h1:ypwPq45y8ezzwxUTHL0VkzkT2+pcHnE4yRoeGTP8fp8= +github.com/cloudnative-pg/cnpg-i v0.0.0-20240122164555-5215ff219c8f/go.mod h1:0G5GXQVj09KvONIcYURyroL74zOFGjv4eI5OXz7/G/0= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -62,7 +64,6 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -71,6 +72,7 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= @@ -101,6 +103,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.1 h1:HcUWd006luQPljE73d5sk+/VgYPGUReEVz2y1/qylwY= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.1/go.mod h1:w9Y7gY31krpLmrVU5ZPG9H7l9fZuRu5/3R3S3FMtVQ4= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= @@ -211,6 +215,7 @@ github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -224,6 +229,7 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -234,15 +240,17 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -253,6 +261,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -260,17 +269,23 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= @@ -283,6 +298,7 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -293,14 +309,18 @@ gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 h1:gphdwh0npgs8elJ4T6J+DQJHPVF7RsuJHCfwztUb4J4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= +google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/internal/cnpi/plugin/client/cluster.go b/internal/cnpi/plugin/client/cluster.go new file mode 100644 index 0000000000..0894f21f19 --- /dev/null +++ b/internal/cnpi/plugin/client/cluster.go @@ -0,0 +1,204 @@ +/* +Copyright The CloudNativePG Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package client + +import ( + "context" + "encoding/json" + "fmt" + "slices" + + "github.com/cloudnative-pg/cnpg-i/pkg/operator" + jsonpatch "github.com/evanphx/json-patch/v5" + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/cloudnative-pg/cloudnative-pg/pkg/management/log" +) + +func (data *data) MutateCluster(ctx context.Context, object client.Object, mutatedObject client.Object) error { + contextLogger := log.FromContext(ctx) + + serializedObject, err := json.Marshal(object) + if err != nil { + return fmt.Errorf("while serializing %s %s/%s to JSON: %w", + object.GetObjectKind().GroupVersionKind().Kind, + object.GetNamespace(), object.GetName(), + err, + ) + } + + for idx := range data.plugins { + plugin := &data.plugins[idx] + + if !slices.Contains(plugin.operatorCapabilities, operator.OperatorCapability_RPC_TYPE_MUTATE_CLUSTER) { + continue + } + + contextLogger := contextLogger.WithValues( + "pluginName", plugin.name, + ) + request := operator.OperatorMutateClusterRequest{ + Definition: serializedObject, + } + + contextLogger.Trace("Calling MutateCluster endpoint", "definition", request.Definition) + result, err := plugin.operatorClient.MutateCluster(ctx, &request) + if err != nil { + contextLogger.Error(err, "Error while calling MutateCluster") + return err + } + + if len(result.JsonPatch) == 0 { + // There's nothing to mutate + continue + } + + mutatedObject, err := jsonpatch.MergePatch(serializedObject, result.JsonPatch) + if err != nil { + contextLogger.Error(err, "Error while applying JSON patch from plugin", "patch", result.JsonPatch) + return err + } + + serializedObject = mutatedObject + } + + if err := json.Unmarshal(serializedObject, mutatedObject); err != nil { + return fmt.Errorf("while deserializing %s %s/%s to JSON: %w", + object.GetObjectKind().GroupVersionKind().Kind, + object.GetNamespace(), object.GetName(), + err, + ) + } + + return nil +} + +func (data *data) ValidateClusterCreate( + ctx context.Context, + object client.Object, +) (field.ErrorList, error) { + contextLogger := log.FromContext(ctx) + + serializedObject, err := json.Marshal(object) + if err != nil { + return nil, fmt.Errorf("while serializing %s %s/%s to JSON: %w", + object.GetObjectKind().GroupVersionKind().Kind, + object.GetNamespace(), object.GetName(), + err, + ) + } + + var validationErrors []*operator.ValidationError + for idx := range data.plugins { + plugin := &data.plugins[idx] + + if !slices.Contains(plugin.operatorCapabilities, operator.OperatorCapability_RPC_TYPE_VALIDATE_CLUSTER_CREATE) { + continue + } + + contextLogger := contextLogger.WithValues( + "pluginName", plugin.name, + ) + request := operator.OperatorValidateClusterCreateRequest{ + Definition: serializedObject, + } + + contextLogger.Trace("Calling ValidatedClusterCreate endpoint", "definition", request.Definition) + result, err := plugin.operatorClient.ValidateClusterCreate(ctx, &request) + if err != nil { + contextLogger.Error(err, "Error while calling ValidatedClusterCreate") + return nil, err + } + + validationErrors = append(validationErrors, result.ValidationErrors...) + } + + return validationErrorsToErrorList(validationErrors), nil +} + +func (data *data) ValidateClusterUpdate( + ctx context.Context, + oldObject client.Object, + newObject client.Object, +) (field.ErrorList, error) { + contextLogger := log.FromContext(ctx) + + serializedOldObject, err := json.Marshal(oldObject) + if err != nil { + return nil, fmt.Errorf("while serializing %s %s/%s to JSON: %w", + oldObject.GetObjectKind().GroupVersionKind().Kind, + oldObject.GetNamespace(), oldObject.GetName(), + err, + ) + } + + serializedNewObject, err := json.Marshal(newObject) + if err != nil { + return nil, fmt.Errorf("while serializing %s %s/%s to JSON: %w", + newObject.GetObjectKind().GroupVersionKind().Kind, + newObject.GetNamespace(), newObject.GetName(), + err, + ) + } + + var validationErrors []*operator.ValidationError + for idx := range data.plugins { + plugin := &data.plugins[idx] + + if !slices.Contains(plugin.operatorCapabilities, operator.OperatorCapability_RPC_TYPE_VALIDATE_CLUSTER_CHANGE) { + continue + } + + contextLogger := contextLogger.WithValues( + "pluginName", plugin.name, + ) + request := operator.OperatorValidateClusterChangeRequest{ + OldCluster: serializedOldObject, + NewCluster: serializedNewObject, + } + + contextLogger.Trace( + "Calling ValidateClusterChange endpoint", + "oldCluster", request.OldCluster, + "newCluster", request.NewCluster) + result, err := plugin.operatorClient.ValidateClusterChange(ctx, &request) + if err != nil { + contextLogger.Error(err, "Error while calling ValidatedClusterCreate") + return nil, err + } + + validationErrors = append(validationErrors, result.ValidationErrors...) + } + + return validationErrorsToErrorList(validationErrors), nil +} + +// validationErrorsToErrorList makes up a list of validation errors as required by +// the Kubernetes API from the GRPC plugin interface types +func validationErrorsToErrorList(validationErrors []*operator.ValidationError) (result field.ErrorList) { + result = make(field.ErrorList, len(validationErrors)) + for i, validationError := range validationErrors { + result[i] = field.Invalid( + field.NewPath(validationError.PathComponents[0], validationError.PathComponents[1:]...), + validationError.Value, + validationError.Message, + ) + } + + return result +} diff --git a/internal/cnpi/plugin/client/connection.go b/internal/cnpi/plugin/client/connection.go new file mode 100644 index 0000000000..bd79e1950a --- /dev/null +++ b/internal/cnpi/plugin/client/connection.go @@ -0,0 +1,251 @@ +/* +Copyright The CloudNativePG Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package client + +import ( + "context" + "fmt" + "io" + "path" + "slices" + "time" + + "github.com/cloudnative-pg/cnpg-i/pkg/identity" + "github.com/cloudnative-pg/cnpg-i/pkg/operator" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/timeout" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/cloudnative-pg/cloudnative-pg/pkg/management/log" +) + +// defaultTimeout is the timeout applied by default to every GRPC call +const defaultTimeout = 30 * time.Second + +type protocol interface { + dial(ctx context.Context, path string) (connectionHandler, error) +} + +type connectionHandler interface { + grpc.ClientConnInterface + io.Closer +} + +type protocolUnix string + +func (p protocolUnix) dial(ctx context.Context, path string) (connectionHandler, error) { + contextLogger := log.FromContext(ctx) + dialPath := fmt.Sprintf("unix://%s", path) + + contextLogger.Debug("Connecting to plugin", "path", dialPath) + + return grpc.Dial( + dialPath, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithUnaryInterceptor( + timeout.UnaryClientInterceptor(defaultTimeout), + ), + ) +} + +// data represent a new CNPI client collection +type data struct { + pluginPath string + protocol protocol + plugins []pluginData +} + +type pluginData struct { + connection connectionHandler + identityClient identity.IdentityClient + operatorClient operator.OperatorClient + + name string + version string + capabilities []identity.PluginCapability_Service_Type + operatorCapabilities []operator.OperatorCapability_RPC_Type +} + +// NewUnixSocketClient creates a new CNPI client discovering plugins +// registered in a specific path +func NewUnixSocketClient(pluginPath string) Client { + return &data{ + pluginPath: pluginPath, + protocol: protocolUnix(""), + } +} + +func (data *data) Load(ctx context.Context, name string) error { + pluginData, err := data.loadPlugin(ctx, name) + if err != nil { + return err + } + + data.plugins = append(data.plugins, pluginData) + return nil +} + +func (data *data) MetadataList() []Metadata { + result := make([]Metadata, len(data.plugins)) + for i := range data.plugins { + result[i] = data.plugins[i].Metadata() + } + + return result +} + +func (data *data) loadPlugin(ctx context.Context, name string) (pluginData, error) { + var connection connectionHandler + var err error + + defer func() { + if err != nil && connection != nil { + _ = connection.Close() + } + }() + + contextLogger := log.FromContext(ctx).WithValues("pluginName", name) + ctx = log.IntoContext(ctx, contextLogger) + + if connection, err = data.protocol.dial( + ctx, + path.Join(data.pluginPath, name), + ); err != nil { + contextLogger.Error(err, "Error while connecting to plugin") + return pluginData{}, err + } + + var result pluginData + result, err = newPluginDataFromConnection(ctx, connection) + if err != nil { + return pluginData{}, err + } + + // Load the list of services implemented by the plugin + if err = result.loadPluginCapabilities(ctx); err != nil { + return pluginData{}, err + } + + // If the plugin implements the Operator service, load its + // capabilities + if slices.Contains(result.capabilities, identity.PluginCapability_Service_TYPE_OPERATOR_SERVICE) { + if err = result.loadOperatorCapabilities(ctx); err != nil { + return pluginData{}, err + } + } + + return result, nil +} + +func (data *data) Close(ctx context.Context) { + contextLogger := log.FromContext(ctx) + for i := range data.plugins { + plugin := &data.plugins[i] + contextLogger := contextLogger.WithValues("pluginName", plugin.name) + + if err := plugin.connection.Close(); err != nil { + contextLogger.Error(err, "while closing plugin connection") + } + } + + data.plugins = nil +} + +func newPluginDataFromConnection(ctx context.Context, connection connectionHandler) (pluginData, error) { + var err error + + identityClient := identity.NewIdentityClient(connection) + + var pluginInfoResponse *identity.GetPluginMetadataResponse + + if pluginInfoResponse, err = identityClient.GetPluginMetadata( + ctx, + &identity.GetPluginMetadataRequest{}, + ); err != nil { + return pluginData{}, fmt.Errorf("while querying plugin identity: %w", err) + } + + result := pluginData{} + result.connection = connection + result.name = pluginInfoResponse.Name + result.version = pluginInfoResponse.Version + result.identityClient = identity.NewIdentityClient(connection) + result.operatorClient = operator.NewOperatorClient(connection) + + return result, err +} + +func (pluginData *pluginData) loadPluginCapabilities(ctx context.Context) error { + var pluginCapabilitiesResponse *identity.GetPluginCapabilitiesResponse + var err error + + if pluginCapabilitiesResponse, err = pluginData.identityClient.GetPluginCapabilities( + ctx, + &identity.GetPluginCapabilitiesRequest{}, + ); err != nil { + return fmt.Errorf("while querying plugin capabilities: %w", err) + } + + pluginData.capabilities = make([]identity.PluginCapability_Service_Type, len(pluginCapabilitiesResponse.Capabilities)) + for i := range pluginData.capabilities { + pluginData.capabilities[i] = pluginCapabilitiesResponse.Capabilities[i].GetService().Type + } + + return nil +} + +func (pluginData *pluginData) loadOperatorCapabilities(ctx context.Context) error { + var operatorCapabilitiesResponse *operator.OperatorCapabilitiesResult + var err error + + if operatorCapabilitiesResponse, err = pluginData.operatorClient.GetCapabilities( + ctx, + &operator.OperatorCapabilitiesRequest{}, + ); err != nil { + return fmt.Errorf("while querying plugin operator capabilities: %w", err) + } + + pluginData.operatorCapabilities = make( + []operator.OperatorCapability_RPC_Type, + len(operatorCapabilitiesResponse.Capabilities)) + for i := range pluginData.operatorCapabilities { + pluginData.operatorCapabilities[i] = operatorCapabilitiesResponse.Capabilities[i].GetRpc().Type + } + + return nil +} + +// Metadata extracts the plugin metadata reading from +// the internal metadata +func (pluginData *pluginData) Metadata() Metadata { + result := Metadata{ + Name: pluginData.name, + Version: pluginData.version, + Capabilities: make([]string, len(pluginData.capabilities)), + OperatorCapabilities: make([]string, len(pluginData.operatorCapabilities)), + } + + for i := range pluginData.capabilities { + result.Capabilities[i] = pluginData.capabilities[i].String() + } + + for i := range pluginData.operatorCapabilities { + result.OperatorCapabilities[i] = pluginData.operatorCapabilities[i].String() + } + + return result +} diff --git a/internal/cnpi/plugin/client/contracts.go b/internal/cnpi/plugin/client/contracts.go new file mode 100644 index 0000000000..dafde761a5 --- /dev/null +++ b/internal/cnpi/plugin/client/contracts.go @@ -0,0 +1,90 @@ +/* +Copyright The CloudNativePG Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package client + +import ( + "context" + + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Metadata expose the metadata as discovered +// from a plugin +type Metadata struct { + Name string + Version string + Capabilities []string + OperatorCapabilities []string +} + +// Client describes a set of behaviour needed to properly handle all the plugin client expected features +type Client interface { + Connection + ClusterCapabilities + PodCapabilities +} + +// Connection describes a set of behaviour needed to properly handle the plugin connections +type Connection interface { + // Load connect to the plugin with the specified name + Load(ctx context.Context, name string) error + + // Close closes the connection to every loaded plugin + Close(ctx context.Context) + + // MetadataList exposes the metadata of the loaded plugins + MetadataList() []Metadata +} + +// PodCapabilities describes a set of behaviour needed to implement the Pod capabilities +type PodCapabilities interface { + // MutatePod calls the loaded plugins to help to enhance + // a PostgreSQL instance Pod definition + MutatePod( + ctx context.Context, + cluster client.Object, + object client.Object, + mutatedObject client.Object, + ) error +} + +// ClusterCapabilities describes a set of behaviour needed to implement the Cluster capabilities +type ClusterCapabilities interface { + // MutateCluster calls the loaded plugisn to help to enhance + // a cluster definition + MutateCluster( + ctx context.Context, + object client.Object, + mutatedObject client.Object, + ) error + + // ValidateClusterCreate calls all the loaded plugin to check if a cluster definition + // is correct + ValidateClusterCreate( + ctx context.Context, + object client.Object, + ) (field.ErrorList, error) + + // ValidateClusterUpdate calls all the loaded plugin to check if a cluster can + // be changed from a value to another + ValidateClusterUpdate( + ctx context.Context, + oldObject client.Object, + newObject client.Object, + ) (field.ErrorList, error) +} diff --git a/internal/cnpi/plugin/client/doc.go b/internal/cnpi/plugin/client/doc.go new file mode 100644 index 0000000000..1cb0e5ee6d --- /dev/null +++ b/internal/cnpi/plugin/client/doc.go @@ -0,0 +1,19 @@ +/* +Copyright The CloudNativePG Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package client contains a set of helper structures for CNPG to use the +// plugins exposing the CNPI interface +package client diff --git a/internal/cnpi/plugin/client/pod.go b/internal/cnpi/plugin/client/pod.go new file mode 100644 index 0000000000..f9a6fe4ee6 --- /dev/null +++ b/internal/cnpi/plugin/client/pod.go @@ -0,0 +1,106 @@ +/* +Copyright The CloudNativePG Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package client + +import ( + "context" + "encoding/json" + "fmt" + "slices" + + "github.com/cloudnative-pg/cnpg-i/pkg/operator" + jsonpatch "github.com/evanphx/json-patch/v5" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/cloudnative-pg/cloudnative-pg/pkg/management/log" +) + +func (data *data) MutatePod( + ctx context.Context, + cluster client.Object, + object client.Object, + mutatedObject client.Object, +) error { + contextLogger := log.FromContext(ctx) + + serializedCluster, err := json.Marshal(cluster) + if err != nil { + return fmt.Errorf("while serializing %s %s/%s to JSON: %w", + cluster.GetObjectKind().GroupVersionKind().Kind, + cluster.GetNamespace(), cluster.GetName(), + err, + ) + } + + serializedObject, err := json.Marshal(object) + if err != nil { + return fmt.Errorf("while serializing %s %s/%s to JSON: %w", + object.GetObjectKind().GroupVersionKind().Kind, + object.GetNamespace(), object.GetName(), + err, + ) + } + + for idx := range data.plugins { + plugin := &data.plugins[idx] + + if !slices.Contains(plugin.operatorCapabilities, operator.OperatorCapability_RPC_TYPE_MUTATE_POD) { + continue + } + + contextLogger := contextLogger.WithValues( + "pluginName", plugin.name, + ) + request := operator.OperatorMutatePodRequest{ + ClusterDefinition: serializedCluster, + PodDefinition: serializedObject, + } + + contextLogger.Trace( + "Calling MutatePod endpoint", + "clusterDefinition", request.ClusterDefinition, + "podDefinition", request.PodDefinition) + result, err := plugin.operatorClient.MutatePod(ctx, &request) + if err != nil { + contextLogger.Error(err, "Error while calling MutatePod") + return err + } + + if len(result.JsonPatch) == 0 { + // There's nothing to mutate + continue + } + + mutatedObject, err := jsonpatch.MergePatch(serializedObject, result.JsonPatch) + if err != nil { + contextLogger.Error(err, "Error while applying JSON patch from plugin", "patch", result.JsonPatch) + return err + } + + serializedObject = mutatedObject + } + + if err := json.Unmarshal(serializedObject, mutatedObject); err != nil { + return fmt.Errorf("while deserializing %s %s/%s to JSON: %w", + object.GetObjectKind().GroupVersionKind().Kind, + object.GetNamespace(), object.GetName(), + err, + ) + } + + return nil +} diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index 1bcdfb64d7..44cab9a14a 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -32,6 +32,9 @@ var configurationLog = log.WithName("configuration") // DefaultOperatorPullSecretName is implicitly copied into newly created clusters. const DefaultOperatorPullSecretName = "cnpg-pull-secret" // #nosec +// DefaultPluginSocketDir is the default directory where the plugin sockets are located. +const DefaultPluginSocketDir = "/plugins" + // Data is the struct containing the configuration of the operator. // Usually the operator code will use the "Current" configuration. type Data struct { @@ -39,6 +42,10 @@ type Data struct { // need to written. This is different between plain Kubernetes and OpenShift WebhookCertDir string `json:"webhookCertDir" env:"WEBHOOK_CERT_DIR"` + // PluginSocketDir is the directory where the plugins sockets are to be + // found + PluginSocketDir string `json:"pluginSocketDir" env:"PLUGIN_SOCKET_DIR"` + // WatchNamespace is the namespace where the operator should watch and // is configurable via environment variables in the OpenShift console. // Multiple namespaces can be specified separated by comma @@ -99,6 +106,7 @@ func newDefaultConfig() *Data { OperatorPullSecretName: DefaultOperatorPullSecretName, OperatorImageName: versions.DefaultOperatorImageName, PostgresImageName: versions.DefaultImageName, + PluginSocketDir: DefaultPluginSocketDir, CreateAnyService: false, } }