From fa3bdc9623f8147771c9df049a38022f12ce84bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Fern=C3=A1ndez=20Campo?= Date: Sat, 26 Oct 2024 09:48:41 +0200 Subject: [PATCH 01/17] WIP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joaquín Fernández Campo --- README.md | 2 +- .../v1alpha1/default_grant_types.go | 98 +++ apis/postgresql/v1alpha1/register.go | 8 + .../v1alpha1/zz_generated.deepcopy.go | 157 +++++ .../v1alpha1/zz_generated.managed.go | 60 ++ .../v1alpha1/zz_generated.managedlist.go | 9 + ...resql.sql.crossplane.io_defaultgrants.yaml | 532 +++++++++++++++ .../postgresql/default_grant/reconciler.go | 385 +++++++++++ .../default_grant/reconciler_test.go | 627 ++++++++++++++++++ 9 files changed, 1877 insertions(+), 1 deletion(-) create mode 100644 apis/postgresql/v1alpha1/default_grant_types.go create mode 100644 package/crds/postgresql.sql.crossplane.io_defaultgrants.yaml create mode 100644 pkg/controller/postgresql/default_grant/reconciler.go create mode 100644 pkg/controller/postgresql/default_grant/reconciler_test.go diff --git a/README.md b/README.md index 1c67f598..5cbee906 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Check the example: 2. Create managed resources for your SQL server flavor: - **MySQL**: `Database`, `Grant`, `User` (See [the examples](examples/mysql)) - - **PostgreSQL**: `Database`, `Grant`, `Extension`, `Role` (See [the examples](examples/postgresql)) + - **PostgreSQL**: `Database`, `Grant`,`DefaultGrant`, `Extension`, `Role` (See [the examples](examples/postgresql)) - **MSSQL**: `Database`, `Grant`, `User` (See [the examples](examples/mssql)) [crossplane]: https://crossplane.io diff --git a/apis/postgresql/v1alpha1/default_grant_types.go b/apis/postgresql/v1alpha1/default_grant_types.go new file mode 100644 index 00000000..0570df32 --- /dev/null +++ b/apis/postgresql/v1alpha1/default_grant_types.go @@ -0,0 +1,98 @@ +package v1alpha1 + +import ( + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true + +// A Grant represents the declarative state of a PostgreSQL grant. +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" +// +kubebuilder:printcolumn:name="SYNCED",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:printcolumn:name="ROLE",type="string",JSONPath=".spec.forProvider.role" +// +kubebuilder:printcolumn:name="MEMBER OF",type="string",JSONPath=".spec.forProvider.memberOf" +// +kubebuilder:printcolumn:name="DATABASE",type="string",JSONPath=".spec.forProvider.database" +// +kubebuilder:printcolumn:name="PRIVILEGES",type="string",JSONPath=".spec.forProvider.privileges" +// +kubebuilder:resource:scope=Cluster,categories={crossplane,managed,sql} +type DefaultGrant struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DefaultGrantSpec `json:"spec"` + Status DefaultGrantStatus `json:"status,omitempty"` +} + +// A DefaultGrantSpec defines the desired state of a Default Grant. +type DefaultGrantSpec struct { + xpv1.ResourceSpec `json:",inline"` + ForProvider DefaultGrantParameters `json:"forProvider"` +} + +// A DefaultGrantStatus represents the observed state of a Grant. +type DefaultGrantStatus struct { + xpv1.ResourceStatus `json:",inline"` +} + +// DefaultGrantParameters defines the desired state of a Default Grant. +type DefaultGrantParameters struct { + // Privileges to be granted. + // See https://www.postgresql.org/docs/current/sql-grant.html for available privileges. + // +optional + Privileges GrantPrivileges `json:"privileges,omitempty"` + + // WithOption allows an option to be set on the grant. + // See https://www.postgresql.org/docs/current/sql-grant.html for available + // options for each grant type, and the effects of applying the option. + // +kubebuilder:validation:Enum=ADMIN;GRANT + // +optional + WithOption *GrantOption `json:"withOption,omitempty"` + + // Role to which default privileges are granted + // +optional + Role *string `json:"role,omitempty"` + + // RoleRef to which default privileges are granted. + // +immutable + // +optional + RoleRef *xpv1.Reference `json:"roleRef,omitempty"` + + // Database in which the default privileges are applied + // +optional + Database *string `json:"database,omitempty"` + + // DatabaseRef references the database object this default grant it for. + // +immutable + // +optional + DatabaseRef *xpv1.Reference `json:"databaseRef,omitempty"` + + // DatabaseSelector selects a reference to a Database this grant is for. + // +immutable + // +optional + DatabaseSelector *xpv1.Selector `json:"databaseSelector,omitempty"` + + // Schema in which the default privileges are applied + // +optional + Schema *string `json:"schema,omitempty"` + + // SchemaRef references the database object this default grant it for. + // +immutable + // +optional + SchemaRef *xpv1.Reference `json:"schemaRef,omitempty"` + + // SchemaSelector selects a reference to a Database this grant is for. + // +immutable + // +optional + SchemaSelector *xpv1.Selector `json:"schemaSelector,omitempty"` +} + +// +kubebuilder:object:root=true + +// DefaultGrantList contains a list of DefaultGrant. +type DefaultGrantList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DefaultGrant `json:"items"` +} diff --git a/apis/postgresql/v1alpha1/register.go b/apis/postgresql/v1alpha1/register.go index 536b8882..bae29c8d 100644 --- a/apis/postgresql/v1alpha1/register.go +++ b/apis/postgresql/v1alpha1/register.go @@ -90,6 +90,14 @@ var ( GrantGroupVersionKind = SchemeGroupVersion.WithKind(GrantKind) ) +// DefaultGrant type metadata. +var ( + DefaultGrantKind = reflect.TypeOf(DefaultGrant{}).Name() + DefaultGrantGroupKind = schema.GroupKind{Group: Group, Kind: DefaultGrantKind}.String() + DefaultGrantKindAPIVersion = DefaultGrantKind + "." + SchemeGroupVersion.String() + DefaultGrantGroupVersionKind = SchemeGroupVersion.WithKind(DefaultGrantKind) +) + // Schema type metadata. var ( SchemaKind = reflect.TypeOf(Schema{}).Name() diff --git a/apis/postgresql/v1alpha1/zz_generated.deepcopy.go b/apis/postgresql/v1alpha1/zz_generated.deepcopy.go index 09ae90b7..904ef655 100644 --- a/apis/postgresql/v1alpha1/zz_generated.deepcopy.go +++ b/apis/postgresql/v1alpha1/zz_generated.deepcopy.go @@ -177,6 +177,163 @@ func (in *DatabaseStatus) DeepCopy() *DatabaseStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DefaultGrant) DeepCopyInto(out *DefaultGrant) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultGrant. +func (in *DefaultGrant) DeepCopy() *DefaultGrant { + if in == nil { + return nil + } + out := new(DefaultGrant) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DefaultGrant) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DefaultGrantList) DeepCopyInto(out *DefaultGrantList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DefaultGrant, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultGrantList. +func (in *DefaultGrantList) DeepCopy() *DefaultGrantList { + if in == nil { + return nil + } + out := new(DefaultGrantList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DefaultGrantList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DefaultGrantParameters) DeepCopyInto(out *DefaultGrantParameters) { + *out = *in + if in.Privileges != nil { + in, out := &in.Privileges, &out.Privileges + *out = make(GrantPrivileges, len(*in)) + copy(*out, *in) + } + if in.WithOption != nil { + in, out := &in.WithOption, &out.WithOption + *out = new(GrantOption) + **out = **in + } + if in.Role != nil { + in, out := &in.Role, &out.Role + *out = new(string) + **out = **in + } + if in.RoleRef != nil { + in, out := &in.RoleRef, &out.RoleRef + *out = new(v1.Reference) + (*in).DeepCopyInto(*out) + } + if in.Database != nil { + in, out := &in.Database, &out.Database + *out = new(string) + **out = **in + } + if in.DatabaseRef != nil { + in, out := &in.DatabaseRef, &out.DatabaseRef + *out = new(v1.Reference) + (*in).DeepCopyInto(*out) + } + if in.DatabaseSelector != nil { + in, out := &in.DatabaseSelector, &out.DatabaseSelector + *out = new(v1.Selector) + (*in).DeepCopyInto(*out) + } + if in.Schema != nil { + in, out := &in.Schema, &out.Schema + *out = new(string) + **out = **in + } + if in.SchemaRef != nil { + in, out := &in.SchemaRef, &out.SchemaRef + *out = new(v1.Reference) + (*in).DeepCopyInto(*out) + } + if in.SchemaSelector != nil { + in, out := &in.SchemaSelector, &out.SchemaSelector + *out = new(v1.Selector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultGrantParameters. +func (in *DefaultGrantParameters) DeepCopy() *DefaultGrantParameters { + if in == nil { + return nil + } + out := new(DefaultGrantParameters) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DefaultGrantSpec) DeepCopyInto(out *DefaultGrantSpec) { + *out = *in + in.ResourceSpec.DeepCopyInto(&out.ResourceSpec) + in.ForProvider.DeepCopyInto(&out.ForProvider) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultGrantSpec. +func (in *DefaultGrantSpec) DeepCopy() *DefaultGrantSpec { + if in == nil { + return nil + } + out := new(DefaultGrantSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DefaultGrantStatus) DeepCopyInto(out *DefaultGrantStatus) { + *out = *in + in.ResourceStatus.DeepCopyInto(&out.ResourceStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultGrantStatus. +func (in *DefaultGrantStatus) DeepCopy() *DefaultGrantStatus { + if in == nil { + return nil + } + out := new(DefaultGrantStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Extension) DeepCopyInto(out *Extension) { *out = *in diff --git a/apis/postgresql/v1alpha1/zz_generated.managed.go b/apis/postgresql/v1alpha1/zz_generated.managed.go index db72a823..3f1acd1c 100644 --- a/apis/postgresql/v1alpha1/zz_generated.managed.go +++ b/apis/postgresql/v1alpha1/zz_generated.managed.go @@ -79,6 +79,66 @@ func (mg *Database) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) mg.Spec.WriteConnectionSecretToReference = r } +// GetCondition of this DefaultGrant. +func (mg *DefaultGrant) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this DefaultGrant. +func (mg *DefaultGrant) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetManagementPolicies of this DefaultGrant. +func (mg *DefaultGrant) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this DefaultGrant. +func (mg *DefaultGrant) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +// GetPublishConnectionDetailsTo of this DefaultGrant. +func (mg *DefaultGrant) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { + return mg.Spec.PublishConnectionDetailsTo +} + +// GetWriteConnectionSecretToReference of this DefaultGrant. +func (mg *DefaultGrant) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this DefaultGrant. +func (mg *DefaultGrant) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this DefaultGrant. +func (mg *DefaultGrant) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetManagementPolicies of this DefaultGrant. +func (mg *DefaultGrant) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this DefaultGrant. +func (mg *DefaultGrant) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +// SetPublishConnectionDetailsTo of this DefaultGrant. +func (mg *DefaultGrant) SetPublishConnectionDetailsTo(r *xpv1.PublishConnectionDetailsTo) { + mg.Spec.PublishConnectionDetailsTo = r +} + +// SetWriteConnectionSecretToReference of this DefaultGrant. +func (mg *DefaultGrant) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} + // GetCondition of this Extension. func (mg *Extension) GetCondition(ct xpv1.ConditionType) xpv1.Condition { return mg.Status.GetCondition(ct) diff --git a/apis/postgresql/v1alpha1/zz_generated.managedlist.go b/apis/postgresql/v1alpha1/zz_generated.managedlist.go index d69f37e8..48a72558 100644 --- a/apis/postgresql/v1alpha1/zz_generated.managedlist.go +++ b/apis/postgresql/v1alpha1/zz_generated.managedlist.go @@ -28,6 +28,15 @@ func (l *DatabaseList) GetItems() []resource.Managed { return items } +// GetItems of this DefaultGrantList. +func (l *DefaultGrantList) GetItems() []resource.Managed { + items := make([]resource.Managed, len(l.Items)) + for i := range l.Items { + items[i] = &l.Items[i] + } + return items +} + // GetItems of this ExtensionList. func (l *ExtensionList) GetItems() []resource.Managed { items := make([]resource.Managed, len(l.Items)) diff --git a/package/crds/postgresql.sql.crossplane.io_defaultgrants.yaml b/package/crds/postgresql.sql.crossplane.io_defaultgrants.yaml new file mode 100644 index 00000000..1e20dfa5 --- /dev/null +++ b/package/crds/postgresql.sql.crossplane.io_defaultgrants.yaml @@ -0,0 +1,532 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: defaultgrants.postgresql.sql.crossplane.io +spec: + group: postgresql.sql.crossplane.io + names: + categories: + - crossplane + - managed + - sql + kind: DefaultGrant + listKind: DefaultGrantList + plural: defaultgrants + singular: defaultgrant + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: READY + type: string + - jsonPath: .status.conditions[?(@.type=='Synced')].status + name: SYNCED + type: string + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + - jsonPath: .spec.forProvider.role + name: ROLE + type: string + - jsonPath: .spec.forProvider.memberOf + name: MEMBER OF + type: string + - jsonPath: .spec.forProvider.database + name: DATABASE + type: string + - jsonPath: .spec.forProvider.privileges + name: PRIVILEGES + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: A Grant represents the declarative state of a PostgreSQL grant. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: A DefaultGrantSpec defines the desired state of a Default + Grant. + properties: + deletionPolicy: + default: Delete + description: |- + DeletionPolicy specifies what will happen to the underlying external + when this managed resource is deleted - either "Delete" or "Orphan" the + external resource. + This field is planned to be deprecated in favor of the ManagementPolicies + field in a future release. Currently, both could be set independently and + non-default values would be honored if the feature flag is enabled. + See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 + enum: + - Orphan + - Delete + type: string + forProvider: + description: DefaultGrantParameters defines the desired state of a + Default Grant. + properties: + database: + description: Database in which the default privileges are applied + type: string + databaseRef: + description: DatabaseRef references the database object this default + grant it for. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + databaseSelector: + description: DatabaseSelector selects a reference to a Database + this grant is for. + properties: + matchControllerRef: + description: |- + MatchControllerRef ensures an object with the same controller reference + as the selecting object is selected. + type: boolean + matchLabels: + additionalProperties: + type: string + description: MatchLabels ensures an object with matching labels + is selected. + type: object + policy: + description: Policies for selection. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + type: object + privileges: + description: |- + Privileges to be granted. + See https://www.postgresql.org/docs/current/sql-grant.html for available privileges. + items: + description: GrantPrivilege represents a privilege to be granted + pattern: ^[A-Z]+$ + type: string + minItems: 1 + type: array + role: + description: Role to which default privileges are granted + type: string + roleRef: + description: RoleRef to which default privileges are granted. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + schema: + description: Schema in which the default privileges are applied + type: string + schemaRef: + description: SchemaRef references the database object this default + grant it for. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + schemaSelector: + description: SchemaSelector selects a reference to a Database + this grant is for. + properties: + matchControllerRef: + description: |- + MatchControllerRef ensures an object with the same controller reference + as the selecting object is selected. + type: boolean + matchLabels: + additionalProperties: + type: string + description: MatchLabels ensures an object with matching labels + is selected. + type: object + policy: + description: Policies for selection. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + type: object + withOption: + description: |- + WithOption allows an option to be set on the grant. + See https://www.postgresql.org/docs/current/sql-grant.html for available + options for each grant type, and the effects of applying the option. + enum: + - ADMIN + - GRANT + type: string + type: object + managementPolicies: + default: + - '*' + description: |- + THIS IS A BETA FIELD. It is on by default but can be opted out + through a Crossplane feature flag. + ManagementPolicies specify the array of actions Crossplane is allowed to + take on the managed and external resources. + This field is planned to replace the DeletionPolicy field in a future + release. Currently, both could be set independently and non-default + values would be honored if the feature flag is enabled. If both are + custom, the DeletionPolicy field will be ignored. + See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 + and this one: https://github.com/crossplane/crossplane/blob/444267e84783136daa93568b364a5f01228cacbe/design/one-pager-ignore-changes.md + items: + description: |- + A ManagementAction represents an action that the Crossplane controllers + can take on an external resource. + enum: + - Observe + - Create + - Update + - Delete + - LateInitialize + - '*' + type: string + type: array + providerConfigRef: + default: + name: default + description: |- + ProviderConfigReference specifies how the provider that will be used to + create, observe, update, and delete this managed resource should be + configured. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + publishConnectionDetailsTo: + description: |- + PublishConnectionDetailsTo specifies the connection secret config which + contains a name, metadata and a reference to secret store config to + which any connection details for this managed resource should be written. + Connection details frequently include the endpoint, username, + and password required to connect to the managed resource. + properties: + configRef: + default: + name: default + description: |- + SecretStoreConfigRef specifies which secret store config should be used + for this ConnectionSecret. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + metadata: + description: Metadata is the metadata for connection secret. + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations are the annotations to be added to connection secret. + - For Kubernetes secrets, this will be used as "metadata.annotations". + - It is up to Secret Store implementation for others store types. + type: object + labels: + additionalProperties: + type: string + description: |- + Labels are the labels/tags to be added to connection secret. + - For Kubernetes secrets, this will be used as "metadata.labels". + - It is up to Secret Store implementation for others store types. + type: object + type: + description: |- + Type is the SecretType for the connection secret. + - Only valid for Kubernetes Secret Stores. + type: string + type: object + name: + description: Name is the name of the connection secret. + type: string + required: + - name + type: object + writeConnectionSecretToRef: + description: |- + WriteConnectionSecretToReference specifies the namespace and name of a + Secret to which any connection details for this managed resource should + be written. Connection details frequently include the endpoint, username, + and password required to connect to the managed resource. + This field is planned to be replaced in a future release in favor of + PublishConnectionDetailsTo. Currently, both could be set independently + and connection details would be published to both without affecting + each other. + properties: + name: + description: Name of the secret. + type: string + namespace: + description: Namespace of the secret. + type: string + required: + - name + - namespace + type: object + required: + - forProvider + type: object + status: + description: A DefaultGrantStatus represents the observed state of a Grant. + properties: + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastTransitionTime: + description: |- + LastTransitionTime is the last time this condition transitioned from one + status to another. + format: date-time + type: string + message: + description: |- + A Message containing details about this condition's last transition from + one status to another, if any. + type: string + observedGeneration: + description: |- + ObservedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + type: integer + reason: + description: A Reason for this condition's last transition from + one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown? + type: string + type: + description: |- + Type of this condition. At most one of each condition type may apply to + a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedGeneration: + description: |- + ObservedGeneration is the latest metadata.generation + which resulted in either a ready state, or stalled due to error + it can not recover from without human intervention. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/pkg/controller/postgresql/default_grant/reconciler.go b/pkg/controller/postgresql/default_grant/reconciler.go new file mode 100644 index 00000000..e72185b3 --- /dev/null +++ b/pkg/controller/postgresql/default_grant/reconciler.go @@ -0,0 +1,385 @@ +/* +Copyright 2020 The Crossplane Authors. + +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 default_grant + +import ( + "context" + "fmt" + "strings" + + "github.com/lib/pq" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller" + "github.com/crossplane/crossplane-runtime/pkg/event" + "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/pkg/resource" + + "github.com/crossplane-contrib/provider-sql/apis/postgresql/v1alpha1" + "github.com/crossplane-contrib/provider-sql/pkg/clients" + "github.com/crossplane-contrib/provider-sql/pkg/clients/postgresql" + "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" +) + +const ( + errTrackPCUsage = "cannot track ProviderConfig usage" + errGetPC = "cannot get ProviderConfig" + errNoSecretRef = "ProviderConfig does not reference a credentials Secret" + errGetSecret = "cannot get credentials Secret" + + errNotGrant = "managed resource is not a Grant custom resource" + errSelectGrant = "cannot select grant" + errCreateGrant = "cannot create grant" + errRevokeGrant = "cannot revoke grant" + errNoRole = "role not passed or could not be resolved" + errNoDatabase = "database not passed or could not be resolved" + errNoPrivileges = "privileges not passed" + errUnknownGrant = "cannot identify grant type based on passed params" + + errInvalidParams = "invalid parameters for grant type %s" + + errMemberOfWithDatabaseOrPrivileges = "cannot set privileges or database in the same grant as memberOf" + + maxConcurrency = 5 +) + +// Setup adds a controller that reconciles Grant managed resources. +func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { + name := managed.ControllerName(v1alpha1.GrantGroupKind) + + t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{}) + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1alpha1.GrantGroupVersionKind), + managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newDB: postgresql.New}), + managed.WithLogger(o.Logger.WithValues("controller", name)), + managed.WithPollInterval(o.PollInterval), + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) + + return ctrl.NewControllerManagedBy(mgr). + Named(name). + For(&v1alpha1.DefaultGrant{}). + WithOptions(controller.Options{ + MaxConcurrentReconciles: maxConcurrency, + }). + Complete(r) +} + +type connector struct { + kube client.Client + usage resource.Tracker + newDB func(creds map[string][]byte, database string, sslmode string) xsql.DB +} + +func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { + cr, ok := mg.(*v1alpha1.Grant) + if !ok { + return nil, errors.New(errNotGrant) + } + + if err := c.usage.Track(ctx, mg); err != nil { + return nil, errors.Wrap(err, errTrackPCUsage) + } + + // ProviderConfigReference could theoretically be nil, but in practice the + // DefaultProviderConfig initializer will set it before we get here. + pc := &v1alpha1.ProviderConfig{} + if err := c.kube.Get(ctx, types.NamespacedName{Name: cr.GetProviderConfigReference().Name}, pc); err != nil { + return nil, errors.Wrap(err, errGetPC) + } + + // We don't need to check the credentials source because we currently only + // support one source (PostgreSQLConnectionSecret), which is required and + // enforced by the ProviderConfig schema. + ref := pc.Spec.Credentials.ConnectionSecretRef + if ref == nil { + return nil, errors.New(errNoSecretRef) + } + + s := &corev1.Secret{} + if err := c.kube.Get(ctx, types.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}, s); err != nil { + return nil, errors.Wrap(err, errGetSecret) + } + return &external{ + db: c.newDB(s.Data, pc.Spec.DefaultDatabase, clients.ToString(pc.Spec.SSLMode)), + kube: c.kube, + }, nil +} + +type external struct { + db xsql.DB + kube client.Client +} + +type grantType string + +const ( + roleMember grantType = "ROLE_MEMBER" + roleDatabase grantType = "ROLE_DATABASE" +) + +func identifyGrantType(gp v1alpha1.GrantParameters) (grantType, error) { + pc := len(gp.Privileges) + + // If memberOf is specified, this is ROLE_MEMBER + // NOTE: If any of these are set, even if the lookup by ref or selector fails, + // then this is still a roleMember grant type. + if gp.MemberOfRef != nil || gp.MemberOfSelector != nil || gp.MemberOf != nil { + if gp.Database != nil || pc > 0 { + return "", errors.New(errMemberOfWithDatabaseOrPrivileges) + } + return roleMember, nil + } + + if gp.Database == nil { + return "", errors.New(errNoDatabase) + } + + if pc < 1 { + return "", errors.New(errNoPrivileges) + } + + // This is ROLE_DATABASE + return roleDatabase, nil +} + +func selectGrantQuery(gp v1alpha1.GrantParameters, q *xsql.Query) error { + gt, err := identifyGrantType(gp) + if err != nil { + return err + } + + switch gt { + case roleMember: + ao := gp.WithOption != nil && *gp.WithOption == v1alpha1.GrantOptionAdmin + + // Always returns a row with a true or false value + // A simpler query would use ::regrol to cast the + // roleid and member oids to their role names, but + // if this is used with a nonexistent role name it will + // throw an error rather than return false. + q.String = "SELECT EXISTS(SELECT 1 FROM pg_auth_members m " + + "INNER JOIN pg_roles mo ON m.roleid = mo.oid " + + "INNER JOIN pg_roles r ON m.member = r.oid " + + "WHERE r.rolname=$1 AND mo.rolname=$2 AND " + + "m.admin_option = $3)" + + q.Parameters = []interface{}{ + gp.Role, + gp.MemberOf, + ao, + } + return nil + case roleDatabase: + gro := gp.WithOption != nil && *gp.WithOption == v1alpha1.GrantOptionGrant + + ep := gp.Privileges.ExpandPrivileges() + sp := ep.ToStringSlice() + // Join grantee. Filter by database name and grantee name. + // Finally, perform a permission comparison against expected + // permissions. + q.String = "SELECT EXISTS(SELECT 1 " + + "FROM pg_database db, " + + "aclexplode(datacl) as acl " + + "INNER JOIN pg_roles s ON acl.grantee = s.oid " + + // Filter by database, role and grantable setting + "WHERE db.datname=$1 " + + "AND s.rolname=$2 " + + "AND acl.is_grantable=$3 " + + "GROUP BY db.datname, s.rolname, acl.is_grantable " + + // Check privileges match. Convoluted right-hand-side is necessary to + // ensure identical sort order of the input permissions. + "HAVING array_agg(acl.privilege_type ORDER BY privilege_type ASC) " + + "= (SELECT array(SELECT unnest($4::text[]) as perms ORDER BY perms ASC)))" + + q.Parameters = []interface{}{ + gp.Database, + gp.Role, + gro, + pq.Array(sp), + } + return nil + } + return errors.New(errUnknownGrant) +} + +func withOption(option *v1alpha1.GrantOption) string { + if option != nil { + return fmt.Sprintf("WITH %s OPTION", string(*option)) + } + return "" +} + +func createGrantQueries(gp v1alpha1.GrantParameters, ql *[]xsql.Query) error { // nolint: gocyclo + gt, err := identifyGrantType(gp) + if err != nil { + return err + } + + ro := pq.QuoteIdentifier(*gp.Role) + + switch gt { + case roleMember: + if gp.MemberOf == nil || gp.Role == nil { + return errors.Errorf(errInvalidParams, roleMember) + } + + mo := pq.QuoteIdentifier(*gp.MemberOf) + + *ql = append(*ql, + xsql.Query{String: fmt.Sprintf("REVOKE %s FROM %s", mo, ro)}, + xsql.Query{String: fmt.Sprintf("GRANT %s TO %s %s", mo, ro, + withOption(gp.WithOption), + )}, + ) + return nil + case roleDatabase: + if gp.Database == nil || gp.Role == nil || len(gp.Privileges) < 1 { + return errors.Errorf(errInvalidParams, roleDatabase) + } + + db := pq.QuoteIdentifier(*gp.Database) + sp := strings.Join(gp.Privileges.ToStringSlice(), ",") + + *ql = append(*ql, + // REVOKE ANY MATCHING EXISTING PERMISSIONS + xsql.Query{String: fmt.Sprintf("REVOKE %s ON DATABASE %s FROM %s", + sp, + db, + ro, + )}, + + // GRANT REQUESTED PERMISSIONS + xsql.Query{String: fmt.Sprintf("GRANT %s ON DATABASE %s TO %s %s", + sp, + db, + ro, + withOption(gp.WithOption), + )}, + ) + return nil + } + return errors.New(errUnknownGrant) +} + +func deleteGrantQuery(gp v1alpha1.GrantParameters, q *xsql.Query) error { + gt, err := identifyGrantType(gp) + if err != nil { + return err + } + + ro := pq.QuoteIdentifier(*gp.Role) + + switch gt { + case roleMember: + q.String = fmt.Sprintf("REVOKE %s FROM %s", + pq.QuoteIdentifier(*gp.MemberOf), + ro, + ) + return nil + case roleDatabase: + q.String = fmt.Sprintf("REVOKE %s ON DATABASE %s FROM %s", + strings.Join(gp.Privileges.ToStringSlice(), ","), + pq.QuoteIdentifier(*gp.Database), + ro, + ) + return nil + } + return errors.New(errUnknownGrant) +} + +func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { + cr, ok := mg.(*v1alpha1.Grant) + if !ok { + return managed.ExternalObservation{}, errors.New(errNotGrant) + } + + if cr.Spec.ForProvider.Role == nil { + return managed.ExternalObservation{}, errors.New(errNoRole) + } + + gp := cr.Spec.ForProvider + var query xsql.Query + if err := selectGrantQuery(gp, &query); err != nil { + return managed.ExternalObservation{}, err + } + + exists := false + + if err := c.db.Scan(ctx, query, &exists); err != nil { + return managed.ExternalObservation{}, errors.Wrap(err, errSelectGrant) + } + + if !exists { + return managed.ExternalObservation{ResourceExists: false}, nil + } + + // Grants have no way of being 'not up to date' - if they exist, they are up to date + cr.SetConditions(xpv1.Available()) + + return managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + ResourceLateInitialized: false, + }, nil +} + +func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) { + cr, ok := mg.(*v1alpha1.Grant) + if !ok { + return managed.ExternalCreation{}, errors.New(errNotGrant) + } + + var queries []xsql.Query + + cr.SetConditions(xpv1.Creating()) + + if err := createGrantQueries(cr.Spec.ForProvider, &queries); err != nil { + return managed.ExternalCreation{}, errors.Wrap(err, errCreateGrant) + } + + err := c.db.ExecTx(ctx, queries) + return managed.ExternalCreation{}, errors.Wrap(err, errCreateGrant) +} + +func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { + // Update is a no-op, as permissions are fully revoked and then granted in the Create function, + // inside a transaction. + return managed.ExternalUpdate{}, nil +} + +func (c *external) Delete(ctx context.Context, mg resource.Managed) error { + cr, ok := mg.(*v1alpha1.Grant) + if !ok { + return errors.New(errNotGrant) + } + var query xsql.Query + + cr.SetConditions(xpv1.Deleting()) + + err := deleteGrantQuery(cr.Spec.ForProvider, &query) + if err != nil { + return errors.Wrap(err, errRevokeGrant) + } + + return errors.Wrap(c.db.Exec(ctx, query), errRevokeGrant) +} diff --git a/pkg/controller/postgresql/default_grant/reconciler_test.go b/pkg/controller/postgresql/default_grant/reconciler_test.go new file mode 100644 index 00000000..eed4a81a --- /dev/null +++ b/pkg/controller/postgresql/default_grant/reconciler_test.go @@ -0,0 +1,627 @@ +/* +Copyright 2020 The Crossplane Authors. + +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 default_grant + +import ( + "context" + "database/sql" + "fmt" + "sort" + "testing" + + "github.com/crossplane-contrib/provider-sql/apis/postgresql/v1alpha1" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/lib/pq" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/pkg/resource" + "github.com/crossplane/crossplane-runtime/pkg/test" + + "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" +) + +type mockDB struct { + MockExec func(ctx context.Context, q xsql.Query) error + MockExecTx func(ctx context.Context, ql []xsql.Query) error + MockScan func(ctx context.Context, q xsql.Query, dest ...interface{}) error + MockQuery func(ctx context.Context, q xsql.Query) (*sql.Rows, error) + MockGetConnectionDetails func(username, password string) managed.ConnectionDetails +} + +func (m mockDB) Exec(ctx context.Context, q xsql.Query) error { + return m.MockExec(ctx, q) +} + +func (m mockDB) ExecTx(ctx context.Context, ql []xsql.Query) error { + return m.MockExecTx(ctx, ql) +} + +func (m mockDB) Scan(ctx context.Context, q xsql.Query, dest ...interface{}) error { + return m.MockScan(ctx, q, dest...) +} + +func (m mockDB) Query(ctx context.Context, q xsql.Query) (*sql.Rows, error) { + return m.MockQuery(ctx, q) +} + +func (m mockDB) GetConnectionDetails(username, password string) managed.ConnectionDetails { + return m.MockGetConnectionDetails(username, password) +} + +func TestConnect(t *testing.T) { + errBoom := errors.New("boom") + + type fields struct { + kube client.Client + usage resource.Tracker + newDB func(creds map[string][]byte, database string, sslmode string) xsql.DB + } + + type args struct { + ctx context.Context + mg resource.Managed + } + + cases := map[string]struct { + reason string + fields fields + args args + want error + }{ + "ErrNotGrant": { + reason: "An error should be returned if the managed resource is not a *Grant", + args: args{ + mg: nil, + }, + want: errors.New(errNotGrant), + }, + "ErrTrackProviderConfigUsage": { + reason: "An error should be returned if we can't track our ProviderConfig usage", + fields: fields{ + usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return errBoom }), + }, + args: args{ + mg: &v1alpha1.Grant{}, + }, + want: errors.Wrap(errBoom, errTrackPCUsage), + }, + "ErrGetProviderConfig": { + reason: "An error should be returned if we can't get our ProviderConfig", + fields: fields{ + kube: &test.MockClient{ + MockGet: test.NewMockGetFn(errBoom), + }, + usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ResourceSpec: xpv1.ResourceSpec{ + ProviderConfigReference: &xpv1.Reference{}, + }, + }, + }, + }, + want: errors.Wrap(errBoom, errGetPC), + }, + "ErrMissingConnectionSecret": { + reason: "An error should be returned if our ProviderConfig doesn't specify a connection secret", + fields: fields{ + kube: &test.MockClient{ + // We call get to populate the Grant struct, then again + // to populate the (empty) ProviderConfig struct, resulting + // in a ProviderConfig with a nil connection secret. + MockGet: test.NewMockGetFn(nil), + }, + usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ResourceSpec: xpv1.ResourceSpec{ + ProviderConfigReference: &xpv1.Reference{}, + }, + }, + }, + }, + want: errors.New(errNoSecretRef), + }, + "ErrGetConnectionSecret": { + reason: "An error should be returned if we can't get our ProviderConfig's connection secret", + fields: fields{ + kube: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + switch o := obj.(type) { + case *v1alpha1.ProviderConfig: + o.Spec.Credentials.ConnectionSecretRef = &xpv1.SecretReference{} + case *corev1.Secret: + return errBoom + } + return nil + }), + }, + usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ResourceSpec: xpv1.ResourceSpec{ + ProviderConfigReference: &xpv1.Reference{}, + }, + }, + }, + }, + want: errors.Wrap(errBoom, errGetSecret), + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + e := &connector{kube: tc.fields.kube, usage: tc.fields.usage, newDB: tc.fields.newDB} + _, err := e.Connect(tc.args.ctx, tc.args.mg) + if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\ne.Connect(...): -want error, +got error:\n%s\n", tc.reason, diff) + } + }) + } +} + +func TestObserve(t *testing.T) { + errBoom := errors.New("boom") + goa := v1alpha1.GrantOptionAdmin + gog := v1alpha1.GrantOptionGrant + + type fields struct { + db xsql.DB + } + + type args struct { + ctx context.Context + mg resource.Managed + } + + type want struct { + o managed.ExternalObservation + err error + } + + cases := map[string]struct { + reason string + fields fields + args args + want want + }{ + "ErrNotGrant": { + reason: "An error should be returned if the managed resource is not a *Grant", + args: args{ + mg: nil, + }, + want: want{ + err: errors.New(errNotGrant), + }, + }, + "SuccessNoGrant": { + reason: "We should return ResourceExists: false when no grant is found", + fields: fields{ + db: mockDB{ + MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { + // Default value is false, so just return + bv := dest[0].(*bool) + *bv = false + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Database: ptr.To("test-example"), + Role: ptr.To("test-example"), + Privileges: v1alpha1.GrantPrivileges{"ALL"}, + }, + }, + }, + }, + want: want{ + o: managed.ExternalObservation{ResourceExists: false}, + }, + }, + "AllMapsToExpandedPrivileges": { + reason: "We expand ALL to CREATE, TEMPORARY, CONNECT when checking for existing grants", + fields: fields{ + db: mockDB{ + MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { + privileges := q.Parameters[3] + + privs, ok := privileges.(*pq.StringArray) + if !ok { + return fmt.Errorf("expected Scan parameter to be pq.StringArray, got %T", privileges) + } + + // The order is not guaranteed, so sort the slices before comparing + sort.Strings(*privs) + + // Return if there's a diff between the expected and actual privileges + diff := cmp.Diff(&pq.StringArray{"CONNECT", "CREATE", "TEMPORARY"}, privileges) + + bv := dest[0].(*bool) + *bv = diff == "" + + // Extra logging in case this test is going to fail + if diff != "" { + t.Logf("expected empty diff, got: %s", diff) + } + + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Database: ptr.To("test-example"), + Role: ptr.To("test-example"), + Privileges: v1alpha1.GrantPrivileges{"ALL"}, + }, + }, + }, + }, + want: want{ + o: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + }, + err: nil, + }, + }, + "ErrSelectGrant": { + reason: "We should return any errors encountered while trying to show the grant", + fields: fields{ + db: mockDB{ + MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { + return errBoom + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Database: ptr.To("test-example"), + Role: ptr.To("test-example"), + Privileges: v1alpha1.GrantPrivileges{"CONNECT", "TEMPORARY"}, + WithOption: &gog, + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errBoom, errSelectGrant), + }, + }, + "SuccessRoleDb": { + reason: "We should return no error if we can find our role-db grant", + fields: fields{ + db: mockDB{ + MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { + bv := dest[0].(*bool) + *bv = true + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Database: ptr.To("testdb"), + Role: ptr.To("testrole"), + Privileges: v1alpha1.GrantPrivileges{"ALL"}, + WithOption: &gog, + }, + }, + }, + }, + want: want{ + o: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + }, + err: nil, + }, + }, + "SuccessRoleMembership": { + reason: "We should return no error if we can find our role-membership grant", + fields: fields{ + db: mockDB{ + MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { + bv := dest[0].(*bool) + *bv = true + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Role: ptr.To("testrole"), + MemberOf: ptr.To("parentrole"), + WithOption: &goa, + }, + }, + }, + }, + want: want{ + o: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + }, + err: nil, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + e := external{db: tc.fields.db} + got, err := e.Observe(tc.args.ctx, tc.args.mg) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\ne.Observe(...): -want error, +got error:\n%s\n", tc.reason, diff) + } + if diff := cmp.Diff(tc.want.o, got); diff != "" { + t.Errorf("\n%s\ne.Observe(...): -want, +got:\n%s\n", tc.reason, diff) + } + }) + } +} + +func TestCreate(t *testing.T) { + errBoom := errors.New("boom") + + type fields struct { + db xsql.DB + } + + type args struct { + ctx context.Context + mg resource.Managed + } + + type want struct { + c managed.ExternalCreation + err error + } + + cases := map[string]struct { + reason string + fields fields + args args + want want + }{ + "ErrNotGrant": { + reason: "An error should be returned if the managed resource is not a *Grant", + args: args{ + mg: nil, + }, + want: want{ + err: errors.New(errNotGrant), + }, + }, + "ErrExec": { + reason: "Any errors encountered while creating the grant should be returned", + fields: fields{ + db: &mockDB{ + MockExecTx: func(ctx context.Context, ql []xsql.Query) error { return errBoom }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Database: ptr.To("test-example"), + Role: ptr.To("test-example"), + Privileges: v1alpha1.GrantPrivileges{"ALL"}, + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errBoom, errCreateGrant), + }, + }, + "Success": { + reason: "No error should be returned when we successfully create a grant", + fields: fields{ + db: &mockDB{ + MockExecTx: func(ctx context.Context, ql []xsql.Query) error { return nil }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Database: ptr.To("test-example"), + Role: ptr.To("test-example"), + Privileges: v1alpha1.GrantPrivileges{"ALL"}, + }, + }, + }, + }, + want: want{ + err: nil, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + e := external{db: tc.fields.db} + got, err := e.Create(tc.args.ctx, tc.args.mg) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\ne.Create(...): -want error, +got error:\n%s\n", tc.reason, diff) + } + if diff := cmp.Diff(tc.want.c, got); diff != "" { + t.Errorf("\n%s\ne.Create(...): -want, +got:\n%s\n", tc.reason, diff) + } + }) + } +} + +func TestUpdate(t *testing.T) { + type fields struct { + db xsql.DB + } + + type args struct { + ctx context.Context + mg resource.Managed + } + + type want struct { + c managed.ExternalUpdate + err error + } + + cases := map[string]struct { + reason string + fields fields + args args + want want + }{ + "ErrNoOp": { + reason: "Update is a no-op, make sure we dont throw an error *Grant", + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Database: ptr.To("test-example"), + Role: ptr.To("test-example"), + Privileges: v1alpha1.GrantPrivileges{"ALL"}, + }, + }, + }, + }, + want: want{ + err: nil, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + e := external{ + db: tc.fields.db, + } + got, err := e.Update(tc.args.ctx, tc.args.mg) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\ne.Create(...): -want error, +got error:\n%s\n", tc.reason, diff) + } + if diff := cmp.Diff(tc.want.c, got, cmpopts.IgnoreMapEntries(func(key string, _ []byte) bool { return key == "password" })); diff != "" { + t.Errorf("\n%s\ne.Create(...): -want, +got:\n%s\n", tc.reason, diff) + } + }) + } +} + +func TestDelete(t *testing.T) { + errBoom := errors.New("boom") + + type fields struct { + db xsql.DB + } + + type args struct { + ctx context.Context + mg resource.Managed + } + + cases := map[string]struct { + reason string + fields fields + args args + want error + }{ + "ErrNotGrant": { + reason: "An error should be returned if the managed resource is not a *Grant", + args: args{ + mg: nil, + }, + want: errors.New(errNotGrant), + }, + "ErrDropGrant": { + reason: "Errors dropping a grant should be returned", + fields: fields{ + db: &mockDB{ + MockExec: func(ctx context.Context, q xsql.Query) error { + return errBoom + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Database: ptr.To("test-example"), + Role: ptr.To("test-example"), + Privileges: v1alpha1.GrantPrivileges{"ALL"}, + }, + }, + }, + }, + want: errors.Wrap(errBoom, errRevokeGrant), + }, + "Success": { + reason: "No error should be returned if the grant was revoked", + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Database: ptr.To("test-example"), + Role: ptr.To("test-example"), + Privileges: v1alpha1.GrantPrivileges{"ALL"}, + }, + }, + }, + }, + fields: fields{ + db: &mockDB{ + MockExec: func(ctx context.Context, q xsql.Query) error { return nil }, + }, + }, + want: nil, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + e := external{db: tc.fields.db} + err := e.Delete(tc.args.ctx, tc.args.mg) + if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\ne.Delete(...): -want error, +got error:\n%s\n", tc.reason, diff) + } + }) + } +} From 6173d6ac59564290a7ad3552600aee20d779ca8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Fern=C3=A1ndez=20Campo?= Date: Sat, 26 Oct 2024 09:50:36 +0200 Subject: [PATCH 02/17] formatted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joaquín Fernández Campo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5cbee906..51ef52e4 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Check the example: 2. Create managed resources for your SQL server flavor: - **MySQL**: `Database`, `Grant`, `User` (See [the examples](examples/mysql)) - - **PostgreSQL**: `Database`, `Grant`,`DefaultGrant`, `Extension`, `Role` (See [the examples](examples/postgresql)) + - **PostgreSQL**: `Database`, `Grant`, `DefaultGrant`, `Extension`, `Role` (See [the examples](examples/postgresql)) - **MSSQL**: `Database`, `Grant`, `User` (See [the examples](examples/mssql)) [crossplane]: https://crossplane.io From 87f21dac25bed544216a828ddc84cee44c440ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Fern=C3=A1ndez=20Campo?= Date: Sat, 26 Oct 2024 14:19:11 +0200 Subject: [PATCH 03/17] added default grant type, implemented an initial version of the controller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joaquín Fernández Campo --- .../v1alpha1/default_grant_types.go | 69 +++++ pkg/clients/postgresql/postgresql.go | 9 +- .../postgresql/default_grant/reconciler.go | 291 +++++++----------- .../default_grant/reconciler_test.go | 6 +- pkg/controller/sql.go | 2 +- 5 files changed, 191 insertions(+), 186 deletions(-) diff --git a/apis/postgresql/v1alpha1/default_grant_types.go b/apis/postgresql/v1alpha1/default_grant_types.go index 0570df32..0d328097 100644 --- a/apis/postgresql/v1alpha1/default_grant_types.go +++ b/apis/postgresql/v1alpha1/default_grant_types.go @@ -1,8 +1,13 @@ package v1alpha1 import ( + "context" + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/reference" + "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" ) // +kubebuilder:object:root=true @@ -43,6 +48,16 @@ type DefaultGrantParameters struct { // +optional Privileges GrantPrivileges `json:"privileges,omitempty"` + // TargetRole is the role who owns objects on which the default privileges are granted. + // See https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html + // +required + TargetRole string `json:"targetRole"` + + // ObjectType to which the privileges are granted. + // +kubebuilder:validation:Enum=table;sequence;function;schema + // +required + ObjectType *string `json:"objectType,omitempty"` + // WithOption allows an option to be set on the grant. // See https://www.postgresql.org/docs/current/sql-grant.html for available // options for each grant type, and the effects of applying the option. @@ -59,6 +74,11 @@ type DefaultGrantParameters struct { // +optional RoleRef *xpv1.Reference `json:"roleRef,omitempty"` + // RoleSelector selects a reference to a Role this default grant is for. + // +immutable + // +optional + RoleSelector *xpv1.Selector `json:"roleSelector,omitempty"` + // Database in which the default privileges are applied // +optional Database *string `json:"database,omitempty"` @@ -96,3 +116,52 @@ type DefaultGrantList struct { metav1.ListMeta `json:"metadata,omitempty"` Items []DefaultGrant `json:"items"` } + +// ResolveReferences of this DefaultGrant. +func (mg *DefaultGrant) ResolveReferences(ctx context.Context, c client.Reader) error { + r := reference.NewAPIResolver(c, mg) + + // Resolve spec.forProvider.database + rsp, err := r.Resolve(ctx, reference.ResolutionRequest{ + CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Database), + Reference: mg.Spec.ForProvider.DatabaseRef, + Selector: mg.Spec.ForProvider.DatabaseSelector, + To: reference.To{Managed: &Database{}, List: &DatabaseList{}}, + Extract: reference.ExternalName(), + }) + if err != nil { + return errors.Wrap(err, "spec.forProvider.database") + } + mg.Spec.ForProvider.Database = reference.ToPtrValue(rsp.ResolvedValue) + mg.Spec.ForProvider.DatabaseRef = rsp.ResolvedReference + + // Resolve spec.forProvider.role + rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ + CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Role), + Reference: mg.Spec.ForProvider.RoleRef, + Selector: mg.Spec.ForProvider.RoleSelector, + To: reference.To{Managed: &Role{}, List: &RoleList{}}, + Extract: reference.ExternalName(), + }) + if err != nil { + return errors.Wrap(err, "spec.forProvider.role") + } + mg.Spec.ForProvider.Role = reference.ToPtrValue(rsp.ResolvedValue) + mg.Spec.ForProvider.RoleRef = rsp.ResolvedReference + + // Resolve spec.forProvider.schema + rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ + CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Schema), + Reference: mg.Spec.ForProvider.SchemaRef, + Selector: mg.Spec.ForProvider.SchemaSelector, + To: reference.To{Managed: &Role{}, List: &RoleList{}}, + Extract: reference.ExternalName(), + }) + if err != nil { + return errors.Wrap(err, "spec.forProvider.schema") + } + mg.Spec.ForProvider.Schema = reference.ToPtrValue(rsp.ResolvedValue) + mg.Spec.ForProvider.SchemaRef = rsp.ResolvedReference + + return nil +} diff --git a/pkg/clients/postgresql/postgresql.go b/pkg/clients/postgresql/postgresql.go index 270bb92c..ada8eed3 100644 --- a/pkg/clients/postgresql/postgresql.go +++ b/pkg/clients/postgresql/postgresql.go @@ -77,11 +77,12 @@ func (c postgresDB) ExecTx(ctx context.Context, ql []xsql.Query) error { // sure the connection is always closed. defer func() { defer d.Close() //nolint:errcheck - if err != nil { - tx.Rollback() //nolint:errcheck - return + // We always rollback, it's a no-op if the tx was already committed. + defer tx.Rollback() //nolint:errcheck + + if err == nil { + err = tx.Commit() } - err = tx.Commit() }() for _, q := range ql { diff --git a/pkg/controller/postgresql/default_grant/reconciler.go b/pkg/controller/postgresql/default_grant/reconciler.go index e72185b3..e74c8d5a 100644 --- a/pkg/controller/postgresql/default_grant/reconciler.go +++ b/pkg/controller/postgresql/default_grant/reconciler.go @@ -19,6 +19,7 @@ package default_grant import ( "context" "fmt" + "sort" "strings" "github.com/lib/pq" @@ -47,14 +48,14 @@ const ( errNoSecretRef = "ProviderConfig does not reference a credentials Secret" errGetSecret = "cannot get credentials Secret" - errNotGrant = "managed resource is not a Grant custom resource" - errSelectGrant = "cannot select grant" - errCreateGrant = "cannot create grant" - errRevokeGrant = "cannot revoke grant" - errNoRole = "role not passed or could not be resolved" - errNoDatabase = "database not passed or could not be resolved" - errNoPrivileges = "privileges not passed" - errUnknownGrant = "cannot identify grant type based on passed params" + errNotGrant = "managed resource is not a Grant custom resource" + errSelectDefaultGrant = "cannot select default grant" + errCreateDefaultGrant = "cannot create default grant" + errRevokeDefaultGrant = "cannot revoke default grant" + errNoRole = "role not passed or could not be resolved" + errNoDatabase = "database not passed or could not be resolved" + errNoPrivileges = "privileges not passed" + errUnknownGrant = "cannot identify grant type based on passed params" errInvalidParams = "invalid parameters for grant type %s" @@ -91,7 +92,7 @@ type connector struct { } func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { - cr, ok := mg.(*v1alpha1.Grant) + cr, ok := mg.(*v1alpha1.DefaultGrant) if !ok { return nil, errors.New(errNotGrant) } @@ -137,89 +138,33 @@ const ( roleDatabase grantType = "ROLE_DATABASE" ) -func identifyGrantType(gp v1alpha1.GrantParameters) (grantType, error) { - pc := len(gp.Privileges) - - // If memberOf is specified, this is ROLE_MEMBER - // NOTE: If any of these are set, even if the lookup by ref or selector fails, - // then this is still a roleMember grant type. - if gp.MemberOfRef != nil || gp.MemberOfSelector != nil || gp.MemberOf != nil { - if gp.Database != nil || pc > 0 { - return "", errors.New(errMemberOfWithDatabaseOrPrivileges) - } - return roleMember, nil - } - - if gp.Database == nil { - return "", errors.New(errNoDatabase) +var ( + objectTypes = map[string]string{ + "table": "r", + "sequence": "S", + "function": "f", + "type": "T", + "schema": "n", } +) - if pc < 1 { - return "", errors.New(errNoPrivileges) - } - - // This is ROLE_DATABASE - return roleDatabase, nil -} - -func selectGrantQuery(gp v1alpha1.GrantParameters, q *xsql.Query) error { - gt, err := identifyGrantType(gp) - if err != nil { - return err +func selectDefaultGrantQuery(gp v1alpha1.DefaultGrantParameters, q *xsql.Query) error { + + sqlString := ` + select distinct(default_acl.privilege_type) + from pg_roles r + join (SELECT defaclnamespace, (aclexplode(defaclacl)).* FROM pg_default_acl + WHERE defaclobjtype = $1) default_acl + on r.oid = default_acl.grantee + where r.rolname = $2; + ` + q.String = sqlString + q.Parameters = []interface{}{ + objectTypes[*gp.ObjectType], + *gp.Role, } - switch gt { - case roleMember: - ao := gp.WithOption != nil && *gp.WithOption == v1alpha1.GrantOptionAdmin - - // Always returns a row with a true or false value - // A simpler query would use ::regrol to cast the - // roleid and member oids to their role names, but - // if this is used with a nonexistent role name it will - // throw an error rather than return false. - q.String = "SELECT EXISTS(SELECT 1 FROM pg_auth_members m " + - "INNER JOIN pg_roles mo ON m.roleid = mo.oid " + - "INNER JOIN pg_roles r ON m.member = r.oid " + - "WHERE r.rolname=$1 AND mo.rolname=$2 AND " + - "m.admin_option = $3)" - - q.Parameters = []interface{}{ - gp.Role, - gp.MemberOf, - ao, - } - return nil - case roleDatabase: - gro := gp.WithOption != nil && *gp.WithOption == v1alpha1.GrantOptionGrant - - ep := gp.Privileges.ExpandPrivileges() - sp := ep.ToStringSlice() - // Join grantee. Filter by database name and grantee name. - // Finally, perform a permission comparison against expected - // permissions. - q.String = "SELECT EXISTS(SELECT 1 " + - "FROM pg_database db, " + - "aclexplode(datacl) as acl " + - "INNER JOIN pg_roles s ON acl.grantee = s.oid " + - // Filter by database, role and grantable setting - "WHERE db.datname=$1 " + - "AND s.rolname=$2 " + - "AND acl.is_grantable=$3 " + - "GROUP BY db.datname, s.rolname, acl.is_grantable " + - // Check privileges match. Convoluted right-hand-side is necessary to - // ensure identical sort order of the input permissions. - "HAVING array_agg(acl.privilege_type ORDER BY privilege_type ASC) " + - "= (SELECT array(SELECT unnest($4::text[]) as perms ORDER BY perms ASC)))" - - q.Parameters = []interface{}{ - gp.Database, - gp.Role, - gro, - pq.Array(sp), - } - return nil - } - return errors.New(errUnknownGrant) + return nil } func withOption(option *v1alpha1.GrantOption) string { @@ -229,86 +174,70 @@ func withOption(option *v1alpha1.GrantOption) string { return "" } -func createGrantQueries(gp v1alpha1.GrantParameters, ql *[]xsql.Query) error { // nolint: gocyclo - gt, err := identifyGrantType(gp) - if err != nil { - return err +func inSchema(params *v1alpha1.DefaultGrantParameters) string { + if params.Schema != nil { + return fmt.Sprintf("IN SCHEMA %s", pq.QuoteIdentifier(*params.Schema)) } + return "" +} - ro := pq.QuoteIdentifier(*gp.Role) +func createDefaultGrantQuery(gp v1alpha1.DefaultGrantParameters, q *xsql.Query) error { // nolint: gocyclo - switch gt { - case roleMember: - if gp.MemberOf == nil || gp.Role == nil { - return errors.Errorf(errInvalidParams, roleMember) - } + roleName := pq.QuoteIdentifier(*gp.Role) + targetRoleName := pq.QuoteIdentifier(gp.TargetRole) + objectType := objectTypes[*gp.ObjectType] - mo := pq.QuoteIdentifier(*gp.MemberOf) - - *ql = append(*ql, - xsql.Query{String: fmt.Sprintf("REVOKE %s FROM %s", mo, ro)}, - xsql.Query{String: fmt.Sprintf("GRANT %s TO %s %s", mo, ro, - withOption(gp.WithOption), - )}, - ) - return nil - case roleDatabase: - if gp.Database == nil || gp.Role == nil || len(gp.Privileges) < 1 { - return errors.Errorf(errInvalidParams, roleDatabase) - } + query := strings.TrimSpace(fmt.Sprintf( + "ALTER DEFAULT PRIVILEGES FOR ROLE %s %s GRANT %s ON %s TO %s %s", + targetRoleName, + inSchema(&gp), + strings.Join(gp.Privileges.ToStringSlice(), ","), + objectType, + roleName, + withOption(gp.WithOption), + )) - db := pq.QuoteIdentifier(*gp.Database) - sp := strings.Join(gp.Privileges.ToStringSlice(), ",") - - *ql = append(*ql, - // REVOKE ANY MATCHING EXISTING PERMISSIONS - xsql.Query{String: fmt.Sprintf("REVOKE %s ON DATABASE %s FROM %s", - sp, - db, - ro, - )}, - - // GRANT REQUESTED PERMISSIONS - xsql.Query{String: fmt.Sprintf("GRANT %s ON DATABASE %s TO %s %s", - sp, - db, - ro, - withOption(gp.WithOption), - )}, - ) - return nil - } - return errors.New(errUnknownGrant) + q.String = query + return nil } -func deleteGrantQuery(gp v1alpha1.GrantParameters, q *xsql.Query) error { - gt, err := identifyGrantType(gp) - if err != nil { - return err +func deleteDefaultGrantQuery(gp v1alpha1.DefaultGrantParameters, q *xsql.Query) error { + roleName := pq.QuoteIdentifier(*gp.Role) + targetRoleName := pq.QuoteIdentifier(gp.TargetRole) + objectType := objectTypes[*gp.ObjectType] + + query := strings.TrimSpace(fmt.Sprintf( + "ALTER DEFAULT PRIVILEGES FOR ROLE %s %s REVOKE ALL ON %s ON %s TO %s %s", + targetRoleName, + inSchema(&gp), + strings.Join(gp.Privileges.ToStringSlice(), ","), + objectType, + roleName, + withOption(gp.WithOption), + )) + + q.String = query + return nil +} + +func haveToUpdate(currentGrants []string, specGrants []string) bool { + if len(currentGrants) != len(specGrants) { + return true } - ro := pq.QuoteIdentifier(*gp.Role) - - switch gt { - case roleMember: - q.String = fmt.Sprintf("REVOKE %s FROM %s", - pq.QuoteIdentifier(*gp.MemberOf), - ro, - ) - return nil - case roleDatabase: - q.String = fmt.Sprintf("REVOKE %s ON DATABASE %s FROM %s", - strings.Join(gp.Privileges.ToStringSlice(), ","), - pq.QuoteIdentifier(*gp.Database), - ro, - ) - return nil + sort.Strings(currentGrants) + sort.Strings(specGrants) + + for i, g := range currentGrants { + if g != specGrants[i] { + return true + } } - return errors.New(errUnknownGrant) -} + return false +} func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { - cr, ok := mg.(*v1alpha1.Grant) + cr, ok := mg.(*v1alpha1.DefaultGrant) if !ok { return managed.ExternalObservation{}, errors.New(errNotGrant) } @@ -319,17 +248,13 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex gp := cr.Spec.ForProvider var query xsql.Query - if err := selectGrantQuery(gp, &query); err != nil { + if err := selectDefaultGrantQuery(gp, &query); err != nil { return managed.ExternalObservation{}, err } - exists := false - - if err := c.db.Scan(ctx, query, &exists); err != nil { - return managed.ExternalObservation{}, errors.Wrap(err, errSelectGrant) - } - - if !exists { + var grants []string + err := c.db.Scan(ctx, query, &grants) + if xsql.IsNoRows(err) || len(grants) == 0 { return managed.ExternalObservation{ResourceExists: false}, nil } @@ -337,38 +262,48 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex cr.SetConditions(xpv1.Available()) return managed.ExternalObservation{ - ResourceExists: true, - ResourceUpToDate: true, ResourceLateInitialized: false, + // check that the list of grants matches the expected grants + // if not, the resource is not up to date. + // Becase create first revokes all grants and then grants them again, + // we can assume that if the grants are present, they are up to date. + ResourceExists: haveToUpdate(grants, gp.Privileges.ToStringSlice()), }, nil } func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) { - cr, ok := mg.(*v1alpha1.Grant) + cr, ok := mg.(*v1alpha1.DefaultGrant) if !ok { return managed.ExternalCreation{}, errors.New(errNotGrant) } - var queries []xsql.Query + var createQuery xsql.Query cr.SetConditions(xpv1.Creating()) + if err := createDefaultGrantQuery(cr.Spec.ForProvider, &createQuery); err != nil { + return managed.ExternalCreation{}, errors.Wrap(err, errCreateDefaultGrant) + } - if err := createGrantQueries(cr.Spec.ForProvider, &queries); err != nil { - return managed.ExternalCreation{}, errors.Wrap(err, errCreateGrant) + var deleteQuery xsql.Query + if err := deleteDefaultGrantQuery(cr.Spec.ForProvider, &deleteQuery); err != nil { + return managed.ExternalCreation{}, errors.Wrap(err, errCreateDefaultGrant) } - err := c.db.ExecTx(ctx, queries) - return managed.ExternalCreation{}, errors.Wrap(err, errCreateGrant) + err := c.db.ExecTx(ctx, []xsql.Query{ + deleteQuery, createQuery, + }) + return managed.ExternalCreation{}, errors.Wrap(err, errCreateDefaultGrant) } -func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { +func (c *external) Update( + ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { // Update is a no-op, as permissions are fully revoked and then granted in the Create function, - // inside a transaction. + // inside a transaction. Same approach as the grant resource. return managed.ExternalUpdate{}, nil } func (c *external) Delete(ctx context.Context, mg resource.Managed) error { - cr, ok := mg.(*v1alpha1.Grant) + cr, ok := mg.(*v1alpha1.DefaultGrant) if !ok { return errors.New(errNotGrant) } @@ -376,10 +311,10 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error { cr.SetConditions(xpv1.Deleting()) - err := deleteGrantQuery(cr.Spec.ForProvider, &query) + err := deleteDefaultGrantQuery(cr.Spec.ForProvider, &query) if err != nil { - return errors.Wrap(err, errRevokeGrant) + return errors.Wrap(err, errRevokeDefaultGrant) } - return errors.Wrap(c.db.Exec(ctx, query), errRevokeGrant) + return errors.Wrap(c.db.Exec(ctx, query), errRevokeDefaultGrant) } diff --git a/pkg/controller/postgresql/default_grant/reconciler_test.go b/pkg/controller/postgresql/default_grant/reconciler_test.go index eed4a81a..4f9c1f58 100644 --- a/pkg/controller/postgresql/default_grant/reconciler_test.go +++ b/pkg/controller/postgresql/default_grant/reconciler_test.go @@ -318,7 +318,7 @@ func TestObserve(t *testing.T) { }, }, want: want{ - err: errors.Wrap(errBoom, errSelectGrant), + err: errors.Wrap(errBoom, errSelectDefaultGrant), }, }, "SuccessRoleDb": { @@ -449,7 +449,7 @@ func TestCreate(t *testing.T) { }, }, want: want{ - err: errors.Wrap(errBoom, errCreateGrant), + err: errors.Wrap(errBoom, errCreateDefaultGrant), }, }, "Success": { @@ -591,7 +591,7 @@ func TestDelete(t *testing.T) { }, }, }, - want: errors.Wrap(errBoom, errRevokeGrant), + want: errors.Wrap(errBoom, errRevokeDefaultGrant), }, "Success": { reason: "No error should be returned if the grant was revoked", diff --git a/pkg/controller/sql.go b/pkg/controller/sql.go index cca80839..9ef94832 100644 --- a/pkg/controller/sql.go +++ b/pkg/controller/sql.go @@ -26,7 +26,7 @@ import ( "github.com/crossplane-contrib/provider-sql/pkg/controller/postgresql" ) -// Setup creates all PostgreSQL controllers with the supplied logger and adds +// Setup creates all controllers with the supplied logger and adds // them to the supplied manager. func Setup(mgr ctrl.Manager, l controller.Options) error { for _, setup := range []func(ctrl.Manager, controller.Options) error{ From 996d11b961fd073c9229f4c9dd3ad54fe299b702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Fern=C3=A1ndez=20Campo?= Date: Sat, 26 Oct 2024 14:20:23 +0200 Subject: [PATCH 04/17] updated generated files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joaquín Fernández Campo --- .../v1alpha1/zz_generated.deepcopy.go | 10 ++++ ...resql.sql.crossplane.io_defaultgrants.yaml | 56 +++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/apis/postgresql/v1alpha1/zz_generated.deepcopy.go b/apis/postgresql/v1alpha1/zz_generated.deepcopy.go index 904ef655..5b92a5af 100644 --- a/apis/postgresql/v1alpha1/zz_generated.deepcopy.go +++ b/apis/postgresql/v1alpha1/zz_generated.deepcopy.go @@ -244,6 +244,11 @@ func (in *DefaultGrantParameters) DeepCopyInto(out *DefaultGrantParameters) { *out = make(GrantPrivileges, len(*in)) copy(*out, *in) } + if in.ObjectType != nil { + in, out := &in.ObjectType, &out.ObjectType + *out = new(string) + **out = **in + } if in.WithOption != nil { in, out := &in.WithOption, &out.WithOption *out = new(GrantOption) @@ -259,6 +264,11 @@ func (in *DefaultGrantParameters) DeepCopyInto(out *DefaultGrantParameters) { *out = new(v1.Reference) (*in).DeepCopyInto(*out) } + if in.RoleSelector != nil { + in, out := &in.RoleSelector, &out.RoleSelector + *out = new(v1.Selector) + (*in).DeepCopyInto(*out) + } if in.Database != nil { in, out := &in.Database, &out.Database *out = new(string) diff --git a/package/crds/postgresql.sql.crossplane.io_defaultgrants.yaml b/package/crds/postgresql.sql.crossplane.io_defaultgrants.yaml index 1e20dfa5..3d7467c2 100644 --- a/package/crds/postgresql.sql.crossplane.io_defaultgrants.yaml +++ b/package/crds/postgresql.sql.crossplane.io_defaultgrants.yaml @@ -163,6 +163,14 @@ spec: type: string type: object type: object + objectType: + description: ObjectType to which the privileges are granted. + enum: + - table + - sequence + - function + - schema + type: string privileges: description: |- Privileges to be granted. @@ -210,6 +218,47 @@ spec: required: - name type: object + roleSelector: + description: RoleSelector selects a reference to a Role this default + grant is for. + properties: + matchControllerRef: + description: |- + MatchControllerRef ensures an object with the same controller reference + as the selecting object is selected. + type: boolean + matchLabels: + additionalProperties: + type: string + description: MatchLabels ensures an object with matching labels + is selected. + type: object + policy: + description: Policies for selection. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + type: object schema: description: Schema in which the default privileges are applied type: string @@ -289,6 +338,11 @@ spec: type: string type: object type: object + targetRole: + description: |- + TargetRole is the role who owns objects on which the default privileges are granted. + See https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html + type: string withOption: description: |- WithOption allows an option to be set on the grant. @@ -298,6 +352,8 @@ spec: - ADMIN - GRANT type: string + required: + - targetRole type: object managementPolicies: default: From 5e76862fddc13d84ea59d98e54684f7cdbb2fa90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Fern=C3=A1ndez=20Campo?= Date: Sat, 26 Oct 2024 21:39:18 +0200 Subject: [PATCH 05/17] fixed tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joaquín Fernández Campo --- .../v1alpha1/default_grant_types.go | 2 +- .../v1alpha1/zz_generated.deepcopy.go | 5 + .../postgresql/default_grant/reconciler.go | 69 +++-- .../default_grant/reconciler_test.go | 286 +++++++++--------- 4 files changed, 192 insertions(+), 170 deletions(-) diff --git a/apis/postgresql/v1alpha1/default_grant_types.go b/apis/postgresql/v1alpha1/default_grant_types.go index 0d328097..50b568c3 100644 --- a/apis/postgresql/v1alpha1/default_grant_types.go +++ b/apis/postgresql/v1alpha1/default_grant_types.go @@ -51,7 +51,7 @@ type DefaultGrantParameters struct { // TargetRole is the role who owns objects on which the default privileges are granted. // See https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html // +required - TargetRole string `json:"targetRole"` + TargetRole *string `json:"targetRole"` // ObjectType to which the privileges are granted. // +kubebuilder:validation:Enum=table;sequence;function;schema diff --git a/apis/postgresql/v1alpha1/zz_generated.deepcopy.go b/apis/postgresql/v1alpha1/zz_generated.deepcopy.go index 5b92a5af..2db9ce5f 100644 --- a/apis/postgresql/v1alpha1/zz_generated.deepcopy.go +++ b/apis/postgresql/v1alpha1/zz_generated.deepcopy.go @@ -244,6 +244,11 @@ func (in *DefaultGrantParameters) DeepCopyInto(out *DefaultGrantParameters) { *out = make(GrantPrivileges, len(*in)) copy(*out, *in) } + if in.TargetRole != nil { + in, out := &in.TargetRole, &out.TargetRole + *out = new(string) + **out = **in + } if in.ObjectType != nil { in, out := &in.ObjectType, &out.ObjectType *out = new(string) diff --git a/pkg/controller/postgresql/default_grant/reconciler.go b/pkg/controller/postgresql/default_grant/reconciler.go index e74c8d5a..752bf65f 100644 --- a/pkg/controller/postgresql/default_grant/reconciler.go +++ b/pkg/controller/postgresql/default_grant/reconciler.go @@ -48,11 +48,13 @@ const ( errNoSecretRef = "ProviderConfig does not reference a credentials Secret" errGetSecret = "cannot get credentials Secret" - errNotGrant = "managed resource is not a Grant custom resource" + errNotDefaultGrant = "managed resource is not a Grant custom resource" errSelectDefaultGrant = "cannot select default grant" errCreateDefaultGrant = "cannot create default grant" errRevokeDefaultGrant = "cannot revoke default grant" errNoRole = "role not passed or could not be resolved" + errNoTargetRole = "target role not passed or could not be resolved" + errNoObjectType = "object type not passed" errNoDatabase = "database not passed or could not be resolved" errNoPrivileges = "privileges not passed" errUnknownGrant = "cannot identify grant type based on passed params" @@ -94,7 +96,7 @@ type connector struct { func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { cr, ok := mg.(*v1alpha1.DefaultGrant) if !ok { - return nil, errors.New(errNotGrant) + return nil, errors.New(errNotDefaultGrant) } if err := c.usage.Track(ctx, mg); err != nil { @@ -181,10 +183,12 @@ func inSchema(params *v1alpha1.DefaultGrantParameters) string { return "" } -func createDefaultGrantQuery(gp v1alpha1.DefaultGrantParameters, q *xsql.Query) error { // nolint: gocyclo +func createDefaultGrantQuery(gp v1alpha1.DefaultGrantParameters, q *xsql.Query) { // nolint: gocyclo roleName := pq.QuoteIdentifier(*gp.Role) - targetRoleName := pq.QuoteIdentifier(gp.TargetRole) + + targetRoleName := pq.QuoteIdentifier(*gp.TargetRole) + objectType := objectTypes[*gp.ObjectType] query := strings.TrimSpace(fmt.Sprintf( @@ -198,12 +202,11 @@ func createDefaultGrantQuery(gp v1alpha1.DefaultGrantParameters, q *xsql.Query) )) q.String = query - return nil } -func deleteDefaultGrantQuery(gp v1alpha1.DefaultGrantParameters, q *xsql.Query) error { +func deleteDefaultGrantQuery(gp v1alpha1.DefaultGrantParameters, q *xsql.Query) { roleName := pq.QuoteIdentifier(*gp.Role) - targetRoleName := pq.QuoteIdentifier(gp.TargetRole) + targetRoleName := pq.QuoteIdentifier(*gp.TargetRole) objectType := objectTypes[*gp.ObjectType] query := strings.TrimSpace(fmt.Sprintf( @@ -217,12 +220,12 @@ func deleteDefaultGrantQuery(gp v1alpha1.DefaultGrantParameters, q *xsql.Query) )) q.String = query - return nil + return } -func haveToUpdate(currentGrants []string, specGrants []string) bool { +func matchingGrants(currentGrants []string, specGrants []string) bool { if len(currentGrants) != len(specGrants) { - return true + return false } sort.Strings(currentGrants) @@ -230,22 +233,30 @@ func haveToUpdate(currentGrants []string, specGrants []string) bool { for i, g := range currentGrants { if g != specGrants[i] { - return true + return false } } - return false + return true } func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { cr, ok := mg.(*v1alpha1.DefaultGrant) if !ok { - return managed.ExternalObservation{}, errors.New(errNotGrant) + return managed.ExternalObservation{}, errors.New(errNotDefaultGrant) } if cr.Spec.ForProvider.Role == nil { return managed.ExternalObservation{}, errors.New(errNoRole) } + if cr.Spec.ForProvider.TargetRole == nil { + return managed.ExternalObservation{}, errors.New(errNoTargetRole) + } + + if cr.Spec.ForProvider.ObjectType == nil { + return managed.ExternalObservation{}, errors.New(errNoObjectType) + } + gp := cr.Spec.ForProvider var query xsql.Query if err := selectDefaultGrantQuery(gp, &query); err != nil { @@ -254,40 +265,41 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex var grants []string err := c.db.Scan(ctx, query, &grants) - if xsql.IsNoRows(err) || len(grants) == 0 { + if err != nil && !xsql.IsNoRows(err) { + return managed.ExternalObservation{}, errors.Wrap(err, errSelectDefaultGrant) + } + if len(grants) == 0 { return managed.ExternalObservation{ResourceExists: false}, nil } // Grants have no way of being 'not up to date' - if they exist, they are up to date cr.SetConditions(xpv1.Available()) + resourceMatches := matchingGrants(grants, gp.Privileges.ToStringSlice()) return managed.ExternalObservation{ ResourceLateInitialized: false, // check that the list of grants matches the expected grants // if not, the resource is not up to date. - // Becase create first revokes all grants and then grants them again, + // Because create first revokes all grants and then grants them again, // we can assume that if the grants are present, they are up to date. - ResourceExists: haveToUpdate(grants, gp.Privileges.ToStringSlice()), + ResourceExists: resourceMatches, + ResourceUpToDate: resourceMatches, }, nil } func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) { cr, ok := mg.(*v1alpha1.DefaultGrant) if !ok { - return managed.ExternalCreation{}, errors.New(errNotGrant) + return managed.ExternalCreation{}, errors.New(errNotDefaultGrant) } - var createQuery xsql.Query - cr.SetConditions(xpv1.Creating()) - if err := createDefaultGrantQuery(cr.Spec.ForProvider, &createQuery); err != nil { - return managed.ExternalCreation{}, errors.Wrap(err, errCreateDefaultGrant) - } + + var createQuery xsql.Query + createDefaultGrantQuery(cr.Spec.ForProvider, &createQuery) var deleteQuery xsql.Query - if err := deleteDefaultGrantQuery(cr.Spec.ForProvider, &deleteQuery); err != nil { - return managed.ExternalCreation{}, errors.Wrap(err, errCreateDefaultGrant) - } + deleteDefaultGrantQuery(cr.Spec.ForProvider, &deleteQuery) err := c.db.ExecTx(ctx, []xsql.Query{ deleteQuery, createQuery, @@ -305,16 +317,13 @@ func (c *external) Update( func (c *external) Delete(ctx context.Context, mg resource.Managed) error { cr, ok := mg.(*v1alpha1.DefaultGrant) if !ok { - return errors.New(errNotGrant) + return errors.New(errNotDefaultGrant) } var query xsql.Query cr.SetConditions(xpv1.Deleting()) - err := deleteDefaultGrantQuery(cr.Spec.ForProvider, &query) - if err != nil { - return errors.Wrap(err, errRevokeDefaultGrant) - } + deleteDefaultGrantQuery(cr.Spec.ForProvider, &query) return errors.Wrap(c.db.Exec(ctx, query), errRevokeDefaultGrant) } diff --git a/pkg/controller/postgresql/default_grant/reconciler_test.go b/pkg/controller/postgresql/default_grant/reconciler_test.go index 4f9c1f58..3a5dc324 100644 --- a/pkg/controller/postgresql/default_grant/reconciler_test.go +++ b/pkg/controller/postgresql/default_grant/reconciler_test.go @@ -19,14 +19,11 @@ package default_grant import ( "context" "database/sql" - "fmt" - "sort" "testing" "github.com/crossplane-contrib/provider-sql/apis/postgresql/v1alpha1" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/lib/pq" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" "k8s.io/utils/ptr" @@ -89,11 +86,11 @@ func TestConnect(t *testing.T) { want error }{ "ErrNotGrant": { - reason: "An error should be returned if the managed resource is not a *Grant", + reason: "An error should be returned if the managed resource is not a *DefaultGrant", args: args{ mg: nil, }, - want: errors.New(errNotGrant), + want: errors.New(errNotDefaultGrant), }, "ErrTrackProviderConfigUsage": { reason: "An error should be returned if we can't track our ProviderConfig usage", @@ -101,7 +98,7 @@ func TestConnect(t *testing.T) { usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return errBoom }), }, args: args{ - mg: &v1alpha1.Grant{}, + mg: &v1alpha1.DefaultGrant{}, }, want: errors.Wrap(errBoom, errTrackPCUsage), }, @@ -114,8 +111,8 @@ func TestConnect(t *testing.T) { usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), }, args: args{ - mg: &v1alpha1.Grant{ - Spec: v1alpha1.GrantSpec{ + mg: &v1alpha1.DefaultGrant{ + Spec: v1alpha1.DefaultGrantSpec{ ResourceSpec: xpv1.ResourceSpec{ ProviderConfigReference: &xpv1.Reference{}, }, @@ -136,8 +133,8 @@ func TestConnect(t *testing.T) { usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), }, args: args{ - mg: &v1alpha1.Grant{ - Spec: v1alpha1.GrantSpec{ + mg: &v1alpha1.DefaultGrant{ + Spec: v1alpha1.DefaultGrantSpec{ ResourceSpec: xpv1.ResourceSpec{ ProviderConfigReference: &xpv1.Reference{}, }, @@ -163,8 +160,8 @@ func TestConnect(t *testing.T) { usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), }, args: args{ - mg: &v1alpha1.Grant{ - Spec: v1alpha1.GrantSpec{ + mg: &v1alpha1.DefaultGrant{ + Spec: v1alpha1.DefaultGrantSpec{ ResourceSpec: xpv1.ResourceSpec{ ProviderConfigReference: &xpv1.Reference{}, }, @@ -188,7 +185,7 @@ func TestConnect(t *testing.T) { func TestObserve(t *testing.T) { errBoom := errors.New("boom") - goa := v1alpha1.GrantOptionAdmin + // goa := v1alpha1.GrantOptionAdmin gog := v1alpha1.GrantOptionGrant type fields struct { @@ -212,32 +209,32 @@ func TestObserve(t *testing.T) { want want }{ "ErrNotGrant": { - reason: "An error should be returned if the managed resource is not a *Grant", + reason: "An error should be returned if the managed resource is not a *DefaultGrant", args: args{ mg: nil, }, want: want{ - err: errors.New(errNotGrant), + err: errors.New(errNotDefaultGrant), }, }, "SuccessNoGrant": { - reason: "We should return ResourceExists: false when no grant is found", + reason: "We should return ResourceExists: false when no default grant is found", fields: fields{ db: mockDB{ MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { - // Default value is false, so just return - bv := dest[0].(*bool) - *bv = false + // Default value is empty, so we don't need to do anything here return nil }, }, }, args: args{ - mg: &v1alpha1.Grant{ - Spec: v1alpha1.GrantSpec{ - ForProvider: v1alpha1.GrantParameters{ + mg: &v1alpha1.DefaultGrant{ + Spec: v1alpha1.DefaultGrantSpec{ + ForProvider: v1alpha1.DefaultGrantParameters{ Database: ptr.To("test-example"), Role: ptr.To("test-example"), + TargetRole: ptr.To("target-role"), + ObjectType: ptr.To("TABLE"), Privileges: v1alpha1.GrantPrivileges{"ALL"}, }, }, @@ -247,57 +244,57 @@ func TestObserve(t *testing.T) { o: managed.ExternalObservation{ResourceExists: false}, }, }, - "AllMapsToExpandedPrivileges": { - reason: "We expand ALL to CREATE, TEMPORARY, CONNECT when checking for existing grants", - fields: fields{ - db: mockDB{ - MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { - privileges := q.Parameters[3] - - privs, ok := privileges.(*pq.StringArray) - if !ok { - return fmt.Errorf("expected Scan parameter to be pq.StringArray, got %T", privileges) - } - - // The order is not guaranteed, so sort the slices before comparing - sort.Strings(*privs) - - // Return if there's a diff between the expected and actual privileges - diff := cmp.Diff(&pq.StringArray{"CONNECT", "CREATE", "TEMPORARY"}, privileges) - - bv := dest[0].(*bool) - *bv = diff == "" - - // Extra logging in case this test is going to fail - if diff != "" { - t.Logf("expected empty diff, got: %s", diff) - } - - return nil - }, - }, - }, - args: args{ - mg: &v1alpha1.Grant{ - Spec: v1alpha1.GrantSpec{ - ForProvider: v1alpha1.GrantParameters{ - Database: ptr.To("test-example"), - Role: ptr.To("test-example"), - Privileges: v1alpha1.GrantPrivileges{"ALL"}, - }, - }, - }, - }, - want: want{ - o: managed.ExternalObservation{ - ResourceExists: true, - ResourceUpToDate: true, - }, - err: nil, - }, - }, + // "AllMapsToExpandedPrivileges": { + // reason: "We expand ALL to CREATE, TEMPORARY, CONNECT when checking for existing grants", + // fields: fields{ + // db: mockDB{ + // MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { + // privileges := q.Parameters[3] + + // privs, ok := privileges.(*pq.StringArray) + // if !ok { + // return fmt.Errorf("expected Scan parameter to be pq.StringArray, got %T", privileges) + // } + + // // The order is not guaranteed, so sort the slices before comparing + // sort.Strings(*privs) + + // // Return if there's a diff between the expected and actual privileges + // diff := cmp.Diff(&pq.StringArray{"CONNECT", "CREATE", "TEMPORARY"}, privileges) + + // bv := dest[0].(*bool) + // *bv = diff == "" + + // // Extra logging in case this test is going to fail + // if diff != "" { + // t.Logf("expected empty diff, got: %s", diff) + // } + + // return nil + // }, + // }, + // }, + // args: args{ + // mg: &v1alpha1.DefaultGrant{ + // Spec: v1alpha1.DefaultGrantSpec{ + // ForProvider: v1alpha1.DefaultGrantParameters{ + // Database: ptr.To("test-example"), + // Role: ptr.To("test-example"), + // Privileges: v1alpha1.GrantPrivileges{"ALL"}, + // }, + // }, + // }, + // }, + // want: want{ + // o: managed.ExternalObservation{ + // ResourceExists: true, + // ResourceUpToDate: true, + // }, + // err: nil, + // }, + // }, "ErrSelectGrant": { - reason: "We should return any errors encountered while trying to show the grant", + reason: "We should return any errors encountered while trying to show the default grant", fields: fields{ db: mockDB{ MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { @@ -306,11 +303,13 @@ func TestObserve(t *testing.T) { }, }, args: args{ - mg: &v1alpha1.Grant{ - Spec: v1alpha1.GrantSpec{ - ForProvider: v1alpha1.GrantParameters{ + mg: &v1alpha1.DefaultGrant{ + Spec: v1alpha1.DefaultGrantSpec{ + ForProvider: v1alpha1.DefaultGrantParameters{ Database: ptr.To("test-example"), Role: ptr.To("test-example"), + TargetRole: ptr.To("target-role"), + ObjectType: ptr.To("TABLE"), Privileges: v1alpha1.GrantPrivileges{"CONNECT", "TEMPORARY"}, WithOption: &gog, }, @@ -321,24 +320,26 @@ func TestObserve(t *testing.T) { err: errors.Wrap(errBoom, errSelectDefaultGrant), }, }, - "SuccessRoleDb": { - reason: "We should return no error if we can find our role-db grant", + "DefaultGrantFound": { + reason: "We should return no error if we can find the right permissions in the default grant", fields: fields{ db: mockDB{ MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { - bv := dest[0].(*bool) - *bv = true + bv := dest[0].(*[]string) + *bv = []string{"SELECT", "UPDATE"} return nil }, }, }, args: args{ - mg: &v1alpha1.Grant{ - Spec: v1alpha1.GrantSpec{ - ForProvider: v1alpha1.GrantParameters{ + mg: &v1alpha1.DefaultGrant{ + Spec: v1alpha1.DefaultGrantSpec{ + ForProvider: v1alpha1.DefaultGrantParameters{ Database: ptr.To("testdb"), Role: ptr.To("testrole"), - Privileges: v1alpha1.GrantPrivileges{"ALL"}, + TargetRole: ptr.To("target-role"), + ObjectType: ptr.To("TABLE"), + Privileges: v1alpha1.GrantPrivileges{"SELECT", "UPDATE"}, WithOption: &gog, }, }, @@ -352,36 +353,35 @@ func TestObserve(t *testing.T) { err: nil, }, }, - "SuccessRoleMembership": { - reason: "We should return no error if we can find our role-membership grant", - fields: fields{ - db: mockDB{ - MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { - bv := dest[0].(*bool) - *bv = true - return nil - }, - }, - }, - args: args{ - mg: &v1alpha1.Grant{ - Spec: v1alpha1.GrantSpec{ - ForProvider: v1alpha1.GrantParameters{ - Role: ptr.To("testrole"), - MemberOf: ptr.To("parentrole"), - WithOption: &goa, - }, - }, - }, - }, - want: want{ - o: managed.ExternalObservation{ - ResourceExists: true, - ResourceUpToDate: true, - }, - err: nil, - }, - }, + // "SuccessRoleMembership": { + // reason: "We should return no error if we can find our role-membership grant", + // fields: fields{ + // db: mockDB{ + // MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { + // bv := dest[0].(*bool) + // *bv = true + // return nil + // }, + // }, + // }, + // args: args{ + // mg: &v1alpha1.DefaultGrant{ + // Spec: v1alpha1.DefaultGrantSpec{ + // ForProvider: v1alpha1.DefaultGrantParameters{ + // Role: ptr.To("testrole"), + // WithOption: &goa, + // }, + // }, + // }, + // }, + // want: want{ + // o: managed.ExternalObservation{ + // ResourceExists: true, + // ResourceUpToDate: true, + // }, + // err: nil, + // }, + // }, } for name, tc := range cases { @@ -422,27 +422,29 @@ func TestCreate(t *testing.T) { want want }{ "ErrNotGrant": { - reason: "An error should be returned if the managed resource is not a *Grant", + reason: "An error should be returned if the managed resource is not a *DefaultGrant", args: args{ mg: nil, }, want: want{ - err: errors.New(errNotGrant), + err: errors.New(errNotDefaultGrant), }, }, "ErrExec": { - reason: "Any errors encountered while creating the grant should be returned", + reason: "Any errors encountered while creating the default grant should be returned", fields: fields{ db: &mockDB{ MockExecTx: func(ctx context.Context, ql []xsql.Query) error { return errBoom }, }, }, args: args{ - mg: &v1alpha1.Grant{ - Spec: v1alpha1.GrantSpec{ - ForProvider: v1alpha1.GrantParameters{ + mg: &v1alpha1.DefaultGrant{ + Spec: v1alpha1.DefaultGrantSpec{ + ForProvider: v1alpha1.DefaultGrantParameters{ Database: ptr.To("test-example"), Role: ptr.To("test-example"), + TargetRole: ptr.To("target-role"), + ObjectType: ptr.To("TABLE"), Privileges: v1alpha1.GrantPrivileges{"ALL"}, }, }, @@ -453,19 +455,21 @@ func TestCreate(t *testing.T) { }, }, "Success": { - reason: "No error should be returned when we successfully create a grant", + reason: "No error should be returned when we successfully create a default grant", fields: fields{ db: &mockDB{ MockExecTx: func(ctx context.Context, ql []xsql.Query) error { return nil }, }, }, args: args{ - mg: &v1alpha1.Grant{ - Spec: v1alpha1.GrantSpec{ - ForProvider: v1alpha1.GrantParameters{ + mg: &v1alpha1.DefaultGrant{ + Spec: v1alpha1.DefaultGrantSpec{ + ForProvider: v1alpha1.DefaultGrantParameters{ Database: ptr.To("test-example"), Role: ptr.To("test-example"), + TargetRole: ptr.To("target-role"), Privileges: v1alpha1.GrantPrivileges{"ALL"}, + ObjectType: ptr.To("TABLE"), }, }, }, @@ -512,11 +516,11 @@ func TestUpdate(t *testing.T) { want want }{ "ErrNoOp": { - reason: "Update is a no-op, make sure we dont throw an error *Grant", + reason: "Update is a no-op, make sure we dont throw an error *DefaultGrant", args: args{ - mg: &v1alpha1.Grant{ - Spec: v1alpha1.GrantSpec{ - ForProvider: v1alpha1.GrantParameters{ + mg: &v1alpha1.DefaultGrant{ + Spec: v1alpha1.DefaultGrantSpec{ + ForProvider: v1alpha1.DefaultGrantParameters{ Database: ptr.To("test-example"), Role: ptr.To("test-example"), Privileges: v1alpha1.GrantPrivileges{"ALL"}, @@ -564,15 +568,15 @@ func TestDelete(t *testing.T) { args args want error }{ - "ErrNotGrant": { - reason: "An error should be returned if the managed resource is not a *Grant", + "ErrNotDefaultGrant": { + reason: "An error should be returned if the managed resource is not a *DefaultGrant", args: args{ mg: nil, }, - want: errors.New(errNotGrant), + want: errors.New(errNotDefaultGrant), }, - "ErrDropGrant": { - reason: "Errors dropping a grant should be returned", + "ErrDropDefaultGrant": { + reason: "Errors dropping a default grant should be returned", fields: fields{ db: &mockDB{ MockExec: func(ctx context.Context, q xsql.Query) error { @@ -581,12 +585,14 @@ func TestDelete(t *testing.T) { }, }, args: args{ - mg: &v1alpha1.Grant{ - Spec: v1alpha1.GrantSpec{ - ForProvider: v1alpha1.GrantParameters{ + mg: &v1alpha1.DefaultGrant{ + Spec: v1alpha1.DefaultGrantSpec{ + ForProvider: v1alpha1.DefaultGrantParameters{ Database: ptr.To("test-example"), Role: ptr.To("test-example"), Privileges: v1alpha1.GrantPrivileges{"ALL"}, + ObjectType: ptr.To("SEQUENCE"), + TargetRole: ptr.To("target-role"), }, }, }, @@ -594,14 +600,16 @@ func TestDelete(t *testing.T) { want: errors.Wrap(errBoom, errRevokeDefaultGrant), }, "Success": { - reason: "No error should be returned if the grant was revoked", + reason: "No error should be returned if the default grant was revoked", args: args{ - mg: &v1alpha1.Grant{ - Spec: v1alpha1.GrantSpec{ - ForProvider: v1alpha1.GrantParameters{ + mg: &v1alpha1.DefaultGrant{ + Spec: v1alpha1.DefaultGrantSpec{ + ForProvider: v1alpha1.DefaultGrantParameters{ Database: ptr.To("test-example"), Role: ptr.To("test-example"), Privileges: v1alpha1.GrantPrivileges{"ALL"}, + ObjectType: ptr.To("SEQUENCE"), + TargetRole: ptr.To("target-role"), }, }, }, From c77b82655bc67f22453e675c528fe0e07ae2a23b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Fern=C3=A1ndez=20Campo?= Date: Sat, 26 Oct 2024 21:39:27 +0200 Subject: [PATCH 06/17] added an example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joaquín Fernández Campo --- examples/postgresql/default_grant.yaml | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 examples/postgresql/default_grant.yaml diff --git a/examples/postgresql/default_grant.yaml b/examples/postgresql/default_grant.yaml new file mode 100644 index 00000000..df1b919c --- /dev/null +++ b/examples/postgresql/default_grant.yaml @@ -0,0 +1,42 @@ +apiVersion: postgresql.sql.crossplane.io/v1alpha1 +kind: DefaultGrant +metadata: + name: default-grant-role-1-on-database +spec: + forProvider: + privileges: + - SELECT + roleRef: + name: reader-role + targetRoleRef: + name: example-role + schemaRef: + name: example + databaseRef: + name: example +--- +apiVersion: postgresql.sql.crossplane.io/v1alpha1 +kind: Grant +metadata: + name: example-grant-role-2-on-database +spec: + forProvider: + privileges: + - CONNECT + - TEMPORARY + roleRef: + name: example-role + databaseRef: + name: example +--- +apiVersion: postgresql.sql.crossplane.io/v1alpha1 +kind: Grant +metadata: + name: example-grant-role-membership +spec: + forProvider: + withOption: ADMIN + roleRef: + name: example-role + memberOfRef: + name: parent-role From 2331a1cd381a91e0c139bc680e37d4090e995102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Fern=C3=A1ndez=20Campo?= Date: Sat, 26 Oct 2024 21:51:37 +0200 Subject: [PATCH 07/17] lint issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joaquín Fernández Campo --- .../postgresql/default_grant/reconciler.go | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/pkg/controller/postgresql/default_grant/reconciler.go b/pkg/controller/postgresql/default_grant/reconciler.go index 752bf65f..0657df6d 100644 --- a/pkg/controller/postgresql/default_grant/reconciler.go +++ b/pkg/controller/postgresql/default_grant/reconciler.go @@ -59,10 +59,6 @@ const ( errNoPrivileges = "privileges not passed" errUnknownGrant = "cannot identify grant type based on passed params" - errInvalidParams = "invalid parameters for grant type %s" - - errMemberOfWithDatabaseOrPrivileges = "cannot set privileges or database in the same grant as memberOf" - maxConcurrency = 5 ) @@ -133,13 +129,6 @@ type external struct { kube client.Client } -type grantType string - -const ( - roleMember grantType = "ROLE_MEMBER" - roleDatabase grantType = "ROLE_DATABASE" -) - var ( objectTypes = map[string]string{ "table": "r", @@ -150,8 +139,7 @@ var ( } ) -func selectDefaultGrantQuery(gp v1alpha1.DefaultGrantParameters, q *xsql.Query) error { - +func selectDefaultGrantQuery(gp v1alpha1.DefaultGrantParameters, q *xsql.Query) { sqlString := ` select distinct(default_acl.privilege_type) from pg_roles r @@ -166,7 +154,6 @@ func selectDefaultGrantQuery(gp v1alpha1.DefaultGrantParameters, q *xsql.Query) *gp.Role, } - return nil } func withOption(option *v1alpha1.GrantOption) string { @@ -220,7 +207,7 @@ func deleteDefaultGrantQuery(gp v1alpha1.DefaultGrantParameters, q *xsql.Query) )) q.String = query - return + } func matchingGrants(currentGrants []string, specGrants []string) bool { @@ -259,9 +246,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex gp := cr.Spec.ForProvider var query xsql.Query - if err := selectDefaultGrantQuery(gp, &query); err != nil { - return managed.ExternalObservation{}, err - } + selectDefaultGrantQuery(gp, &query) var grants []string err := c.db.Scan(ctx, query, &grants) From 38e702b78b92acaae231e790a0f525d825d58df3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Fern=C3=A1ndez=20Campo?= Date: Sun, 27 Oct 2024 08:54:29 +0100 Subject: [PATCH 08/17] added DefaultGrant to the crossplane package definition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joaquín Fernández Campo --- package/crossplane.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/package/crossplane.yaml b/package/crossplane.yaml index c146ae27..69f3e4a6 100644 --- a/package/crossplane.yaml +++ b/package/crossplane.yaml @@ -29,3 +29,4 @@ metadata: friendly-kind-name.meta.crossplane.io/grant.postgresql.sql.crossplane.io: Grant friendly-kind-name.meta.crossplane.io/role.postgresql.sql.crossplane.io: Role friendly-kind-name.meta.crossplane.io/user.mysql.sql.crossplane.io: User + friendly-kind-name.meta.crossplane.io/defaultgrant.postgresql.sql.crossplane.io: DefaultGrant From b2a01649096c67f9359e142d1a3a0a267ce7cee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Fern=C3=A1ndez=20Campo?= Date: Sun, 27 Oct 2024 09:23:28 +0100 Subject: [PATCH 09/17] renamed from DefaultGrants to DefaultPrivileges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joaquín Fernández Campo --- README.md | 2 +- ...t_types.go => default_privileges_types.go} | 129 +++++++++--------- apis/postgresql/v1alpha1/register.go | 10 +- .../v1alpha1/zz_generated.deepcopy.go | 46 +++---- .../v1alpha1/zz_generated.managed.go | 48 +++---- .../v1alpha1/zz_generated.managedlist.go | 4 +- examples/postgresql/default_grant.yaml | 42 ------ examples/postgresql/default_privileges.yaml | 17 +++ ....sql.crossplane.io_defaultprivileges.yaml} | 23 ++-- package/crossplane.yaml | 2 +- .../reconciler.go | 62 ++++----- .../reconciler_test.go | 108 +++++++-------- pkg/controller/postgresql/postgresql.go | 2 + 13 files changed, 234 insertions(+), 261 deletions(-) rename apis/postgresql/v1alpha1/{default_grant_types.go => default_privileges_types.go} (55%) delete mode 100644 examples/postgresql/default_grant.yaml create mode 100644 examples/postgresql/default_privileges.yaml rename package/crds/{postgresql.sql.crossplane.io_defaultgrants.yaml => postgresql.sql.crossplane.io_defaultprivileges.yaml} (98%) rename pkg/controller/postgresql/{default_grant => default_privileges}/reconciler.go (82%) rename pkg/controller/postgresql/{default_grant => default_privileges}/reconciler_test.go (86%) diff --git a/README.md b/README.md index 51ef52e4..6d98a77c 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Check the example: 2. Create managed resources for your SQL server flavor: - **MySQL**: `Database`, `Grant`, `User` (See [the examples](examples/mysql)) - - **PostgreSQL**: `Database`, `Grant`, `DefaultGrant`, `Extension`, `Role` (See [the examples](examples/postgresql)) + - **PostgreSQL**: `Database`, `Grant`, `DefaultPrivileges`, `Extension`, `Role` (See [the examples](examples/postgresql)) - **MSSQL**: `Database`, `Grant`, `User` (See [the examples](examples/mssql)) [crossplane]: https://crossplane.io diff --git a/apis/postgresql/v1alpha1/default_grant_types.go b/apis/postgresql/v1alpha1/default_privileges_types.go similarity index 55% rename from apis/postgresql/v1alpha1/default_grant_types.go rename to apis/postgresql/v1alpha1/default_privileges_types.go index 50b568c3..067df57b 100644 --- a/apis/postgresql/v1alpha1/default_grant_types.go +++ b/apis/postgresql/v1alpha1/default_privileges_types.go @@ -1,13 +1,8 @@ package v1alpha1 import ( - "context" - xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" - "github.com/crossplane/crossplane-runtime/pkg/reference" - "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" ) // +kubebuilder:object:root=true @@ -18,31 +13,31 @@ import ( // +kubebuilder:printcolumn:name="SYNCED",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status" // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" // +kubebuilder:printcolumn:name="ROLE",type="string",JSONPath=".spec.forProvider.role" -// +kubebuilder:printcolumn:name="MEMBER OF",type="string",JSONPath=".spec.forProvider.memberOf" +// +kubebuilder:printcolumn:name="SCHEMA",type="string",JSONPath=".spec.forProvider.schema" // +kubebuilder:printcolumn:name="DATABASE",type="string",JSONPath=".spec.forProvider.database" // +kubebuilder:printcolumn:name="PRIVILEGES",type="string",JSONPath=".spec.forProvider.privileges" // +kubebuilder:resource:scope=Cluster,categories={crossplane,managed,sql} -type DefaultGrant struct { +type DefaultPrivileges struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec DefaultGrantSpec `json:"spec"` - Status DefaultGrantStatus `json:"status,omitempty"` + Spec DefaultPrivilegesSpec `json:"spec"` + Status DefaultPrivilegesStatus `json:"status,omitempty"` } -// A DefaultGrantSpec defines the desired state of a Default Grant. -type DefaultGrantSpec struct { +// A DefaultPrivilegesSpec defines the desired state of a Default Grant. +type DefaultPrivilegesSpec struct { xpv1.ResourceSpec `json:",inline"` - ForProvider DefaultGrantParameters `json:"forProvider"` + ForProvider DefaultPrivilegesParameters `json:"forProvider"` } -// A DefaultGrantStatus represents the observed state of a Grant. -type DefaultGrantStatus struct { +// A DefaultPrivilegesStatus represents the observed state of a Grant. +type DefaultPrivilegesStatus struct { xpv1.ResourceStatus `json:",inline"` } -// DefaultGrantParameters defines the desired state of a Default Grant. -type DefaultGrantParameters struct { +// DefaultPrivilegesParameters defines the desired state of a Default Grant. +type DefaultPrivilegesParameters struct { // Privileges to be granted. // See https://www.postgresql.org/docs/current/sql-grant.html for available privileges. // +optional @@ -110,58 +105,58 @@ type DefaultGrantParameters struct { // +kubebuilder:object:root=true -// DefaultGrantList contains a list of DefaultGrant. -type DefaultGrantList struct { +// DefaultPrivilegesList contains a list of DefaultPrivileges. +type DefaultPrivilegesList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []DefaultGrant `json:"items"` + Items []DefaultPrivileges `json:"items"` } -// ResolveReferences of this DefaultGrant. -func (mg *DefaultGrant) ResolveReferences(ctx context.Context, c client.Reader) error { - r := reference.NewAPIResolver(c, mg) - - // Resolve spec.forProvider.database - rsp, err := r.Resolve(ctx, reference.ResolutionRequest{ - CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Database), - Reference: mg.Spec.ForProvider.DatabaseRef, - Selector: mg.Spec.ForProvider.DatabaseSelector, - To: reference.To{Managed: &Database{}, List: &DatabaseList{}}, - Extract: reference.ExternalName(), - }) - if err != nil { - return errors.Wrap(err, "spec.forProvider.database") - } - mg.Spec.ForProvider.Database = reference.ToPtrValue(rsp.ResolvedValue) - mg.Spec.ForProvider.DatabaseRef = rsp.ResolvedReference - - // Resolve spec.forProvider.role - rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ - CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Role), - Reference: mg.Spec.ForProvider.RoleRef, - Selector: mg.Spec.ForProvider.RoleSelector, - To: reference.To{Managed: &Role{}, List: &RoleList{}}, - Extract: reference.ExternalName(), - }) - if err != nil { - return errors.Wrap(err, "spec.forProvider.role") - } - mg.Spec.ForProvider.Role = reference.ToPtrValue(rsp.ResolvedValue) - mg.Spec.ForProvider.RoleRef = rsp.ResolvedReference - - // Resolve spec.forProvider.schema - rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ - CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Schema), - Reference: mg.Spec.ForProvider.SchemaRef, - Selector: mg.Spec.ForProvider.SchemaSelector, - To: reference.To{Managed: &Role{}, List: &RoleList{}}, - Extract: reference.ExternalName(), - }) - if err != nil { - return errors.Wrap(err, "spec.forProvider.schema") - } - mg.Spec.ForProvider.Schema = reference.ToPtrValue(rsp.ResolvedValue) - mg.Spec.ForProvider.SchemaRef = rsp.ResolvedReference - - return nil -} +// ResolveReferences of this DefaultPrivileges. +// func (mg *DefaultPrivileges) ResolveReferences(ctx context.Context, c client.Reader) error { +// r := reference.NewAPIResolver(c, mg) + +// // Resolve spec.forProvider.database +// rsp, err := r.Resolve(ctx, reference.ResolutionRequest{ +// CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Database), +// Reference: mg.Spec.ForProvider.DatabaseRef, +// Selector: mg.Spec.ForProvider.DatabaseSelector, +// To: reference.To{Managed: &Database{}, List: &DatabaseList{}}, +// Extract: reference.ExternalName(), +// }) +// if err != nil { +// return errors.Wrap(err, "spec.forProvider.database") +// } +// mg.Spec.ForProvider.Database = reference.ToPtrValue(rsp.ResolvedValue) +// mg.Spec.ForProvider.DatabaseRef = rsp.ResolvedReference + +// // Resolve spec.forProvider.role +// rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ +// CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Role), +// Reference: mg.Spec.ForProvider.RoleRef, +// Selector: mg.Spec.ForProvider.RoleSelector, +// To: reference.To{Managed: &Role{}, List: &RoleList{}}, +// Extract: reference.ExternalName(), +// }) +// if err != nil { +// return errors.Wrap(err, "spec.forProvider.role") +// } +// mg.Spec.ForProvider.Role = reference.ToPtrValue(rsp.ResolvedValue) +// mg.Spec.ForProvider.RoleRef = rsp.ResolvedReference + +// // Resolve spec.forProvider.schema +// rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ +// CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Schema), +// Reference: mg.Spec.ForProvider.SchemaRef, +// Selector: mg.Spec.ForProvider.SchemaSelector, +// To: reference.To{Managed: &Role{}, List: &RoleList{}}, +// Extract: reference.ExternalName(), +// }) +// if err != nil { +// return errors.Wrap(err, "spec.forProvider.schema") +// } +// mg.Spec.ForProvider.Schema = reference.ToPtrValue(rsp.ResolvedValue) +// mg.Spec.ForProvider.SchemaRef = rsp.ResolvedReference + +// return nil +// } diff --git a/apis/postgresql/v1alpha1/register.go b/apis/postgresql/v1alpha1/register.go index bae29c8d..33c242bb 100644 --- a/apis/postgresql/v1alpha1/register.go +++ b/apis/postgresql/v1alpha1/register.go @@ -90,12 +90,12 @@ var ( GrantGroupVersionKind = SchemeGroupVersion.WithKind(GrantKind) ) -// DefaultGrant type metadata. +// DefaultPrivileges type metadata. var ( - DefaultGrantKind = reflect.TypeOf(DefaultGrant{}).Name() - DefaultGrantGroupKind = schema.GroupKind{Group: Group, Kind: DefaultGrantKind}.String() - DefaultGrantKindAPIVersion = DefaultGrantKind + "." + SchemeGroupVersion.String() - DefaultGrantGroupVersionKind = SchemeGroupVersion.WithKind(DefaultGrantKind) + DefaultPrivilegesKind = reflect.TypeOf(DefaultPrivileges{}).Name() + DefaultPrivilegesGroupKind = schema.GroupKind{Group: Group, Kind: DefaultPrivilegesKind}.String() + DefaultPrivilegesKindAPIVersion = DefaultPrivilegesKind + "." + SchemeGroupVersion.String() + DefaultPrivilegesGroupVersionKind = SchemeGroupVersion.WithKind(DefaultPrivilegesKind) ) // Schema type metadata. diff --git a/apis/postgresql/v1alpha1/zz_generated.deepcopy.go b/apis/postgresql/v1alpha1/zz_generated.deepcopy.go index 2db9ce5f..f5eb901a 100644 --- a/apis/postgresql/v1alpha1/zz_generated.deepcopy.go +++ b/apis/postgresql/v1alpha1/zz_generated.deepcopy.go @@ -178,7 +178,7 @@ func (in *DatabaseStatus) DeepCopy() *DatabaseStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DefaultGrant) DeepCopyInto(out *DefaultGrant) { +func (in *DefaultPrivileges) DeepCopyInto(out *DefaultPrivileges) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) @@ -186,18 +186,18 @@ func (in *DefaultGrant) DeepCopyInto(out *DefaultGrant) { in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultGrant. -func (in *DefaultGrant) DeepCopy() *DefaultGrant { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultPrivileges. +func (in *DefaultPrivileges) DeepCopy() *DefaultPrivileges { if in == nil { return nil } - out := new(DefaultGrant) + out := new(DefaultPrivileges) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DefaultGrant) DeepCopyObject() runtime.Object { +func (in *DefaultPrivileges) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -205,31 +205,31 @@ func (in *DefaultGrant) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DefaultGrantList) DeepCopyInto(out *DefaultGrantList) { +func (in *DefaultPrivilegesList) DeepCopyInto(out *DefaultPrivilegesList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]DefaultGrant, len(*in)) + *out = make([]DefaultPrivileges, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultGrantList. -func (in *DefaultGrantList) DeepCopy() *DefaultGrantList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultPrivilegesList. +func (in *DefaultPrivilegesList) DeepCopy() *DefaultPrivilegesList { if in == nil { return nil } - out := new(DefaultGrantList) + out := new(DefaultPrivilegesList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DefaultGrantList) DeepCopyObject() runtime.Object { +func (in *DefaultPrivilegesList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -237,7 +237,7 @@ func (in *DefaultGrantList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DefaultGrantParameters) DeepCopyInto(out *DefaultGrantParameters) { +func (in *DefaultPrivilegesParameters) DeepCopyInto(out *DefaultPrivilegesParameters) { *out = *in if in.Privileges != nil { in, out := &in.Privileges, &out.Privileges @@ -306,45 +306,45 @@ func (in *DefaultGrantParameters) DeepCopyInto(out *DefaultGrantParameters) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultGrantParameters. -func (in *DefaultGrantParameters) DeepCopy() *DefaultGrantParameters { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultPrivilegesParameters. +func (in *DefaultPrivilegesParameters) DeepCopy() *DefaultPrivilegesParameters { if in == nil { return nil } - out := new(DefaultGrantParameters) + out := new(DefaultPrivilegesParameters) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DefaultGrantSpec) DeepCopyInto(out *DefaultGrantSpec) { +func (in *DefaultPrivilegesSpec) DeepCopyInto(out *DefaultPrivilegesSpec) { *out = *in in.ResourceSpec.DeepCopyInto(&out.ResourceSpec) in.ForProvider.DeepCopyInto(&out.ForProvider) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultGrantSpec. -func (in *DefaultGrantSpec) DeepCopy() *DefaultGrantSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultPrivilegesSpec. +func (in *DefaultPrivilegesSpec) DeepCopy() *DefaultPrivilegesSpec { if in == nil { return nil } - out := new(DefaultGrantSpec) + out := new(DefaultPrivilegesSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DefaultGrantStatus) DeepCopyInto(out *DefaultGrantStatus) { +func (in *DefaultPrivilegesStatus) DeepCopyInto(out *DefaultPrivilegesStatus) { *out = *in in.ResourceStatus.DeepCopyInto(&out.ResourceStatus) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultGrantStatus. -func (in *DefaultGrantStatus) DeepCopy() *DefaultGrantStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultPrivilegesStatus. +func (in *DefaultPrivilegesStatus) DeepCopy() *DefaultPrivilegesStatus { if in == nil { return nil } - out := new(DefaultGrantStatus) + out := new(DefaultPrivilegesStatus) in.DeepCopyInto(out) return out } diff --git a/apis/postgresql/v1alpha1/zz_generated.managed.go b/apis/postgresql/v1alpha1/zz_generated.managed.go index 3f1acd1c..2f2b66ab 100644 --- a/apis/postgresql/v1alpha1/zz_generated.managed.go +++ b/apis/postgresql/v1alpha1/zz_generated.managed.go @@ -79,63 +79,63 @@ func (mg *Database) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) mg.Spec.WriteConnectionSecretToReference = r } -// GetCondition of this DefaultGrant. -func (mg *DefaultGrant) GetCondition(ct xpv1.ConditionType) xpv1.Condition { +// GetCondition of this DefaultPrivileges. +func (mg *DefaultPrivileges) GetCondition(ct xpv1.ConditionType) xpv1.Condition { return mg.Status.GetCondition(ct) } -// GetDeletionPolicy of this DefaultGrant. -func (mg *DefaultGrant) GetDeletionPolicy() xpv1.DeletionPolicy { +// GetDeletionPolicy of this DefaultPrivileges. +func (mg *DefaultPrivileges) GetDeletionPolicy() xpv1.DeletionPolicy { return mg.Spec.DeletionPolicy } -// GetManagementPolicies of this DefaultGrant. -func (mg *DefaultGrant) GetManagementPolicies() xpv1.ManagementPolicies { +// GetManagementPolicies of this DefaultPrivileges. +func (mg *DefaultPrivileges) GetManagementPolicies() xpv1.ManagementPolicies { return mg.Spec.ManagementPolicies } -// GetProviderConfigReference of this DefaultGrant. -func (mg *DefaultGrant) GetProviderConfigReference() *xpv1.Reference { +// GetProviderConfigReference of this DefaultPrivileges. +func (mg *DefaultPrivileges) GetProviderConfigReference() *xpv1.Reference { return mg.Spec.ProviderConfigReference } -// GetPublishConnectionDetailsTo of this DefaultGrant. -func (mg *DefaultGrant) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { +// GetPublishConnectionDetailsTo of this DefaultPrivileges. +func (mg *DefaultPrivileges) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { return mg.Spec.PublishConnectionDetailsTo } -// GetWriteConnectionSecretToReference of this DefaultGrant. -func (mg *DefaultGrant) GetWriteConnectionSecretToReference() *xpv1.SecretReference { +// GetWriteConnectionSecretToReference of this DefaultPrivileges. +func (mg *DefaultPrivileges) GetWriteConnectionSecretToReference() *xpv1.SecretReference { return mg.Spec.WriteConnectionSecretToReference } -// SetConditions of this DefaultGrant. -func (mg *DefaultGrant) SetConditions(c ...xpv1.Condition) { +// SetConditions of this DefaultPrivileges. +func (mg *DefaultPrivileges) SetConditions(c ...xpv1.Condition) { mg.Status.SetConditions(c...) } -// SetDeletionPolicy of this DefaultGrant. -func (mg *DefaultGrant) SetDeletionPolicy(r xpv1.DeletionPolicy) { +// SetDeletionPolicy of this DefaultPrivileges. +func (mg *DefaultPrivileges) SetDeletionPolicy(r xpv1.DeletionPolicy) { mg.Spec.DeletionPolicy = r } -// SetManagementPolicies of this DefaultGrant. -func (mg *DefaultGrant) SetManagementPolicies(r xpv1.ManagementPolicies) { +// SetManagementPolicies of this DefaultPrivileges. +func (mg *DefaultPrivileges) SetManagementPolicies(r xpv1.ManagementPolicies) { mg.Spec.ManagementPolicies = r } -// SetProviderConfigReference of this DefaultGrant. -func (mg *DefaultGrant) SetProviderConfigReference(r *xpv1.Reference) { +// SetProviderConfigReference of this DefaultPrivileges. +func (mg *DefaultPrivileges) SetProviderConfigReference(r *xpv1.Reference) { mg.Spec.ProviderConfigReference = r } -// SetPublishConnectionDetailsTo of this DefaultGrant. -func (mg *DefaultGrant) SetPublishConnectionDetailsTo(r *xpv1.PublishConnectionDetailsTo) { +// SetPublishConnectionDetailsTo of this DefaultPrivileges. +func (mg *DefaultPrivileges) SetPublishConnectionDetailsTo(r *xpv1.PublishConnectionDetailsTo) { mg.Spec.PublishConnectionDetailsTo = r } -// SetWriteConnectionSecretToReference of this DefaultGrant. -func (mg *DefaultGrant) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { +// SetWriteConnectionSecretToReference of this DefaultPrivileges. +func (mg *DefaultPrivileges) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { mg.Spec.WriteConnectionSecretToReference = r } diff --git a/apis/postgresql/v1alpha1/zz_generated.managedlist.go b/apis/postgresql/v1alpha1/zz_generated.managedlist.go index 48a72558..39d5ab69 100644 --- a/apis/postgresql/v1alpha1/zz_generated.managedlist.go +++ b/apis/postgresql/v1alpha1/zz_generated.managedlist.go @@ -28,8 +28,8 @@ func (l *DatabaseList) GetItems() []resource.Managed { return items } -// GetItems of this DefaultGrantList. -func (l *DefaultGrantList) GetItems() []resource.Managed { +// GetItems of this DefaultPrivilegesList. +func (l *DefaultPrivilegesList) GetItems() []resource.Managed { items := make([]resource.Managed, len(l.Items)) for i := range l.Items { items[i] = &l.Items[i] diff --git a/examples/postgresql/default_grant.yaml b/examples/postgresql/default_grant.yaml deleted file mode 100644 index df1b919c..00000000 --- a/examples/postgresql/default_grant.yaml +++ /dev/null @@ -1,42 +0,0 @@ -apiVersion: postgresql.sql.crossplane.io/v1alpha1 -kind: DefaultGrant -metadata: - name: default-grant-role-1-on-database -spec: - forProvider: - privileges: - - SELECT - roleRef: - name: reader-role - targetRoleRef: - name: example-role - schemaRef: - name: example - databaseRef: - name: example ---- -apiVersion: postgresql.sql.crossplane.io/v1alpha1 -kind: Grant -metadata: - name: example-grant-role-2-on-database -spec: - forProvider: - privileges: - - CONNECT - - TEMPORARY - roleRef: - name: example-role - databaseRef: - name: example ---- -apiVersion: postgresql.sql.crossplane.io/v1alpha1 -kind: Grant -metadata: - name: example-grant-role-membership -spec: - forProvider: - withOption: ADMIN - roleRef: - name: example-role - memberOfRef: - name: parent-role diff --git a/examples/postgresql/default_privileges.yaml b/examples/postgresql/default_privileges.yaml new file mode 100644 index 00000000..6d851366 --- /dev/null +++ b/examples/postgresql/default_privileges.yaml @@ -0,0 +1,17 @@ +apiVersion: postgresql.sql.crossplane.io/v1alpha1 +kind: DefaultPrivileges +metadata: + name: default-grant-role-1-on-database +spec: + forProvider: + privileges: + - SELECT + roleRef: + name: reader-role + targetRoleRef: + name: example-role + schemaRef: + name: example + databaseRef: + name: example +--- diff --git a/package/crds/postgresql.sql.crossplane.io_defaultgrants.yaml b/package/crds/postgresql.sql.crossplane.io_defaultprivileges.yaml similarity index 98% rename from package/crds/postgresql.sql.crossplane.io_defaultgrants.yaml rename to package/crds/postgresql.sql.crossplane.io_defaultprivileges.yaml index 3d7467c2..6c7bf344 100644 --- a/package/crds/postgresql.sql.crossplane.io_defaultgrants.yaml +++ b/package/crds/postgresql.sql.crossplane.io_defaultprivileges.yaml @@ -4,7 +4,7 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.14.0 - name: defaultgrants.postgresql.sql.crossplane.io + name: defaultprivileges.postgresql.sql.crossplane.io spec: group: postgresql.sql.crossplane.io names: @@ -12,10 +12,10 @@ spec: - crossplane - managed - sql - kind: DefaultGrant - listKind: DefaultGrantList - plural: defaultgrants - singular: defaultgrant + kind: DefaultPrivileges + listKind: DefaultPrivilegesList + plural: defaultprivileges + singular: defaultprivileges scope: Cluster versions: - additionalPrinterColumns: @@ -31,8 +31,8 @@ spec: - jsonPath: .spec.forProvider.role name: ROLE type: string - - jsonPath: .spec.forProvider.memberOf - name: MEMBER OF + - jsonPath: .spec.forProvider.schema + name: SCHEMA type: string - jsonPath: .spec.forProvider.database name: DATABASE @@ -63,7 +63,7 @@ spec: metadata: type: object spec: - description: A DefaultGrantSpec defines the desired state of a Default + description: A DefaultPrivilegesSpec defines the desired state of a Default Grant. properties: deletionPolicy: @@ -81,8 +81,8 @@ spec: - Delete type: string forProvider: - description: DefaultGrantParameters defines the desired state of a - Default Grant. + description: DefaultPrivilegesParameters defines the desired state + of a Default Grant. properties: database: description: Database in which the default privileges are applied @@ -523,7 +523,8 @@ spec: - forProvider type: object status: - description: A DefaultGrantStatus represents the observed state of a Grant. + description: A DefaultPrivilegesStatus represents the observed state of + a Grant. properties: conditions: description: Conditions of the resource. diff --git a/package/crossplane.yaml b/package/crossplane.yaml index 69f3e4a6..09c0fd9a 100644 --- a/package/crossplane.yaml +++ b/package/crossplane.yaml @@ -29,4 +29,4 @@ metadata: friendly-kind-name.meta.crossplane.io/grant.postgresql.sql.crossplane.io: Grant friendly-kind-name.meta.crossplane.io/role.postgresql.sql.crossplane.io: Role friendly-kind-name.meta.crossplane.io/user.mysql.sql.crossplane.io: User - friendly-kind-name.meta.crossplane.io/defaultgrant.postgresql.sql.crossplane.io: DefaultGrant + friendly-kind-name.meta.crossplane.io/defaultprivileges.postgresql.sql.crossplane.io: DefaultPrivileges diff --git a/pkg/controller/postgresql/default_grant/reconciler.go b/pkg/controller/postgresql/default_privileges/reconciler.go similarity index 82% rename from pkg/controller/postgresql/default_grant/reconciler.go rename to pkg/controller/postgresql/default_privileges/reconciler.go index 0657df6d..db535a7d 100644 --- a/pkg/controller/postgresql/default_grant/reconciler.go +++ b/pkg/controller/postgresql/default_privileges/reconciler.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package default_grant +package default_privileges import ( "context" @@ -48,16 +48,16 @@ const ( errNoSecretRef = "ProviderConfig does not reference a credentials Secret" errGetSecret = "cannot get credentials Secret" - errNotDefaultGrant = "managed resource is not a Grant custom resource" - errSelectDefaultGrant = "cannot select default grant" - errCreateDefaultGrant = "cannot create default grant" - errRevokeDefaultGrant = "cannot revoke default grant" - errNoRole = "role not passed or could not be resolved" - errNoTargetRole = "target role not passed or could not be resolved" - errNoObjectType = "object type not passed" - errNoDatabase = "database not passed or could not be resolved" - errNoPrivileges = "privileges not passed" - errUnknownGrant = "cannot identify grant type based on passed params" + errNotDefaultPrileges = "managed resource is not a Grant custom resource" + errSelectDefaultPrileges = "cannot select default grant" + errCreateDefaultPrileges = "cannot create default grant" + errRevokeDefaultPrileges = "cannot revoke default grant" + errNoRole = "role not passed or could not be resolved" + errNoTargetRole = "target role not passed or could not be resolved" + errNoObjectType = "object type not passed" + errNoDatabase = "database not passed or could not be resolved" + errNoPrivileges = "privileges not passed" + errUnknownGrant = "cannot identify grant type based on passed params" maxConcurrency = 5 ) @@ -76,7 +76,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { return ctrl.NewControllerManagedBy(mgr). Named(name). - For(&v1alpha1.DefaultGrant{}). + For(&v1alpha1.DefaultPrivileges{}). WithOptions(controller.Options{ MaxConcurrentReconciles: maxConcurrency, }). @@ -90,9 +90,9 @@ type connector struct { } func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { - cr, ok := mg.(*v1alpha1.DefaultGrant) + cr, ok := mg.(*v1alpha1.DefaultPrivileges) if !ok { - return nil, errors.New(errNotDefaultGrant) + return nil, errors.New(errNotDefaultPrileges) } if err := c.usage.Track(ctx, mg); err != nil { @@ -139,7 +139,7 @@ var ( } ) -func selectDefaultGrantQuery(gp v1alpha1.DefaultGrantParameters, q *xsql.Query) { +func selectDefaultPrilegesQuery(gp v1alpha1.DefaultPrivilegesParameters, q *xsql.Query) { sqlString := ` select distinct(default_acl.privilege_type) from pg_roles r @@ -163,14 +163,14 @@ func withOption(option *v1alpha1.GrantOption) string { return "" } -func inSchema(params *v1alpha1.DefaultGrantParameters) string { +func inSchema(params *v1alpha1.DefaultPrivilegesParameters) string { if params.Schema != nil { return fmt.Sprintf("IN SCHEMA %s", pq.QuoteIdentifier(*params.Schema)) } return "" } -func createDefaultGrantQuery(gp v1alpha1.DefaultGrantParameters, q *xsql.Query) { // nolint: gocyclo +func createDefaultPrilegesQuery(gp v1alpha1.DefaultPrivilegesParameters, q *xsql.Query) { // nolint: gocyclo roleName := pq.QuoteIdentifier(*gp.Role) @@ -191,7 +191,7 @@ func createDefaultGrantQuery(gp v1alpha1.DefaultGrantParameters, q *xsql.Query) q.String = query } -func deleteDefaultGrantQuery(gp v1alpha1.DefaultGrantParameters, q *xsql.Query) { +func deleteDefaultPrilegesQuery(gp v1alpha1.DefaultPrivilegesParameters, q *xsql.Query) { roleName := pq.QuoteIdentifier(*gp.Role) targetRoleName := pq.QuoteIdentifier(*gp.TargetRole) objectType := objectTypes[*gp.ObjectType] @@ -227,9 +227,9 @@ func matchingGrants(currentGrants []string, specGrants []string) bool { return true } func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { - cr, ok := mg.(*v1alpha1.DefaultGrant) + cr, ok := mg.(*v1alpha1.DefaultPrivileges) if !ok { - return managed.ExternalObservation{}, errors.New(errNotDefaultGrant) + return managed.ExternalObservation{}, errors.New(errNotDefaultPrileges) } if cr.Spec.ForProvider.Role == nil { @@ -246,12 +246,12 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex gp := cr.Spec.ForProvider var query xsql.Query - selectDefaultGrantQuery(gp, &query) + selectDefaultPrilegesQuery(gp, &query) var grants []string err := c.db.Scan(ctx, query, &grants) if err != nil && !xsql.IsNoRows(err) { - return managed.ExternalObservation{}, errors.Wrap(err, errSelectDefaultGrant) + return managed.ExternalObservation{}, errors.Wrap(err, errSelectDefaultPrileges) } if len(grants) == 0 { return managed.ExternalObservation{ResourceExists: false}, nil @@ -273,23 +273,23 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex } func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) { - cr, ok := mg.(*v1alpha1.DefaultGrant) + cr, ok := mg.(*v1alpha1.DefaultPrivileges) if !ok { - return managed.ExternalCreation{}, errors.New(errNotDefaultGrant) + return managed.ExternalCreation{}, errors.New(errNotDefaultPrileges) } cr.SetConditions(xpv1.Creating()) var createQuery xsql.Query - createDefaultGrantQuery(cr.Spec.ForProvider, &createQuery) + createDefaultPrilegesQuery(cr.Spec.ForProvider, &createQuery) var deleteQuery xsql.Query - deleteDefaultGrantQuery(cr.Spec.ForProvider, &deleteQuery) + deleteDefaultPrilegesQuery(cr.Spec.ForProvider, &deleteQuery) err := c.db.ExecTx(ctx, []xsql.Query{ deleteQuery, createQuery, }) - return managed.ExternalCreation{}, errors.Wrap(err, errCreateDefaultGrant) + return managed.ExternalCreation{}, errors.Wrap(err, errCreateDefaultPrileges) } func (c *external) Update( @@ -300,15 +300,15 @@ func (c *external) Update( } func (c *external) Delete(ctx context.Context, mg resource.Managed) error { - cr, ok := mg.(*v1alpha1.DefaultGrant) + cr, ok := mg.(*v1alpha1.DefaultPrivileges) if !ok { - return errors.New(errNotDefaultGrant) + return errors.New(errNotDefaultPrileges) } var query xsql.Query cr.SetConditions(xpv1.Deleting()) - deleteDefaultGrantQuery(cr.Spec.ForProvider, &query) + deleteDefaultPrilegesQuery(cr.Spec.ForProvider, &query) - return errors.Wrap(c.db.Exec(ctx, query), errRevokeDefaultGrant) + return errors.Wrap(c.db.Exec(ctx, query), errRevokeDefaultPrileges) } diff --git a/pkg/controller/postgresql/default_grant/reconciler_test.go b/pkg/controller/postgresql/default_privileges/reconciler_test.go similarity index 86% rename from pkg/controller/postgresql/default_grant/reconciler_test.go rename to pkg/controller/postgresql/default_privileges/reconciler_test.go index 3a5dc324..4b2650c9 100644 --- a/pkg/controller/postgresql/default_grant/reconciler_test.go +++ b/pkg/controller/postgresql/default_privileges/reconciler_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package default_grant +package default_privileges import ( "context" @@ -86,11 +86,11 @@ func TestConnect(t *testing.T) { want error }{ "ErrNotGrant": { - reason: "An error should be returned if the managed resource is not a *DefaultGrant", + reason: "An error should be returned if the managed resource is not a *DefaultPrivileges", args: args{ mg: nil, }, - want: errors.New(errNotDefaultGrant), + want: errors.New(errNotDefaultPrivileges), }, "ErrTrackProviderConfigUsage": { reason: "An error should be returned if we can't track our ProviderConfig usage", @@ -98,7 +98,7 @@ func TestConnect(t *testing.T) { usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return errBoom }), }, args: args{ - mg: &v1alpha1.DefaultGrant{}, + mg: &v1alpha1.DefaultPrivileges{}, }, want: errors.Wrap(errBoom, errTrackPCUsage), }, @@ -111,8 +111,8 @@ func TestConnect(t *testing.T) { usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), }, args: args{ - mg: &v1alpha1.DefaultGrant{ - Spec: v1alpha1.DefaultGrantSpec{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ ResourceSpec: xpv1.ResourceSpec{ ProviderConfigReference: &xpv1.Reference{}, }, @@ -133,8 +133,8 @@ func TestConnect(t *testing.T) { usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), }, args: args{ - mg: &v1alpha1.DefaultGrant{ - Spec: v1alpha1.DefaultGrantSpec{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ ResourceSpec: xpv1.ResourceSpec{ ProviderConfigReference: &xpv1.Reference{}, }, @@ -160,8 +160,8 @@ func TestConnect(t *testing.T) { usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), }, args: args{ - mg: &v1alpha1.DefaultGrant{ - Spec: v1alpha1.DefaultGrantSpec{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ ResourceSpec: xpv1.ResourceSpec{ ProviderConfigReference: &xpv1.Reference{}, }, @@ -209,12 +209,12 @@ func TestObserve(t *testing.T) { want want }{ "ErrNotGrant": { - reason: "An error should be returned if the managed resource is not a *DefaultGrant", + reason: "An error should be returned if the managed resource is not a *DefaultPrivileges", args: args{ mg: nil, }, want: want{ - err: errors.New(errNotDefaultGrant), + err: errors.New(errNotDefaultPrivileges), }, }, "SuccessNoGrant": { @@ -228,9 +228,9 @@ func TestObserve(t *testing.T) { }, }, args: args{ - mg: &v1alpha1.DefaultGrant{ - Spec: v1alpha1.DefaultGrantSpec{ - ForProvider: v1alpha1.DefaultGrantParameters{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ Database: ptr.To("test-example"), Role: ptr.To("test-example"), TargetRole: ptr.To("target-role"), @@ -275,9 +275,9 @@ func TestObserve(t *testing.T) { // }, // }, // args: args{ - // mg: &v1alpha1.DefaultGrant{ - // Spec: v1alpha1.DefaultGrantSpec{ - // ForProvider: v1alpha1.DefaultGrantParameters{ + // mg: &v1alpha1.DefaultPrivileges{ + // Spec: v1alpha1.DefaultPrivilegesSpec{ + // ForProvider: v1alpha1.DefaultPrivilegesParameters{ // Database: ptr.To("test-example"), // Role: ptr.To("test-example"), // Privileges: v1alpha1.GrantPrivileges{"ALL"}, @@ -303,9 +303,9 @@ func TestObserve(t *testing.T) { }, }, args: args{ - mg: &v1alpha1.DefaultGrant{ - Spec: v1alpha1.DefaultGrantSpec{ - ForProvider: v1alpha1.DefaultGrantParameters{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ Database: ptr.To("test-example"), Role: ptr.To("test-example"), TargetRole: ptr.To("target-role"), @@ -317,10 +317,10 @@ func TestObserve(t *testing.T) { }, }, want: want{ - err: errors.Wrap(errBoom, errSelectDefaultGrant), + err: errors.Wrap(errBoom, errSelectDefaultPrivileges), }, }, - "DefaultGrantFound": { + "DefaultPrivilegesFound": { reason: "We should return no error if we can find the right permissions in the default grant", fields: fields{ db: mockDB{ @@ -332,9 +332,9 @@ func TestObserve(t *testing.T) { }, }, args: args{ - mg: &v1alpha1.DefaultGrant{ - Spec: v1alpha1.DefaultGrantSpec{ - ForProvider: v1alpha1.DefaultGrantParameters{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ Database: ptr.To("testdb"), Role: ptr.To("testrole"), TargetRole: ptr.To("target-role"), @@ -365,9 +365,9 @@ func TestObserve(t *testing.T) { // }, // }, // args: args{ - // mg: &v1alpha1.DefaultGrant{ - // Spec: v1alpha1.DefaultGrantSpec{ - // ForProvider: v1alpha1.DefaultGrantParameters{ + // mg: &v1alpha1.DefaultPrivileges{ + // Spec: v1alpha1.DefaultPrivilegesSpec{ + // ForProvider: v1alpha1.DefaultPrivilegesParameters{ // Role: ptr.To("testrole"), // WithOption: &goa, // }, @@ -422,12 +422,12 @@ func TestCreate(t *testing.T) { want want }{ "ErrNotGrant": { - reason: "An error should be returned if the managed resource is not a *DefaultGrant", + reason: "An error should be returned if the managed resource is not a *DefaultPrivileges", args: args{ mg: nil, }, want: want{ - err: errors.New(errNotDefaultGrant), + err: errors.New(errNotDefaultPrivileges), }, }, "ErrExec": { @@ -438,9 +438,9 @@ func TestCreate(t *testing.T) { }, }, args: args{ - mg: &v1alpha1.DefaultGrant{ - Spec: v1alpha1.DefaultGrantSpec{ - ForProvider: v1alpha1.DefaultGrantParameters{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ Database: ptr.To("test-example"), Role: ptr.To("test-example"), TargetRole: ptr.To("target-role"), @@ -451,7 +451,7 @@ func TestCreate(t *testing.T) { }, }, want: want{ - err: errors.Wrap(errBoom, errCreateDefaultGrant), + err: errors.Wrap(errBoom, errCreateDefaultPrivileges), }, }, "Success": { @@ -462,9 +462,9 @@ func TestCreate(t *testing.T) { }, }, args: args{ - mg: &v1alpha1.DefaultGrant{ - Spec: v1alpha1.DefaultGrantSpec{ - ForProvider: v1alpha1.DefaultGrantParameters{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ Database: ptr.To("test-example"), Role: ptr.To("test-example"), TargetRole: ptr.To("target-role"), @@ -516,11 +516,11 @@ func TestUpdate(t *testing.T) { want want }{ "ErrNoOp": { - reason: "Update is a no-op, make sure we dont throw an error *DefaultGrant", + reason: "Update is a no-op, make sure we dont throw an error *DefaultPrivileges", args: args{ - mg: &v1alpha1.DefaultGrant{ - Spec: v1alpha1.DefaultGrantSpec{ - ForProvider: v1alpha1.DefaultGrantParameters{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ Database: ptr.To("test-example"), Role: ptr.To("test-example"), Privileges: v1alpha1.GrantPrivileges{"ALL"}, @@ -568,15 +568,15 @@ func TestDelete(t *testing.T) { args args want error }{ - "ErrNotDefaultGrant": { - reason: "An error should be returned if the managed resource is not a *DefaultGrant", + "ErrNotDefaultPrivileges": { + reason: "An error should be returned if the managed resource is not a *DefaultPrivileges", args: args{ mg: nil, }, - want: errors.New(errNotDefaultGrant), + want: errors.New(errNotDefaultPrivileges), }, - "ErrDropDefaultGrant": { - reason: "Errors dropping a default grant should be returned", + "ErrDropDefaultPrivileges": { + reason: "Errors dropping default privileges should be returned", fields: fields{ db: &mockDB{ MockExec: func(ctx context.Context, q xsql.Query) error { @@ -585,9 +585,9 @@ func TestDelete(t *testing.T) { }, }, args: args{ - mg: &v1alpha1.DefaultGrant{ - Spec: v1alpha1.DefaultGrantSpec{ - ForProvider: v1alpha1.DefaultGrantParameters{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ Database: ptr.To("test-example"), Role: ptr.To("test-example"), Privileges: v1alpha1.GrantPrivileges{"ALL"}, @@ -597,14 +597,14 @@ func TestDelete(t *testing.T) { }, }, }, - want: errors.Wrap(errBoom, errRevokeDefaultGrant), + want: errors.Wrap(errBoom, errRevokeDefaultPrivileges), }, "Success": { reason: "No error should be returned if the default grant was revoked", args: args{ - mg: &v1alpha1.DefaultGrant{ - Spec: v1alpha1.DefaultGrantSpec{ - ForProvider: v1alpha1.DefaultGrantParameters{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ Database: ptr.To("test-example"), Role: ptr.To("test-example"), Privileges: v1alpha1.GrantPrivileges{"ALL"}, diff --git a/pkg/controller/postgresql/postgresql.go b/pkg/controller/postgresql/postgresql.go index 5af9b800..f6031cae 100644 --- a/pkg/controller/postgresql/postgresql.go +++ b/pkg/controller/postgresql/postgresql.go @@ -23,6 +23,7 @@ import ( "github.com/crossplane-contrib/provider-sql/pkg/controller/postgresql/config" "github.com/crossplane-contrib/provider-sql/pkg/controller/postgresql/database" + "github.com/crossplane-contrib/provider-sql/pkg/controller/postgresql/default_privileges" "github.com/crossplane-contrib/provider-sql/pkg/controller/postgresql/extension" "github.com/crossplane-contrib/provider-sql/pkg/controller/postgresql/grant" "github.com/crossplane-contrib/provider-sql/pkg/controller/postgresql/role" @@ -39,6 +40,7 @@ func Setup(mgr ctrl.Manager, o controller.Options) error { grant.Setup, extension.Setup, schema.Setup, + default_privileges.Setup, } { if err := setup(mgr, o); err != nil { return err From e0170015e7412e42758f12ad7829d383f11e7e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Fern=C3=A1ndez=20Campo?= Date: Sun, 27 Oct 2024 09:33:02 +0100 Subject: [PATCH 10/17] added resolver back and added one more object type to the list of resources we can add default privileges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joaquín Fernández Campo --- .../v1alpha1/default_privileges_types.go | 102 +++++++++--------- 1 file changed, 54 insertions(+), 48 deletions(-) diff --git a/apis/postgresql/v1alpha1/default_privileges_types.go b/apis/postgresql/v1alpha1/default_privileges_types.go index 067df57b..c2e1450d 100644 --- a/apis/postgresql/v1alpha1/default_privileges_types.go +++ b/apis/postgresql/v1alpha1/default_privileges_types.go @@ -1,8 +1,13 @@ package v1alpha1 import ( + "context" + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/reference" + "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" ) // +kubebuilder:object:root=true @@ -13,6 +18,7 @@ import ( // +kubebuilder:printcolumn:name="SYNCED",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status" // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" // +kubebuilder:printcolumn:name="ROLE",type="string",JSONPath=".spec.forProvider.role" +// +kubebuilder:printcolumn:name="TARGET_ROLE",type="string",JSONPath=".spec.forProvider.targetRole" // +kubebuilder:printcolumn:name="SCHEMA",type="string",JSONPath=".spec.forProvider.schema" // +kubebuilder:printcolumn:name="DATABASE",type="string",JSONPath=".spec.forProvider.database" // +kubebuilder:printcolumn:name="PRIVILEGES",type="string",JSONPath=".spec.forProvider.privileges" @@ -49,7 +55,7 @@ type DefaultPrivilegesParameters struct { TargetRole *string `json:"targetRole"` // ObjectType to which the privileges are granted. - // +kubebuilder:validation:Enum=table;sequence;function;schema + // +kubebuilder:validation:Enum=table;sequence;function;schema;type // +required ObjectType *string `json:"objectType,omitempty"` @@ -113,50 +119,50 @@ type DefaultPrivilegesList struct { } // ResolveReferences of this DefaultPrivileges. -// func (mg *DefaultPrivileges) ResolveReferences(ctx context.Context, c client.Reader) error { -// r := reference.NewAPIResolver(c, mg) - -// // Resolve spec.forProvider.database -// rsp, err := r.Resolve(ctx, reference.ResolutionRequest{ -// CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Database), -// Reference: mg.Spec.ForProvider.DatabaseRef, -// Selector: mg.Spec.ForProvider.DatabaseSelector, -// To: reference.To{Managed: &Database{}, List: &DatabaseList{}}, -// Extract: reference.ExternalName(), -// }) -// if err != nil { -// return errors.Wrap(err, "spec.forProvider.database") -// } -// mg.Spec.ForProvider.Database = reference.ToPtrValue(rsp.ResolvedValue) -// mg.Spec.ForProvider.DatabaseRef = rsp.ResolvedReference - -// // Resolve spec.forProvider.role -// rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ -// CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Role), -// Reference: mg.Spec.ForProvider.RoleRef, -// Selector: mg.Spec.ForProvider.RoleSelector, -// To: reference.To{Managed: &Role{}, List: &RoleList{}}, -// Extract: reference.ExternalName(), -// }) -// if err != nil { -// return errors.Wrap(err, "spec.forProvider.role") -// } -// mg.Spec.ForProvider.Role = reference.ToPtrValue(rsp.ResolvedValue) -// mg.Spec.ForProvider.RoleRef = rsp.ResolvedReference - -// // Resolve spec.forProvider.schema -// rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ -// CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Schema), -// Reference: mg.Spec.ForProvider.SchemaRef, -// Selector: mg.Spec.ForProvider.SchemaSelector, -// To: reference.To{Managed: &Role{}, List: &RoleList{}}, -// Extract: reference.ExternalName(), -// }) -// if err != nil { -// return errors.Wrap(err, "spec.forProvider.schema") -// } -// mg.Spec.ForProvider.Schema = reference.ToPtrValue(rsp.ResolvedValue) -// mg.Spec.ForProvider.SchemaRef = rsp.ResolvedReference - -// return nil -// } +func (mg *DefaultPrivileges) ResolveReferences(ctx context.Context, c client.Reader) error { + r := reference.NewAPIResolver(c, mg) + + // Resolve spec.forProvider.database + rsp, err := r.Resolve(ctx, reference.ResolutionRequest{ + CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Database), + Reference: mg.Spec.ForProvider.DatabaseRef, + Selector: mg.Spec.ForProvider.DatabaseSelector, + To: reference.To{Managed: &Database{}, List: &DatabaseList{}}, + Extract: reference.ExternalName(), + }) + if err != nil { + return errors.Wrap(err, "spec.forProvider.database") + } + mg.Spec.ForProvider.Database = reference.ToPtrValue(rsp.ResolvedValue) + mg.Spec.ForProvider.DatabaseRef = rsp.ResolvedReference + + // Resolve spec.forProvider.role + rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ + CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Role), + Reference: mg.Spec.ForProvider.RoleRef, + Selector: mg.Spec.ForProvider.RoleSelector, + To: reference.To{Managed: &Role{}, List: &RoleList{}}, + Extract: reference.ExternalName(), + }) + if err != nil { + return errors.Wrap(err, "spec.forProvider.role") + } + mg.Spec.ForProvider.Role = reference.ToPtrValue(rsp.ResolvedValue) + mg.Spec.ForProvider.RoleRef = rsp.ResolvedReference + + // Resolve spec.forProvider.schema + rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ + CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Schema), + Reference: mg.Spec.ForProvider.SchemaRef, + Selector: mg.Spec.ForProvider.SchemaSelector, + To: reference.To{Managed: &Role{}, List: &RoleList{}}, + Extract: reference.ExternalName(), + }) + if err != nil { + return errors.Wrap(err, "spec.forProvider.schema") + } + mg.Spec.ForProvider.Schema = reference.ToPtrValue(rsp.ResolvedValue) + mg.Spec.ForProvider.SchemaRef = rsp.ResolvedReference + + return nil +} From 6aae26af8fa3d61089372be80370c29aacd59357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Fern=C3=A1ndez=20Campo?= Date: Sun, 27 Oct 2024 13:37:29 +0100 Subject: [PATCH 11/17] added missing target role to CRD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joaquín Fernández Campo --- .../crds/postgresql.sql.crossplane.io_defaultprivileges.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package/crds/postgresql.sql.crossplane.io_defaultprivileges.yaml b/package/crds/postgresql.sql.crossplane.io_defaultprivileges.yaml index 6c7bf344..a677dc95 100644 --- a/package/crds/postgresql.sql.crossplane.io_defaultprivileges.yaml +++ b/package/crds/postgresql.sql.crossplane.io_defaultprivileges.yaml @@ -31,6 +31,9 @@ spec: - jsonPath: .spec.forProvider.role name: ROLE type: string + - jsonPath: .spec.forProvider.targetRole + name: TARGET_ROLE + type: string - jsonPath: .spec.forProvider.schema name: SCHEMA type: string @@ -170,6 +173,7 @@ spec: - sequence - function - schema + - type type: string privileges: description: |- From e8ad4ea345ac736dfae48cc596256e5f1a1beff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Fern=C3=A1ndez=20Campo?= Date: Sun, 27 Oct 2024 13:38:55 +0100 Subject: [PATCH 12/17] renamed example to match expected value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joaquín Fernández Campo --- .../{default_privileges.yaml => defaultprivileges.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/postgresql/{default_privileges.yaml => defaultprivileges.yaml} (100%) diff --git a/examples/postgresql/default_privileges.yaml b/examples/postgresql/defaultprivileges.yaml similarity index 100% rename from examples/postgresql/default_privileges.yaml rename to examples/postgresql/defaultprivileges.yaml From c1f6e9330d6a849f1b535ffa43e167ac68d02ee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Fern=C3=A1ndez=20Campo?= Date: Sun, 27 Oct 2024 13:42:28 +0100 Subject: [PATCH 13/17] fixed syntax issue related to https://github.com/crossplane/crossplane/issues/4776 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joaquín Fernández Campo --- examples/postgresql/defaultprivileges.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/postgresql/defaultprivileges.yaml b/examples/postgresql/defaultprivileges.yaml index 6d851366..c9135d31 100644 --- a/examples/postgresql/defaultprivileges.yaml +++ b/examples/postgresql/defaultprivileges.yaml @@ -14,4 +14,3 @@ spec: name: example databaseRef: name: example ---- From e9e1d4f998611ecdf77be3f06734f553600ebf3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Fern=C3=A1ndez=20Campo?= Date: Sun, 27 Oct 2024 16:48:07 +0100 Subject: [PATCH 14/17] added default privileges to the schema and fixed a typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joaquín Fernández Campo --- apis/postgresql/v1alpha1/default_privileges_types.go | 2 +- apis/postgresql/v1alpha1/register.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apis/postgresql/v1alpha1/default_privileges_types.go b/apis/postgresql/v1alpha1/default_privileges_types.go index c2e1450d..1c867dd0 100644 --- a/apis/postgresql/v1alpha1/default_privileges_types.go +++ b/apis/postgresql/v1alpha1/default_privileges_types.go @@ -12,7 +12,7 @@ import ( // +kubebuilder:object:root=true -// A Grant represents the declarative state of a PostgreSQL grant. +// A DefaultPrivileges represents the declarative state of a PostgreSQL DefaultPrivileges. // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" // +kubebuilder:printcolumn:name="SYNCED",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status" diff --git a/apis/postgresql/v1alpha1/register.go b/apis/postgresql/v1alpha1/register.go index 33c242bb..453da293 100644 --- a/apis/postgresql/v1alpha1/register.go +++ b/apis/postgresql/v1alpha1/register.go @@ -114,4 +114,5 @@ func init() { SchemeBuilder.Register(&Grant{}, &GrantList{}) SchemeBuilder.Register(&Extension{}, &ExtensionList{}) SchemeBuilder.Register(&Schema{}, &SchemaList{}) + SchemeBuilder.Register(&DefaultPrivileges{}, &DefaultPrivilegesList{}) } From 60ad6586016cba6a704af4313f01eb454203b1a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Fern=C3=A1ndez=20Campo?= Date: Sun, 27 Oct 2024 22:25:46 +0100 Subject: [PATCH 15/17] more fixes and added some debugging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joaquín Fernández Campo --- .../v1alpha1/default_privileges_types.go | 54 ++++-------- .../v1alpha1/zz_generated.deepcopy.go | 10 --- cmd/provider/main.go | 2 +- .../default_privileges/reconciler.go | 62 ++++++++------ .../default_privileges/reconciler_test.go | 84 +------------------ 5 files changed, 55 insertions(+), 157 deletions(-) diff --git a/apis/postgresql/v1alpha1/default_privileges_types.go b/apis/postgresql/v1alpha1/default_privileges_types.go index 1c867dd0..6ca47f48 100644 --- a/apis/postgresql/v1alpha1/default_privileges_types.go +++ b/apis/postgresql/v1alpha1/default_privileges_types.go @@ -95,18 +95,8 @@ type DefaultPrivilegesParameters struct { DatabaseSelector *xpv1.Selector `json:"databaseSelector,omitempty"` // Schema in which the default privileges are applied - // +optional + // +required Schema *string `json:"schema,omitempty"` - - // SchemaRef references the database object this default grant it for. - // +immutable - // +optional - SchemaRef *xpv1.Reference `json:"schemaRef,omitempty"` - - // SchemaSelector selects a reference to a Database this grant is for. - // +immutable - // +optional - SchemaSelector *xpv1.Selector `json:"schemaSelector,omitempty"` } // +kubebuilder:object:root=true @@ -122,22 +112,22 @@ type DefaultPrivilegesList struct { func (mg *DefaultPrivileges) ResolveReferences(ctx context.Context, c client.Reader) error { r := reference.NewAPIResolver(c, mg) - // Resolve spec.forProvider.database - rsp, err := r.Resolve(ctx, reference.ResolutionRequest{ - CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Database), - Reference: mg.Spec.ForProvider.DatabaseRef, - Selector: mg.Spec.ForProvider.DatabaseSelector, - To: reference.To{Managed: &Database{}, List: &DatabaseList{}}, - Extract: reference.ExternalName(), - }) - if err != nil { - return errors.Wrap(err, "spec.forProvider.database") - } - mg.Spec.ForProvider.Database = reference.ToPtrValue(rsp.ResolvedValue) - mg.Spec.ForProvider.DatabaseRef = rsp.ResolvedReference + // // Resolve spec.forProvider.database + // rsp, err := r.Resolve(ctx, reference.ResolutionRequest{ + // CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Database), + // Reference: mg.Spec.ForProvider.DatabaseRef, + // Selector: mg.Spec.ForProvider.DatabaseSelector, + // To: reference.To{Managed: &Database{}, List: &DatabaseList{}}, + // Extract: reference.ExternalName(), + // }) + // if err != nil { + // return errors.Wrap(err, "spec.forProvider.database") + // } + // mg.Spec.ForProvider.Database = reference.ToPtrValue(rsp.ResolvedValue) + // mg.Spec.ForProvider.DatabaseRef = rsp.ResolvedReference // Resolve spec.forProvider.role - rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ + rsp, err := r.Resolve(ctx, reference.ResolutionRequest{ CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Role), Reference: mg.Spec.ForProvider.RoleRef, Selector: mg.Spec.ForProvider.RoleSelector, @@ -150,19 +140,5 @@ func (mg *DefaultPrivileges) ResolveReferences(ctx context.Context, c client.Rea mg.Spec.ForProvider.Role = reference.ToPtrValue(rsp.ResolvedValue) mg.Spec.ForProvider.RoleRef = rsp.ResolvedReference - // Resolve spec.forProvider.schema - rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ - CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Schema), - Reference: mg.Spec.ForProvider.SchemaRef, - Selector: mg.Spec.ForProvider.SchemaSelector, - To: reference.To{Managed: &Role{}, List: &RoleList{}}, - Extract: reference.ExternalName(), - }) - if err != nil { - return errors.Wrap(err, "spec.forProvider.schema") - } - mg.Spec.ForProvider.Schema = reference.ToPtrValue(rsp.ResolvedValue) - mg.Spec.ForProvider.SchemaRef = rsp.ResolvedReference - return nil } diff --git a/apis/postgresql/v1alpha1/zz_generated.deepcopy.go b/apis/postgresql/v1alpha1/zz_generated.deepcopy.go index f5eb901a..f0dff979 100644 --- a/apis/postgresql/v1alpha1/zz_generated.deepcopy.go +++ b/apis/postgresql/v1alpha1/zz_generated.deepcopy.go @@ -294,16 +294,6 @@ func (in *DefaultPrivilegesParameters) DeepCopyInto(out *DefaultPrivilegesParame *out = new(string) **out = **in } - if in.SchemaRef != nil { - in, out := &in.SchemaRef, &out.SchemaRef - *out = new(v1.Reference) - (*in).DeepCopyInto(*out) - } - if in.SchemaSelector != nil { - in, out := &in.SchemaSelector, &out.SchemaSelector - *out = new(v1.Selector) - (*in).DeepCopyInto(*out) - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultPrivilegesParameters. diff --git a/cmd/provider/main.go b/cmd/provider/main.go index c4e98f35..65dd74b5 100644 --- a/cmd/provider/main.go +++ b/cmd/provider/main.go @@ -42,7 +42,7 @@ func main() { debug = app.Flag("debug", "Run with debug logging.").Short('d').Bool() pollInterval = app.Flag("poll", "Poll interval controls how often an individual resource should be checked for drift.").Default("10m").Duration() syncPeriod = app.Flag("sync", "Controller manager sync period such as 300ms, 1.5h, or 2h45m").Short('s').Default("1h").Duration() - leaderElection = app.Flag("leader-election", "Use leader election for the conroller manager.").Short('l').Default("false").Envar("LEADER_ELECTION").Bool() + leaderElection = app.Flag("leader-election", "Use leader election for the controller manager.").Short('l').Default("false").Envar("LEADER_ELECTION").Bool() ) kingpin.MustParse(app.Parse(os.Args[1:])) diff --git a/pkg/controller/postgresql/default_privileges/reconciler.go b/pkg/controller/postgresql/default_privileges/reconciler.go index db535a7d..c92869dd 100644 --- a/pkg/controller/postgresql/default_privileges/reconciler.go +++ b/pkg/controller/postgresql/default_privileges/reconciler.go @@ -48,27 +48,27 @@ const ( errNoSecretRef = "ProviderConfig does not reference a credentials Secret" errGetSecret = "cannot get credentials Secret" - errNotDefaultPrileges = "managed resource is not a Grant custom resource" - errSelectDefaultPrileges = "cannot select default grant" - errCreateDefaultPrileges = "cannot create default grant" - errRevokeDefaultPrileges = "cannot revoke default grant" - errNoRole = "role not passed or could not be resolved" - errNoTargetRole = "target role not passed or could not be resolved" - errNoObjectType = "object type not passed" - errNoDatabase = "database not passed or could not be resolved" - errNoPrivileges = "privileges not passed" - errUnknownGrant = "cannot identify grant type based on passed params" + errNotDefaultPrivileges = "managed resource is not a Grant custom resource" + errSelectDefaultPrivileges = "cannot select default privileges" + errCreateDefaultPrivileges = "cannot create default privileges" + errRevokeDefaultPrivileges = "cannot revoke default privileges" + errNoRole = "role not passed or could not be resolved" + errNoTargetRole = "target role not passed or could not be resolved" + errNoObjectType = "object type not passed" + errNoDatabase = "database not passed or could not be resolved" + errNoPrivileges = "privileges not passed" + errUnknownGrant = "cannot identify grant type based on passed params" maxConcurrency = 5 ) // Setup adds a controller that reconciles Grant managed resources. func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { - name := managed.ControllerName(v1alpha1.GrantGroupKind) + name := managed.ControllerName(v1alpha1.DefaultPrivilegesGroupKind) t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{}) r := managed.NewReconciler(mgr, - resource.ManagedKind(v1alpha1.GrantGroupVersionKind), + resource.ManagedKind(v1alpha1.DefaultPrivilegesGroupVersionKind), managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newDB: postgresql.New}), managed.WithLogger(o.Logger.WithValues("controller", name)), managed.WithPollInterval(o.PollInterval), @@ -92,7 +92,7 @@ type connector struct { func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { cr, ok := mg.(*v1alpha1.DefaultPrivileges) if !ok { - return nil, errors.New(errNotDefaultPrileges) + return nil, errors.New(errNotDefaultPrivileges) } if err := c.usage.Track(ctx, mg); err != nil { @@ -139,7 +139,7 @@ var ( } ) -func selectDefaultPrilegesQuery(gp v1alpha1.DefaultPrivilegesParameters, q *xsql.Query) { +func selectDefaultPrivilegesQuery(gp v1alpha1.DefaultPrivilegesParameters, q *xsql.Query) { sqlString := ` select distinct(default_acl.privilege_type) from pg_roles r @@ -170,7 +170,7 @@ func inSchema(params *v1alpha1.DefaultPrivilegesParameters) string { return "" } -func createDefaultPrilegesQuery(gp v1alpha1.DefaultPrivilegesParameters, q *xsql.Query) { // nolint: gocyclo +func createDefaultPrivilegesQuery(gp v1alpha1.DefaultPrivilegesParameters, q *xsql.Query) { // nolint: gocyclo roleName := pq.QuoteIdentifier(*gp.Role) @@ -191,13 +191,13 @@ func createDefaultPrilegesQuery(gp v1alpha1.DefaultPrivilegesParameters, q *xsql q.String = query } -func deleteDefaultPrilegesQuery(gp v1alpha1.DefaultPrivilegesParameters, q *xsql.Query) { +func deleteDefaultPrivilegesQuery(gp v1alpha1.DefaultPrivilegesParameters, q *xsql.Query) { roleName := pq.QuoteIdentifier(*gp.Role) targetRoleName := pq.QuoteIdentifier(*gp.TargetRole) objectType := objectTypes[*gp.ObjectType] query := strings.TrimSpace(fmt.Sprintf( - "ALTER DEFAULT PRIVILEGES FOR ROLE %s %s REVOKE ALL ON %s ON %s TO %s %s", + "ALTER DEFAULT PRIVILEGES FOR ROLE %s %s REVOKE %s ON %s TO %s %s", targetRoleName, inSchema(&gp), strings.Join(gp.Privileges.ToStringSlice(), ","), @@ -229,7 +229,7 @@ func matchingGrants(currentGrants []string, specGrants []string) bool { func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { cr, ok := mg.(*v1alpha1.DefaultPrivileges) if !ok { - return managed.ExternalObservation{}, errors.New(errNotDefaultPrileges) + return managed.ExternalObservation{}, errors.New(errNotDefaultPrivileges) } if cr.Spec.ForProvider.Role == nil { @@ -246,12 +246,12 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex gp := cr.Spec.ForProvider var query xsql.Query - selectDefaultPrilegesQuery(gp, &query) + selectDefaultPrivilegesQuery(gp, &query) var grants []string err := c.db.Scan(ctx, query, &grants) if err != nil && !xsql.IsNoRows(err) { - return managed.ExternalObservation{}, errors.Wrap(err, errSelectDefaultPrileges) + return managed.ExternalObservation{}, errors.Wrap(err, errSelectDefaultPrivileges) } if len(grants) == 0 { return managed.ExternalObservation{ResourceExists: false}, nil @@ -275,21 +275,29 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) { cr, ok := mg.(*v1alpha1.DefaultPrivileges) if !ok { - return managed.ExternalCreation{}, errors.New(errNotDefaultPrileges) + return managed.ExternalCreation{}, errors.New(errNotDefaultPrivileges) } cr.SetConditions(xpv1.Creating()) var createQuery xsql.Query - createDefaultPrilegesQuery(cr.Spec.ForProvider, &createQuery) + createDefaultPrivilegesQuery(cr.Spec.ForProvider, &createQuery) var deleteQuery xsql.Query - deleteDefaultPrilegesQuery(cr.Spec.ForProvider, &deleteQuery) + deleteDefaultPrivilegesQuery(cr.Spec.ForProvider, &deleteQuery) err := c.db.ExecTx(ctx, []xsql.Query{ deleteQuery, createQuery, }) - return managed.ExternalCreation{}, errors.Wrap(err, errCreateDefaultPrileges) + errString := errCreateDefaultPrivileges + if err != nil { + errString = fmt.Sprintf(` + %s + delete: |%s| + create: |%s| + `, errString, deleteQuery.String, createQuery.String) + } + return managed.ExternalCreation{}, errors.Wrap(err, errString) } func (c *external) Update( @@ -302,13 +310,13 @@ func (c *external) Update( func (c *external) Delete(ctx context.Context, mg resource.Managed) error { cr, ok := mg.(*v1alpha1.DefaultPrivileges) if !ok { - return errors.New(errNotDefaultPrileges) + return errors.New(errNotDefaultPrivileges) } var query xsql.Query cr.SetConditions(xpv1.Deleting()) - deleteDefaultPrilegesQuery(cr.Spec.ForProvider, &query) + deleteDefaultPrivilegesQuery(cr.Spec.ForProvider, &query) - return errors.Wrap(c.db.Exec(ctx, query), errRevokeDefaultPrileges) + return errors.Wrap(c.db.Exec(ctx, query), errRevokeDefaultPrivileges) } diff --git a/pkg/controller/postgresql/default_privileges/reconciler_test.go b/pkg/controller/postgresql/default_privileges/reconciler_test.go index 4b2650c9..debfa1d6 100644 --- a/pkg/controller/postgresql/default_privileges/reconciler_test.go +++ b/pkg/controller/postgresql/default_privileges/reconciler_test.go @@ -244,55 +244,6 @@ func TestObserve(t *testing.T) { o: managed.ExternalObservation{ResourceExists: false}, }, }, - // "AllMapsToExpandedPrivileges": { - // reason: "We expand ALL to CREATE, TEMPORARY, CONNECT when checking for existing grants", - // fields: fields{ - // db: mockDB{ - // MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { - // privileges := q.Parameters[3] - - // privs, ok := privileges.(*pq.StringArray) - // if !ok { - // return fmt.Errorf("expected Scan parameter to be pq.StringArray, got %T", privileges) - // } - - // // The order is not guaranteed, so sort the slices before comparing - // sort.Strings(*privs) - - // // Return if there's a diff between the expected and actual privileges - // diff := cmp.Diff(&pq.StringArray{"CONNECT", "CREATE", "TEMPORARY"}, privileges) - - // bv := dest[0].(*bool) - // *bv = diff == "" - - // // Extra logging in case this test is going to fail - // if diff != "" { - // t.Logf("expected empty diff, got: %s", diff) - // } - - // return nil - // }, - // }, - // }, - // args: args{ - // mg: &v1alpha1.DefaultPrivileges{ - // Spec: v1alpha1.DefaultPrivilegesSpec{ - // ForProvider: v1alpha1.DefaultPrivilegesParameters{ - // Database: ptr.To("test-example"), - // Role: ptr.To("test-example"), - // Privileges: v1alpha1.GrantPrivileges{"ALL"}, - // }, - // }, - // }, - // }, - // want: want{ - // o: managed.ExternalObservation{ - // ResourceExists: true, - // ResourceUpToDate: true, - // }, - // err: nil, - // }, - // }, "ErrSelectGrant": { reason: "We should return any errors encountered while trying to show the default grant", fields: fields{ @@ -353,35 +304,6 @@ func TestObserve(t *testing.T) { err: nil, }, }, - // "SuccessRoleMembership": { - // reason: "We should return no error if we can find our role-membership grant", - // fields: fields{ - // db: mockDB{ - // MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { - // bv := dest[0].(*bool) - // *bv = true - // return nil - // }, - // }, - // }, - // args: args{ - // mg: &v1alpha1.DefaultPrivileges{ - // Spec: v1alpha1.DefaultPrivilegesSpec{ - // ForProvider: v1alpha1.DefaultPrivilegesParameters{ - // Role: ptr.To("testrole"), - // WithOption: &goa, - // }, - // }, - // }, - // }, - // want: want{ - // o: managed.ExternalObservation{ - // ResourceExists: true, - // ResourceUpToDate: true, - // }, - // err: nil, - // }, - // }, } for name, tc := range cases { @@ -458,7 +380,9 @@ func TestCreate(t *testing.T) { reason: "No error should be returned when we successfully create a default grant", fields: fields{ db: &mockDB{ - MockExecTx: func(ctx context.Context, ql []xsql.Query) error { return nil }, + MockExecTx: func(ctx context.Context, ql []xsql.Query) error { + return nil + }, }, }, args: args{ @@ -468,7 +392,7 @@ func TestCreate(t *testing.T) { Database: ptr.To("test-example"), Role: ptr.To("test-example"), TargetRole: ptr.To("target-role"), - Privileges: v1alpha1.GrantPrivileges{"ALL"}, + Privileges: v1alpha1.GrantPrivileges{"SELECT", "UPDATE"}, ObjectType: ptr.To("TABLE"), }, }, From 65128d0b05d08343e385e3a5f294fd2eafae32da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Fern=C3=A1ndez=20Campo?= Date: Sun, 27 Oct 2024 22:33:09 +0100 Subject: [PATCH 16/17] made revoke more simple by just revoking all, fixed bug on grant query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joaquín Fernández Campo --- .../postgresql/default_privileges/reconciler.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pkg/controller/postgresql/default_privileges/reconciler.go b/pkg/controller/postgresql/default_privileges/reconciler.go index c92869dd..3cc1a514 100644 --- a/pkg/controller/postgresql/default_privileges/reconciler.go +++ b/pkg/controller/postgresql/default_privileges/reconciler.go @@ -170,20 +170,18 @@ func inSchema(params *v1alpha1.DefaultPrivilegesParameters) string { return "" } -func createDefaultPrivilegesQuery(gp v1alpha1.DefaultPrivilegesParameters, q *xsql.Query) { // nolint: gocyclo +func createDefaultPrivilegesQuery(gp v1alpha1.DefaultPrivilegesParameters, q *xsql.Query) { roleName := pq.QuoteIdentifier(*gp.Role) targetRoleName := pq.QuoteIdentifier(*gp.TargetRole) - objectType := objectTypes[*gp.ObjectType] - query := strings.TrimSpace(fmt.Sprintf( - "ALTER DEFAULT PRIVILEGES FOR ROLE %s %s GRANT %s ON %s TO %s %s", + "ALTER DEFAULT PRIVILEGES FOR ROLE %s %s GRANT %s ON %sS TO %s %s", targetRoleName, inSchema(&gp), strings.Join(gp.Privileges.ToStringSlice(), ","), - objectType, + *gp.ObjectType, roleName, withOption(gp.WithOption), )) @@ -197,10 +195,9 @@ func deleteDefaultPrivilegesQuery(gp v1alpha1.DefaultPrivilegesParameters, q *xs objectType := objectTypes[*gp.ObjectType] query := strings.TrimSpace(fmt.Sprintf( - "ALTER DEFAULT PRIVILEGES FOR ROLE %s %s REVOKE %s ON %s TO %s %s", + "ALTER DEFAULT PRIVILEGES FOR ROLE %s %s REVOKE ALL ON %s TO %s %s", targetRoleName, inSchema(&gp), - strings.Join(gp.Privileges.ToStringSlice(), ","), objectType, roleName, withOption(gp.WithOption), From b26300fd5c0b322f126c0889269c38b0e0314c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Fern=C3=A1ndez=20Campo?= Date: Sun, 27 Oct 2024 23:43:19 +0100 Subject: [PATCH 17/17] fixed an issue when searching for default privileges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Joaquín Fernández Campo --- .../default_privileges/reconciler.go | 43 ++++++++++++------- .../default_privileges/reconciler_test.go | 40 +++++++++++++++-- 2 files changed, 64 insertions(+), 19 deletions(-) diff --git a/pkg/controller/postgresql/default_privileges/reconciler.go b/pkg/controller/postgresql/default_privileges/reconciler.go index 3cc1a514..adaf922b 100644 --- a/pkg/controller/postgresql/default_privileges/reconciler.go +++ b/pkg/controller/postgresql/default_privileges/reconciler.go @@ -245,19 +245,39 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex var query xsql.Query selectDefaultPrivilegesQuery(gp, &query) - var grants []string - err := c.db.Scan(ctx, query, &grants) - if err != nil && !xsql.IsNoRows(err) { + var defaultPrivileges []string + + rows, err := c.db.Query(ctx, query) + if xsql.IsNoRows(err) { + return managed.ExternalObservation{ResourceExists: false}, nil + } + + if err != nil { + return managed.ExternalObservation{}, errors.Wrap(err, errSelectDefaultPrivileges) + } + defer rows.Close() + for rows.Next() { + var privilege string + if err := rows.Scan(&privilege); err != nil { + return managed.ExternalObservation{}, errors.Wrap(err, errSelectDefaultPrivileges) + } + defaultPrivileges = append(defaultPrivileges, privilege) + } + + // Check for any errors encountered during iteration + if err := rows.Err(); err != nil { return managed.ExternalObservation{}, errors.Wrap(err, errSelectDefaultPrivileges) } - if len(grants) == 0 { + + // If no default privileges are found, the resource does not exist. + // Maybe this is covered by the xsql.IsNoRows(err) check above? + if len(defaultPrivileges) == 0 { return managed.ExternalObservation{ResourceExists: false}, nil } - // Grants have no way of being 'not up to date' - if they exist, they are up to date cr.SetConditions(xpv1.Available()) - resourceMatches := matchingGrants(grants, gp.Privileges.ToStringSlice()) + resourceMatches := matchingGrants(defaultPrivileges, gp.Privileges.ToStringSlice()) return managed.ExternalObservation{ ResourceLateInitialized: false, // check that the list of grants matches the expected grants @@ -286,15 +306,8 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext err := c.db.ExecTx(ctx, []xsql.Query{ deleteQuery, createQuery, }) - errString := errCreateDefaultPrivileges - if err != nil { - errString = fmt.Sprintf(` - %s - delete: |%s| - create: |%s| - `, errString, deleteQuery.String, createQuery.String) - } - return managed.ExternalCreation{}, errors.Wrap(err, errString) + + return managed.ExternalCreation{}, errors.Wrap(err, errCreateDefaultPrivileges) } func (c *external) Update( diff --git a/pkg/controller/postgresql/default_privileges/reconciler_test.go b/pkg/controller/postgresql/default_privileges/reconciler_test.go index debfa1d6..fe77abae 100644 --- a/pkg/controller/postgresql/default_privileges/reconciler_test.go +++ b/pkg/controller/postgresql/default_privileges/reconciler_test.go @@ -21,6 +21,7 @@ import ( "database/sql" "testing" + "github.com/DATA-DOG/go-sqlmock" "github.com/crossplane-contrib/provider-sql/apis/postgresql/v1alpha1" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -221,6 +222,9 @@ func TestObserve(t *testing.T) { reason: "We should return ResourceExists: false when no default grant is found", fields: fields{ db: mockDB{ + MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) { + return mockRowsToSQLRows(sqlmock.NewRows([]string{})), nil + }, MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { // Default value is empty, so we don't need to do anything here return nil @@ -248,6 +252,12 @@ func TestObserve(t *testing.T) { reason: "We should return any errors encountered while trying to show the default grant", fields: fields{ db: mockDB{ + MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) { + r := sqlmock.NewRows([]string{"PRIVILEGE"}). + AddRow("UPDATE"). + AddRow("SELECT") + return mockRowsToSQLRows(r), errBoom + }, MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { return errBoom }, @@ -275,11 +285,22 @@ func TestObserve(t *testing.T) { reason: "We should return no error if we can find the right permissions in the default grant", fields: fields{ db: mockDB{ - MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { - bv := dest[0].(*[]string) - *bv = []string{"SELECT", "UPDATE"} - return nil + MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) { + r := sqlmock.NewRows([]string{"PRIVILEGE"}). + AddRow("UPDATE"). + AddRow("SELECT") + return mockRowsToSQLRows(r), nil }, + // MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { + // if len(dest) == 0 { + // runtime.Breakpoint() + // return nil + // } + // // populate the dest slice with the expected values + // // so we can compare them in the test + // *dest[0].(*string) = "SELECT" + // return nil + // }, }, }, args: args{ @@ -320,6 +341,17 @@ func TestObserve(t *testing.T) { } } +func mockRowsToSQLRows(mockRows *sqlmock.Rows) *sql.Rows { + db, mock, _ := sqlmock.New() + mock.ExpectQuery("select").WillReturnRows(mockRows) + rows, err := db.Query("select") + if err != nil { + println("%v", err) + return nil + } + return rows +} + func TestCreate(t *testing.T) { errBoom := errors.New("boom")