From 8c691442a8038643f4f424744a212d9c04e2f140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Fern=C3=A1ndez?= <7312236+fernandezcuesta@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:49:02 +0100 Subject: [PATCH] fix: adapt for v2, fix default privileges and add examples and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jesús Fernández <7312236+fernandezcuesta@users.noreply.github.com> --- .../v1alpha1/default_privileges_types.go | 77 +- .../v1alpha1/zz_generated.managed.go | 10 - .../v1alpha1/zz_generated.resolvers.go | 44 + .../v1alpha1/default_privileges_types.go} | 64 +- .../postgresql/v1alpha1/register.go | 9 + .../v1alpha1/zz_generated.deepcopy.go | 162 ++++ .../v1alpha1/zz_generated.managed.go | 40 + .../v1alpha1/zz_generated.managedlist.go | 9 + .../v1alpha1/zz_generated.resolvers.go | 44 + .../postgresql/default_privileges.yaml | 16 - .../cluster/postgresql/defaultprivileges.yaml | 106 ++- .../postgresql/defaultprivileges.yaml | 137 +++ ...l.sql.crossplane.io_defaultprivileges.yaml | 172 +--- ...sql.m.crossplane.io_defaultprivileges.yaml | 413 +++++++++ .../default_privileges/reconciler.go | 119 ++- .../default_privileges/reconciler_test.go | 342 +++++-- .../cluster/postgresql/postgresql.go | 14 +- .../default_privileges/reconciler.go | 312 +++++++ .../default_privileges/reconciler_test.go | 865 ++++++++++++++++++ .../namespaced/postgresql/postgresql.go | 2 + 20 files changed, 2522 insertions(+), 435 deletions(-) rename apis/{cluster/postgresql/v1alpha1/default_grant_types.go => namespaced/postgresql/v1alpha1/default_privileges_types.go} (62%) delete mode 100644 examples/cluster/postgresql/default_privileges.yaml create mode 100644 examples/namespaced/postgresql/defaultprivileges.yaml create mode 100644 package/crds/postgresql.sql.m.crossplane.io_defaultprivileges.yaml rename pkg/controller/{ => cluster}/postgresql/default_privileges/reconciler.go (72%) rename pkg/controller/{ => cluster}/postgresql/default_privileges/reconciler_test.go (61%) create mode 100644 pkg/controller/namespaced/postgresql/default_privileges/reconciler.go create mode 100644 pkg/controller/namespaced/postgresql/default_privileges/reconciler_test.go diff --git a/apis/cluster/postgresql/v1alpha1/default_privileges_types.go b/apis/cluster/postgresql/v1alpha1/default_privileges_types.go index 067df57b..f7e25a98 100644 --- a/apis/cluster/postgresql/v1alpha1/default_privileges_types.go +++ b/apis/cluster/postgresql/v1alpha1/default_privileges_types.go @@ -1,18 +1,19 @@ package v1alpha1 import ( - xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + xpv1 "github.com/crossplane/crossplane-runtime/v2/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. +// 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" // +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" @@ -43,13 +44,15 @@ type DefaultPrivilegesParameters struct { // +optional Privileges GrantPrivileges `json:"privileges,omitempty"` - // TargetRole is the role who owns objects on which the default privileges are granted. + // TargetRole is the role whose future objects will have default privileges applied. + // When this role creates new objects, the specified privileges are automatically + // granted. Maps to FOR ROLE in ALTER DEFAULT PRIVILEGES. // 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 + // +kubebuilder:validation:Enum=table;sequence;function;schema;type // +required ObjectType *string `json:"objectType,omitempty"` @@ -60,8 +63,10 @@ type DefaultPrivilegesParameters struct { // +optional WithOption *GrantOption `json:"withOption,omitempty"` - // Role to which default privileges are granted + // Role is the role that will receive the default privileges (the grantee). + // Maps to TO in ALTER DEFAULT PRIVILEGES ... GRANT ... TO role. // +optional + // +crossplane:generate:reference:type=Role Role *string `json:"role,omitempty"` // RoleRef to which default privileges are granted. @@ -76,6 +81,7 @@ type DefaultPrivilegesParameters struct { // Database in which the default privileges are applied // +optional + // +crossplane:generate:reference:type=Database Database *string `json:"database,omitempty"` // DatabaseRef references the database object this default grant it for. @@ -89,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 @@ -111,52 +107,3 @@ type DefaultPrivilegesList struct { metav1.ListMeta `json:"metadata,omitempty"` Items []DefaultPrivileges `json:"items"` } - -// 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/cluster/postgresql/v1alpha1/zz_generated.managed.go b/apis/cluster/postgresql/v1alpha1/zz_generated.managed.go index 7c2d24f1..f57db3d1 100644 --- a/apis/cluster/postgresql/v1alpha1/zz_generated.managed.go +++ b/apis/cluster/postgresql/v1alpha1/zz_generated.managed.go @@ -78,11 +78,6 @@ func (mg *DefaultPrivileges) GetProviderConfigReference() *xpv1.Reference { return mg.Spec.ProviderConfigReference } -// GetPublishConnectionDetailsTo of this DefaultPrivileges. -func (mg *DefaultPrivileges) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { - return mg.Spec.PublishConnectionDetailsTo -} - // GetWriteConnectionSecretToReference of this DefaultPrivileges. func (mg *DefaultPrivileges) GetWriteConnectionSecretToReference() *xpv1.SecretReference { return mg.Spec.WriteConnectionSecretToReference @@ -108,11 +103,6 @@ func (mg *DefaultPrivileges) SetProviderConfigReference(r *xpv1.Reference) { mg.Spec.ProviderConfigReference = r } -// SetPublishConnectionDetailsTo of this DefaultPrivileges. -func (mg *DefaultPrivileges) SetPublishConnectionDetailsTo(r *xpv1.PublishConnectionDetailsTo) { - mg.Spec.PublishConnectionDetailsTo = r -} - // SetWriteConnectionSecretToReference of this DefaultPrivileges. func (mg *DefaultPrivileges) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { mg.Spec.WriteConnectionSecretToReference = r diff --git a/apis/cluster/postgresql/v1alpha1/zz_generated.resolvers.go b/apis/cluster/postgresql/v1alpha1/zz_generated.resolvers.go index b7fe93db..c179ad33 100644 --- a/apis/cluster/postgresql/v1alpha1/zz_generated.resolvers.go +++ b/apis/cluster/postgresql/v1alpha1/zz_generated.resolvers.go @@ -13,6 +13,50 @@ import ( client "sigs.k8s.io/controller-runtime/pkg/client" ) +// ResolveReferences of this DefaultPrivileges. +func (mg *DefaultPrivileges) ResolveReferences(ctx context.Context, c client.Reader) error { + r := reference.NewAPIResolver(c, mg) + + var rsp reference.ResolutionResponse + var err error + + rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ + CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Role), + Extract: reference.ExternalName(), + Namespace: mg.GetNamespace(), + Reference: mg.Spec.ForProvider.RoleRef, + Selector: mg.Spec.ForProvider.RoleSelector, + To: reference.To{ + List: &RoleList{}, + Managed: &Role{}, + }, + }) + if err != nil { + return errors.Wrap(err, "mg.Spec.ForProvider.Role") + } + mg.Spec.ForProvider.Role = reference.ToPtrValue(rsp.ResolvedValue) + mg.Spec.ForProvider.RoleRef = rsp.ResolvedReference + + rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ + CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Database), + Extract: reference.ExternalName(), + Namespace: mg.GetNamespace(), + Reference: mg.Spec.ForProvider.DatabaseRef, + Selector: mg.Spec.ForProvider.DatabaseSelector, + To: reference.To{ + List: &DatabaseList{}, + Managed: &Database{}, + }, + }) + if err != nil { + return errors.Wrap(err, "mg.Spec.ForProvider.Database") + } + mg.Spec.ForProvider.Database = reference.ToPtrValue(rsp.ResolvedValue) + mg.Spec.ForProvider.DatabaseRef = rsp.ResolvedReference + + return nil +} + // ResolveReferences of this Extension. func (mg *Extension) ResolveReferences(ctx context.Context, c client.Reader) error { r := reference.NewAPIResolver(c, mg) diff --git a/apis/cluster/postgresql/v1alpha1/default_grant_types.go b/apis/namespaced/postgresql/v1alpha1/default_privileges_types.go similarity index 62% rename from apis/cluster/postgresql/v1alpha1/default_grant_types.go rename to apis/namespaced/postgresql/v1alpha1/default_privileges_types.go index 6ca47f48..9cd87afd 100644 --- a/apis/cluster/postgresql/v1alpha1/default_grant_types.go +++ b/apis/namespaced/postgresql/v1alpha1/default_privileges_types.go @@ -1,13 +1,9 @@ package v1alpha1 import ( - "context" - - xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" - "github.com/crossplane/crossplane-runtime/pkg/reference" - "github.com/pkg/errors" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + xpv2 "github.com/crossplane/crossplane-runtime/v2/apis/common/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" ) // +kubebuilder:object:root=true @@ -33,8 +29,8 @@ type DefaultPrivileges struct { // A DefaultPrivilegesSpec defines the desired state of a Default Grant. type DefaultPrivilegesSpec struct { - xpv1.ResourceSpec `json:",inline"` - ForProvider DefaultPrivilegesParameters `json:"forProvider"` + xpv2.ManagedResourceSpec `json:",inline"` + ForProvider DefaultPrivilegesParameters `json:"forProvider"` } // A DefaultPrivilegesStatus represents the observed state of a Grant. @@ -49,7 +45,9 @@ type DefaultPrivilegesParameters struct { // +optional Privileges GrantPrivileges `json:"privileges,omitempty"` - // TargetRole is the role who owns objects on which the default privileges are granted. + // TargetRole is the role whose future objects will have default privileges applied. + // When this role creates new objects, the specified privileges are automatically + // granted. Maps to FOR ROLE in ALTER DEFAULT PRIVILEGES. // See https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html // +required TargetRole *string `json:"targetRole"` @@ -66,33 +64,36 @@ type DefaultPrivilegesParameters struct { // +optional WithOption *GrantOption `json:"withOption,omitempty"` - // Role to which default privileges are granted + // Role is the role that will receive the default privileges (the grantee). + // Maps to TO in ALTER DEFAULT PRIVILEGES ... GRANT ... TO role. // +optional + // +crossplane:generate:reference:type=Role Role *string `json:"role,omitempty"` // RoleRef to which default privileges are granted. // +immutable // +optional - RoleRef *xpv1.Reference `json:"roleRef,omitempty"` + RoleRef *xpv1.NamespacedReference `json:"roleRef,omitempty"` // RoleSelector selects a reference to a Role this default grant is for. // +immutable // +optional - RoleSelector *xpv1.Selector `json:"roleSelector,omitempty"` + RoleSelector *xpv1.NamespacedSelector `json:"roleSelector,omitempty"` // Database in which the default privileges are applied // +optional + // +crossplane:generate:reference:type=Database Database *string `json:"database,omitempty"` // DatabaseRef references the database object this default grant it for. // +immutable // +optional - DatabaseRef *xpv1.Reference `json:"databaseRef,omitempty"` + DatabaseRef *xpv1.NamespacedReference `json:"databaseRef,omitempty"` // DatabaseSelector selects a reference to a Database this grant is for. // +immutable // +optional - DatabaseSelector *xpv1.Selector `json:"databaseSelector,omitempty"` + DatabaseSelector *xpv1.NamespacedSelector `json:"databaseSelector,omitempty"` // Schema in which the default privileges are applied // +required @@ -107,38 +108,3 @@ type DefaultPrivilegesList struct { metav1.ListMeta `json:"metadata,omitempty"` Items []DefaultPrivileges `json:"items"` } - -// 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 - - return nil -} diff --git a/apis/namespaced/postgresql/v1alpha1/register.go b/apis/namespaced/postgresql/v1alpha1/register.go index e11d0bd1..c6de743c 100644 --- a/apis/namespaced/postgresql/v1alpha1/register.go +++ b/apis/namespaced/postgresql/v1alpha1/register.go @@ -95,6 +95,14 @@ var ( GrantGroupVersionKind = SchemeGroupVersion.WithKind(GrantKind) ) +// DefaultPrivileges type metadata. +var ( + DefaultPrivilegesKind = reflect.TypeOf(DefaultPrivileges{}).Name() + DefaultPrivilegesGroupKind = schema.GroupKind{Group: Group, Kind: DefaultPrivilegesKind}.String() + DefaultPrivilegesKindAPIVersion = DefaultPrivilegesKind + "." + SchemeGroupVersion.String() + DefaultPrivilegesGroupVersionKind = SchemeGroupVersion.WithKind(DefaultPrivilegesKind) +) + // Schema type metadata. var ( SchemaKind = reflect.TypeOf(Schema{}).Name() @@ -112,4 +120,5 @@ func init() { SchemeBuilder.Register(&Grant{}, &GrantList{}) SchemeBuilder.Register(&Extension{}, &ExtensionList{}) SchemeBuilder.Register(&Schema{}, &SchemaList{}) + SchemeBuilder.Register(&DefaultPrivileges{}, &DefaultPrivilegesList{}) } diff --git a/apis/namespaced/postgresql/v1alpha1/zz_generated.deepcopy.go b/apis/namespaced/postgresql/v1alpha1/zz_generated.deepcopy.go index 23214a34..b84d483a 100644 --- a/apis/namespaced/postgresql/v1alpha1/zz_generated.deepcopy.go +++ b/apis/namespaced/postgresql/v1alpha1/zz_generated.deepcopy.go @@ -277,6 +277,168 @@ 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 *DefaultPrivileges) DeepCopyInto(out *DefaultPrivileges) { + *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 DefaultPrivileges. +func (in *DefaultPrivileges) DeepCopy() *DefaultPrivileges { + if in == nil { + return nil + } + out := new(DefaultPrivileges) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DefaultPrivileges) 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 *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([]DefaultPrivileges, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// 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(DefaultPrivilegesList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DefaultPrivilegesList) 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 *DefaultPrivilegesParameters) DeepCopyInto(out *DefaultPrivilegesParameters) { + *out = *in + if in.Privileges != nil { + in, out := &in.Privileges, &out.Privileges + *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) + **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.NamespacedReference) + (*in).DeepCopyInto(*out) + } + if in.RoleSelector != nil { + in, out := &in.RoleSelector, &out.RoleSelector + *out = new(v1.NamespacedSelector) + (*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.NamespacedReference) + (*in).DeepCopyInto(*out) + } + if in.DatabaseSelector != nil { + in, out := &in.DatabaseSelector, &out.DatabaseSelector + *out = new(v1.NamespacedSelector) + (*in).DeepCopyInto(*out) + } + if in.Schema != nil { + in, out := &in.Schema, &out.Schema + *out = new(string) + **out = **in + } +} + +// 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(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 *DefaultPrivilegesSpec) DeepCopyInto(out *DefaultPrivilegesSpec) { + *out = *in + in.ManagedResourceSpec.DeepCopyInto(&out.ManagedResourceSpec) + in.ForProvider.DeepCopyInto(&out.ForProvider) +} + +// 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(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 *DefaultPrivilegesStatus) DeepCopyInto(out *DefaultPrivilegesStatus) { + *out = *in + in.ResourceStatus.DeepCopyInto(&out.ResourceStatus) +} + +// 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(DefaultPrivilegesStatus) + 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/namespaced/postgresql/v1alpha1/zz_generated.managed.go b/apis/namespaced/postgresql/v1alpha1/zz_generated.managed.go index c2f14eef..98fb3c6c 100644 --- a/apis/namespaced/postgresql/v1alpha1/zz_generated.managed.go +++ b/apis/namespaced/postgresql/v1alpha1/zz_generated.managed.go @@ -48,6 +48,46 @@ func (mg *Database) SetWriteConnectionSecretToReference(r *xpv1.LocalSecretRefer mg.Spec.WriteConnectionSecretToReference = r } +// GetCondition of this DefaultPrivileges. +func (mg *DefaultPrivileges) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetManagementPolicies of this DefaultPrivileges. +func (mg *DefaultPrivileges) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this DefaultPrivileges. +func (mg *DefaultPrivileges) GetProviderConfigReference() *xpv1.ProviderConfigReference { + return mg.Spec.ProviderConfigReference +} + +// GetWriteConnectionSecretToReference of this DefaultPrivileges. +func (mg *DefaultPrivileges) GetWriteConnectionSecretToReference() *xpv1.LocalSecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this DefaultPrivileges. +func (mg *DefaultPrivileges) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetManagementPolicies of this DefaultPrivileges. +func (mg *DefaultPrivileges) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this DefaultPrivileges. +func (mg *DefaultPrivileges) SetProviderConfigReference(r *xpv1.ProviderConfigReference) { + mg.Spec.ProviderConfigReference = r +} + +// SetWriteConnectionSecretToReference of this DefaultPrivileges. +func (mg *DefaultPrivileges) SetWriteConnectionSecretToReference(r *xpv1.LocalSecretReference) { + 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/namespaced/postgresql/v1alpha1/zz_generated.managedlist.go b/apis/namespaced/postgresql/v1alpha1/zz_generated.managedlist.go index d6d2f04d..1f0def26 100644 --- a/apis/namespaced/postgresql/v1alpha1/zz_generated.managedlist.go +++ b/apis/namespaced/postgresql/v1alpha1/zz_generated.managedlist.go @@ -17,6 +17,15 @@ func (l *DatabaseList) GetItems() []resource.Managed { return items } +// 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] + } + return items +} + // GetItems of this ExtensionList. func (l *ExtensionList) GetItems() []resource.Managed { items := make([]resource.Managed, len(l.Items)) diff --git a/apis/namespaced/postgresql/v1alpha1/zz_generated.resolvers.go b/apis/namespaced/postgresql/v1alpha1/zz_generated.resolvers.go index 5d5f279c..a52d046e 100644 --- a/apis/namespaced/postgresql/v1alpha1/zz_generated.resolvers.go +++ b/apis/namespaced/postgresql/v1alpha1/zz_generated.resolvers.go @@ -13,6 +13,50 @@ import ( client "sigs.k8s.io/controller-runtime/pkg/client" ) +// ResolveReferences of this DefaultPrivileges. +func (mg *DefaultPrivileges) ResolveReferences(ctx context.Context, c client.Reader) error { + r := reference.NewAPINamespacedResolver(c, mg) + + var rsp reference.NamespacedResolutionResponse + var err error + + rsp, err = r.Resolve(ctx, reference.NamespacedResolutionRequest{ + CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Role), + Extract: reference.ExternalName(), + Namespace: mg.GetNamespace(), + Reference: mg.Spec.ForProvider.RoleRef, + Selector: mg.Spec.ForProvider.RoleSelector, + To: reference.To{ + List: &RoleList{}, + Managed: &Role{}, + }, + }) + if err != nil { + return errors.Wrap(err, "mg.Spec.ForProvider.Role") + } + mg.Spec.ForProvider.Role = reference.ToPtrValue(rsp.ResolvedValue) + mg.Spec.ForProvider.RoleRef = rsp.ResolvedReference + + rsp, err = r.Resolve(ctx, reference.NamespacedResolutionRequest{ + CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.Database), + Extract: reference.ExternalName(), + Namespace: mg.GetNamespace(), + Reference: mg.Spec.ForProvider.DatabaseRef, + Selector: mg.Spec.ForProvider.DatabaseSelector, + To: reference.To{ + List: &DatabaseList{}, + Managed: &Database{}, + }, + }) + if err != nil { + return errors.Wrap(err, "mg.Spec.ForProvider.Database") + } + mg.Spec.ForProvider.Database = reference.ToPtrValue(rsp.ResolvedValue) + mg.Spec.ForProvider.DatabaseRef = rsp.ResolvedReference + + return nil +} + // ResolveReferences of this Extension. func (mg *Extension) ResolveReferences(ctx context.Context, c client.Reader) error { r := reference.NewAPINamespacedResolver(c, mg) diff --git a/examples/cluster/postgresql/default_privileges.yaml b/examples/cluster/postgresql/default_privileges.yaml deleted file mode 100644 index c9135d31..00000000 --- a/examples/cluster/postgresql/default_privileges.yaml +++ /dev/null @@ -1,16 +0,0 @@ -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/examples/cluster/postgresql/defaultprivileges.yaml b/examples/cluster/postgresql/defaultprivileges.yaml index 6d851366..8218f68f 100644 --- a/examples/cluster/postgresql/defaultprivileges.yaml +++ b/examples/cluster/postgresql/defaultprivileges.yaml @@ -1,17 +1,113 @@ apiVersion: postgresql.sql.crossplane.io/v1alpha1 +kind: Role +metadata: + name: owner-role +spec: + deletionPolicy: Orphan + writeConnectionSecretToRef: + name: owner-role-secret + namespace: default + forProvider: + privileges: + createDb: true + login: true + createRole: true + inherit: true +--- +apiVersion: postgresql.sql.crossplane.io/v1alpha1 +kind: Role +metadata: + name: writer-role +spec: + forProvider: + connectionLimit: 10 + privileges: + login: true + writeConnectionSecretToRef: + name: writer-role-secret + namespace: default +--- +apiVersion: postgresql.sql.crossplane.io/v1alpha1 +kind: Role +metadata: + name: reader-role +spec: + forProvider: + connectionLimit: 10 + privileges: + login: true + writeConnectionSecretToRef: + name: reader-role-secret + namespace: default +--- +# When owner-role creates new tables, grant full DML to writer-role +apiVersion: postgresql.sql.crossplane.io/v1alpha1 kind: DefaultPrivileges metadata: - name: default-grant-role-1-on-database + name: writer-tables spec: forProvider: + objectType: table + roleRef: + name: writer-role + targetRole: owner-role privileges: - SELECT + - INSERT + - UPDATE + - DELETE + - TRUNCATE + schema: public + databaseRef: + name: example +--- +# When owner-role creates new functions, grant EXECUTE to writer-role +apiVersion: postgresql.sql.crossplane.io/v1alpha1 +kind: DefaultPrivileges +metadata: + name: writer-functions +spec: + forProvider: + objectType: function roleRef: - name: reader-role - targetRoleRef: - name: example-role - schemaRef: + name: writer-role + targetRole: owner-role + privileges: + - EXECUTE + schema: public + databaseRef: name: example +--- +# When owner-role creates new tables, grant SELECT to reader-role +apiVersion: postgresql.sql.crossplane.io/v1alpha1 +kind: DefaultPrivileges +metadata: + name: reader-tables +spec: + forProvider: + objectType: table + roleRef: + name: reader-role + targetRole: owner-role + privileges: + - SELECT + schema: public databaseRef: name: example --- +# When owner-role creates new functions, grant EXECUTE to reader-role +apiVersion: postgresql.sql.crossplane.io/v1alpha1 +kind: DefaultPrivileges +metadata: + name: reader-functions +spec: + forProvider: + objectType: function + roleRef: + name: reader-role + targetRole: owner-role + privileges: + - EXECUTE + schema: public + databaseRef: + name: example diff --git a/examples/namespaced/postgresql/defaultprivileges.yaml b/examples/namespaced/postgresql/defaultprivileges.yaml new file mode 100644 index 00000000..166e7276 --- /dev/null +++ b/examples/namespaced/postgresql/defaultprivileges.yaml @@ -0,0 +1,137 @@ +apiVersion: postgresql.sql.m.crossplane.io/v1alpha1 +kind: Role +metadata: + name: owner-role + namespace: default +spec: + forProvider: + privileges: + createDb: true + login: true + createRole: true + inherit: true + writeConnectionSecretToRef: + name: owner-role-secret + providerConfigRef: + kind: ProviderConfig + name: default +--- +apiVersion: postgresql.sql.m.crossplane.io/v1alpha1 +kind: Role +metadata: + name: writer-role + namespace: default +spec: + forProvider: + connectionLimit: 10 + privileges: + login: true + writeConnectionSecretToRef: + name: writer-role-secret + providerConfigRef: + kind: ProviderConfig + name: default +--- +apiVersion: postgresql.sql.m.crossplane.io/v1alpha1 +kind: Role +metadata: + name: reader-role + namespace: default +spec: + forProvider: + connectionLimit: 10 + privileges: + login: true + writeConnectionSecretToRef: + name: reader-role-secret + providerConfigRef: + kind: ProviderConfig + name: default +--- +# When owner-role creates new tables, grant full DML to writer-role +apiVersion: postgresql.sql.m.crossplane.io/v1alpha1 +kind: DefaultPrivileges +metadata: + name: writer-tables + namespace: default +spec: + forProvider: + objectType: table + roleRef: + name: writer-role + targetRole: owner-role + privileges: + - SELECT + - INSERT + - UPDATE + - DELETE + - TRUNCATE + schema: public + databaseRef: + name: example + providerConfigRef: + kind: ProviderConfig + name: default +--- +# When owner-role creates new functions, grant EXECUTE to writer-role +apiVersion: postgresql.sql.m.crossplane.io/v1alpha1 +kind: DefaultPrivileges +metadata: + name: writer-functions + namespace: default +spec: + forProvider: + objectType: function + roleRef: + name: writer-role + targetRole: owner-role + privileges: + - EXECUTE + schema: public + databaseRef: + name: example + providerConfigRef: + kind: ProviderConfig + name: default +--- +# When owner-role creates new tables, grant SELECT to reader-role +apiVersion: postgresql.sql.m.crossplane.io/v1alpha1 +kind: DefaultPrivileges +metadata: + name: reader-tables + namespace: default +spec: + forProvider: + objectType: table + roleRef: + name: reader-role + targetRole: owner-role + privileges: + - SELECT + schema: public + databaseRef: + name: example + providerConfigRef: + kind: ProviderConfig + name: default +--- +# When owner-role creates new functions, grant EXECUTE to reader-role +apiVersion: postgresql.sql.m.crossplane.io/v1alpha1 +kind: DefaultPrivileges +metadata: + name: reader-functions + namespace: default +spec: + forProvider: + objectType: function + roleRef: + name: reader-role + targetRole: owner-role + privileges: + - EXECUTE + schema: public + databaseRef: + name: example + providerConfigRef: + kind: ProviderConfig + name: default diff --git a/package/crds/postgresql.sql.crossplane.io_defaultprivileges.yaml b/package/crds/postgresql.sql.crossplane.io_defaultprivileges.yaml index a677dc95..11a0f4ba 100644 --- a/package/crds/postgresql.sql.crossplane.io_defaultprivileges.yaml +++ b/package/crds/postgresql.sql.crossplane.io_defaultprivileges.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.18.0 name: defaultprivileges.postgresql.sql.crossplane.io spec: group: postgresql.sql.crossplane.io @@ -46,7 +46,8 @@ spec: name: v1alpha1 schema: openAPIV3Schema: - description: A Grant represents the declarative state of a PostgreSQL grant. + description: A DefaultPrivileges represents the declarative state of a PostgreSQL + DefaultPrivileges. properties: apiVersion: description: |- @@ -186,7 +187,9 @@ spec: minItems: 1 type: array role: - description: Role to which default privileges are granted + description: |- + Role is the role that will receive the default privileges (the grantee). + Maps to TO in ALTER DEFAULT PRIVILEGES ... GRANT ... TO role. type: string roleRef: description: RoleRef to which default privileges are granted. @@ -266,85 +269,11 @@ spec: 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 targetRole: description: |- - TargetRole is the role who owns objects on which the default privileges are granted. + TargetRole is the role whose future objects will have default privileges applied. + When this role creates new objects, the specified privileges are automatically + granted. Maps to FOR ROLE in ALTER DEFAULT PRIVILEGES. See https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html type: string withOption: @@ -357,6 +286,8 @@ spec: - GRANT type: string required: + - objectType + - schema - targetRole type: object managementPolicies: @@ -425,93 +356,12 @@ spec: 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. diff --git a/package/crds/postgresql.sql.m.crossplane.io_defaultprivileges.yaml b/package/crds/postgresql.sql.m.crossplane.io_defaultprivileges.yaml new file mode 100644 index 00000000..8fce645c --- /dev/null +++ b/package/crds/postgresql.sql.m.crossplane.io_defaultprivileges.yaml @@ -0,0 +1,413 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: defaultprivileges.postgresql.sql.m.crossplane.io +spec: + group: postgresql.sql.m.crossplane.io + names: + categories: + - crossplane + - managed + - sql + kind: DefaultPrivileges + listKind: DefaultPrivilegesList + plural: defaultprivileges + singular: defaultprivileges + 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.targetRole + name: TARGET_ROLE + type: string + - jsonPath: .spec.forProvider.schema + name: SCHEMA + type: string + - jsonPath: .spec.forProvider.database + name: DATABASE + type: string + - jsonPath: .spec.forProvider.privileges + name: PRIVILEGES + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: A DefaultPrivileges represents the declarative state of a PostgreSQL + DefaultPrivileges. + 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 DefaultPrivilegesSpec defines the desired state of a Default + Grant. + properties: + forProvider: + description: DefaultPrivilegesParameters 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 + namespace: + description: Namespace 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 + namespace: + description: Namespace for the selector + type: string + 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 + objectType: + description: ObjectType to which the privileges are granted. + enum: + - table + - sequence + - function + - schema + - type + type: string + 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 is the role that will receive the default privileges (the grantee). + Maps to TO in ALTER DEFAULT PRIVILEGES ... GRANT ... TO role. + type: string + roleRef: + description: RoleRef to which default privileges are granted. + properties: + name: + description: Name of the referenced object. + type: string + namespace: + description: Namespace 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 + 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 + namespace: + description: Namespace for the selector + type: string + 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 + targetRole: + description: |- + TargetRole is the role whose future objects will have default privileges applied. + When this role creates new objects, the specified privileges are automatically + granted. Maps to FOR ROLE in ALTER DEFAULT PRIVILEGES. + See https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html + type: string + 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 + required: + - objectType + - schema + - targetRole + 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. + 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: + kind: ClusterProviderConfig + 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: + kind: + description: Kind of the referenced object. + type: string + name: + description: Name of the referenced object. + type: string + required: + - kind + - 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. + properties: + name: + description: Name of the secret. + type: string + required: + - name + type: object + required: + - forProvider + type: object + status: + description: A DefaultPrivilegesStatus 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_privileges/reconciler.go b/pkg/controller/cluster/postgresql/default_privileges/reconciler.go similarity index 72% rename from pkg/controller/postgresql/default_privileges/reconciler.go rename to pkg/controller/cluster/postgresql/default_privileges/reconciler.go index be7007ac..6581e1d0 100644 --- a/pkg/controller/postgresql/default_privileges/reconciler.go +++ b/pkg/controller/cluster/postgresql/default_privileges/reconciler.go @@ -30,13 +30,14 @@ import ( "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" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + xpcontroller "github.com/crossplane/crossplane-runtime/v2/pkg/controller" + "github.com/crossplane/crossplane-runtime/v2/pkg/event" + "github.com/crossplane/crossplane-runtime/v2/pkg/feature" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + + "github.com/crossplane-contrib/provider-sql/apis/cluster/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" @@ -48,16 +49,12 @@ const ( errNoSecretRef = "ProviderConfig does not reference a credentials Secret" errGetSecret = "cannot get credentials Secret" - 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 ) @@ -66,14 +63,21 @@ const ( func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { name := managed.ControllerName(v1alpha1.DefaultPrivilegesGroupKind) - t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{}) - r := managed.NewReconciler(mgr, - resource.ManagedKind(v1alpha1.DefaultPrivilegesGroupVersionKind), - managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newDB: postgresql.New}), + t := resource.NewLegacyProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{}) + + reconcilerOptions := []managed.ReconcilerOption{ + managed.WithTypedExternalConnector(&connector{kube: mgr.GetClient(), track: t.Track, newDB: postgresql.New}), managed.WithLogger(o.Logger.WithValues("controller", name)), managed.WithPollInterval(o.PollInterval), - managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) - + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), + } + if o.Features.Enabled(feature.EnableBetaManagementPolicies) { + reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) + } + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1alpha1.DefaultPrivilegesGroupVersionKind), + reconcilerOptions..., + ) return ctrl.NewControllerManagedBy(mgr). Named(name). For(&v1alpha1.DefaultPrivileges{}). @@ -85,24 +89,19 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { type connector struct { kube client.Client - usage resource.Tracker + track func(ctx context.Context, mg resource.LegacyManaged) error 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.DefaultPrivileges) - if !ok { - return nil, errors.New(errNotDefaultPrivileges) - } - - if err := c.usage.Track(ctx, mg); err != nil { +func (c *connector) Connect(ctx context.Context, mg *v1alpha1.DefaultPrivileges) (managed.TypedExternalClient[*v1alpha1.DefaultPrivileges], error) { + if err := c.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 { + if err := c.kube.Get(ctx, types.NamespacedName{Name: mg.GetProviderConfigReference().Name}, pc); err != nil { return nil, errors.Wrap(err, errGetPC) } @@ -118,15 +117,19 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E 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 + + // Connect to the specific database if provided, otherwise use the default. + // ALTER DEFAULT PRIVILEGES is per-database, so we must connect to the target database. + database := pc.Spec.DefaultDatabase + if mg.Spec.ForProvider.Database != nil { + database = *mg.Spec.ForProvider.Database + } + + return &external{db: c.newDB(s.Data, database, clients.ToString(pc.Spec.SSLMode))}, nil } type external struct { - db xsql.DB - kube client.Client + db xsql.DB } var ( @@ -192,15 +195,13 @@ func createDefaultPrivilegesQuery(gp v1alpha1.DefaultPrivilegesParameters, q *xs 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 TO %s %s", + "ALTER DEFAULT PRIVILEGES FOR ROLE %s %s REVOKE ALL ON %sS FROM %s", targetRoleName, inSchema(&gp), - objectType, + *gp.ObjectType, roleName, - withOption(gp.WithOption), )) q.String = query @@ -223,25 +224,25 @@ func matchingGrants(currentGrants []string, specGrants []string) bool { return true } -func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { //nolint:gocyclo - cr, ok := mg.(*v1alpha1.DefaultPrivileges) - if !ok { - return managed.ExternalObservation{}, errors.New(errNotDefaultPrivileges) - } - if cr.Spec.ForProvider.Role == nil { +func (c *external) Disconnect(ctx context.Context) error { + return nil +} + +func (c *external) Observe(ctx context.Context, mg *v1alpha1.DefaultPrivileges) (managed.ExternalObservation, error) { //nolint:gocyclo + if mg.Spec.ForProvider.Role == nil { return managed.ExternalObservation{}, errors.New(errNoRole) } - if cr.Spec.ForProvider.TargetRole == nil { + if mg.Spec.ForProvider.TargetRole == nil { return managed.ExternalObservation{}, errors.New(errNoTargetRole) } - if cr.Spec.ForProvider.ObjectType == nil { + if mg.Spec.ForProvider.ObjectType == nil { return managed.ExternalObservation{}, errors.New(errNoObjectType) } - gp := cr.Spec.ForProvider + gp := mg.Spec.ForProvider var query xsql.Query selectDefaultPrivilegesQuery(gp, &query) @@ -275,7 +276,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex return managed.ExternalObservation{ResourceExists: false}, nil } - cr.SetConditions(xpv1.Available()) + mg.SetConditions(xpv1.Available()) resourceMatches := matchingGrants(defaultPrivileges, gp.Privileges.ToStringSlice()) return managed.ExternalObservation{ @@ -289,19 +290,15 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex }, nil } -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(errNotDefaultPrivileges) - } +func (c *external) Create(ctx context.Context, mg *v1alpha1.DefaultPrivileges) (managed.ExternalCreation, error) { - cr.SetConditions(xpv1.Creating()) + mg.SetConditions(xpv1.Creating()) var createQuery xsql.Query - createDefaultPrivilegesQuery(cr.Spec.ForProvider, &createQuery) + createDefaultPrivilegesQuery(mg.Spec.ForProvider, &createQuery) var deleteQuery xsql.Query - deleteDefaultPrivilegesQuery(cr.Spec.ForProvider, &deleteQuery) + deleteDefaultPrivilegesQuery(mg.Spec.ForProvider, &deleteQuery) err := c.db.ExecTx(ctx, []xsql.Query{ deleteQuery, createQuery, @@ -311,22 +308,18 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext } func (c *external) Update( - ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { + ctx context.Context, mg *v1alpha1.DefaultPrivileges) (managed.ExternalUpdate, error) { // Update is a no-op, as permissions are fully revoked and then granted in the Create function, // 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.DefaultPrivileges) - if !ok { - return errors.New(errNotDefaultPrivileges) - } +func (c *external) Delete(ctx context.Context, mg *v1alpha1.DefaultPrivileges) (managed.ExternalDelete, error) { var query xsql.Query - cr.SetConditions(xpv1.Deleting()) + mg.SetConditions(xpv1.Deleting()) - deleteDefaultPrivilegesQuery(cr.Spec.ForProvider, &query) + deleteDefaultPrivilegesQuery(mg.Spec.ForProvider, &query) - return errors.Wrap(c.db.Exec(ctx, query), errRevokeDefaultPrivileges) + return managed.ExternalDelete{}, errors.Wrap(c.db.Exec(ctx, query), errRevokeDefaultPrivileges) } diff --git a/pkg/controller/postgresql/default_privileges/reconciler_test.go b/pkg/controller/cluster/postgresql/default_privileges/reconciler_test.go similarity index 61% rename from pkg/controller/postgresql/default_privileges/reconciler_test.go rename to pkg/controller/cluster/postgresql/default_privileges/reconciler_test.go index fe77abae..9c4bc538 100644 --- a/pkg/controller/postgresql/default_privileges/reconciler_test.go +++ b/pkg/controller/cluster/postgresql/default_privileges/reconciler_test.go @@ -19,10 +19,11 @@ package default_privileges import ( "context" "database/sql" + "strings" "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/crossplane-contrib/provider-sql/apis/postgresql/v1alpha1" + "github.com/crossplane-contrib/provider-sql/apis/cluster/postgresql/v1alpha1" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/pkg/errors" @@ -30,10 +31,10 @@ import ( "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" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" ) @@ -71,13 +72,13 @@ func TestConnect(t *testing.T) { type fields struct { kube client.Client - usage resource.Tracker + track func(context.Context, resource.LegacyManaged) error newDB func(creds map[string][]byte, database string, sslmode string) xsql.DB } type args struct { ctx context.Context - mg resource.Managed + mg *v1alpha1.DefaultPrivileges } cases := map[string]struct { @@ -86,17 +87,10 @@ func TestConnect(t *testing.T) { args args want error }{ - "ErrNotGrant": { - reason: "An error should be returned if the managed resource is not a *DefaultPrivileges", - args: args{ - mg: nil, - }, - want: errors.New(errNotDefaultPrivileges), - }, "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 }), + track: func(ctx context.Context, mg resource.LegacyManaged) error { return errBoom }, }, args: args{ mg: &v1alpha1.DefaultPrivileges{}, @@ -109,7 +103,7 @@ func TestConnect(t *testing.T) { kube: &test.MockClient{ MockGet: test.NewMockGetFn(errBoom), }, - usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), + track: func(ctx context.Context, mg resource.LegacyManaged) error { return nil }, }, args: args{ mg: &v1alpha1.DefaultPrivileges{ @@ -131,7 +125,7 @@ func TestConnect(t *testing.T) { // 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 }), + track: func(ctx context.Context, mg resource.LegacyManaged) error { return nil }, }, args: args{ mg: &v1alpha1.DefaultPrivileges{ @@ -158,7 +152,7 @@ func TestConnect(t *testing.T) { return nil }), }, - usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), + track: func(ctx context.Context, mg resource.LegacyManaged) error { return nil }, }, args: args{ mg: &v1alpha1.DefaultPrivileges{ @@ -175,7 +169,7 @@ func TestConnect(t *testing.T) { 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} + e := &connector{kube: tc.fields.kube, track: tc.fields.track, 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) @@ -184,6 +178,85 @@ func TestConnect(t *testing.T) { } } +func TestConnectDatabaseSelection(t *testing.T) { + type args struct { + mg *v1alpha1.DefaultPrivileges + } + + type want struct { + database string + } + + cases := map[string]struct { + reason string + args args + want want + }{ + "UsesForProviderDatabase": { + reason: "Connect should use forProvider.database when specified, not the ProviderConfig default", + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ResourceSpec: xpv1.ResourceSpec{ + ProviderConfigReference: &xpv1.Reference{}, + }, + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + Database: ptr.To("mydb"), + }, + }, + }, + }, + want: want{database: "mydb"}, + }, + "FallsBackToProviderConfigDefault": { + reason: "Connect should use the ProviderConfig's DefaultDatabase when forProvider.database is nil", + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ResourceSpec: xpv1.ResourceSpec{ + ProviderConfigReference: &xpv1.Reference{}, + }, + ForProvider: v1alpha1.DefaultPrivilegesParameters{}, + }, + }, + }, + want: want{database: "default-db"}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + var gotDatabase string + e := &connector{ + kube: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + switch o := obj.(type) { + case *v1alpha1.ProviderConfig: + o.Spec.DefaultDatabase = "default-db" + o.Spec.Credentials.ConnectionSecretRef = &xpv1.SecretReference{} + case *corev1.Secret: + // Return empty secret data + } + return nil + }), + }, + track: func(ctx context.Context, mg resource.LegacyManaged) error { return nil }, + newDB: func(creds map[string][]byte, database string, sslmode string) xsql.DB { + gotDatabase = database + return mockDB{} + }, + } + _, err := e.Connect(context.Background(), tc.args.mg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if diff := cmp.Diff(tc.want.database, gotDatabase); diff != "" { + t.Errorf("\n%s\ne.Connect(...) database: -want, +got:\n%s\n", tc.reason, diff) + } + }) + } +} + func TestObserve(t *testing.T) { errBoom := errors.New("boom") // goa := v1alpha1.GrantOptionAdmin @@ -195,7 +268,7 @@ func TestObserve(t *testing.T) { type args struct { ctx context.Context - mg resource.Managed + mg *v1alpha1.DefaultPrivileges } type want struct { @@ -209,15 +282,6 @@ func TestObserve(t *testing.T) { args args want want }{ - "ErrNotGrant": { - reason: "An error should be returned if the managed resource is not a *DefaultPrivileges", - args: args{ - mg: nil, - }, - want: want{ - err: errors.New(errNotDefaultPrivileges), - }, - }, "SuccessNoGrant": { reason: "We should return ResourceExists: false when no default grant is found", fields: fields{ @@ -291,16 +355,6 @@ func TestObserve(t *testing.T) { 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{ @@ -325,6 +379,97 @@ func TestObserve(t *testing.T) { err: nil, }, }, + "ErrNoRole": { + reason: "An error should be returned when role is nil", + fields: fields{ + db: mockDB{}, + }, + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + TargetRole: ptr.To("target-role"), + ObjectType: ptr.To("table"), + Privileges: v1alpha1.GrantPrivileges{"SELECT"}, + }, + }, + }, + }, + want: want{ + err: errors.New(errNoRole), + }, + }, + "ErrNoTargetRole": { + reason: "An error should be returned when targetRole is nil", + fields: fields{ + db: mockDB{}, + }, + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + Role: ptr.To("testrole"), + ObjectType: ptr.To("table"), + Privileges: v1alpha1.GrantPrivileges{"SELECT"}, + }, + }, + }, + }, + want: want{ + err: errors.New(errNoTargetRole), + }, + }, + "ErrNoObjectType": { + reason: "An error should be returned when objectType is nil", + fields: fields{ + db: mockDB{}, + }, + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + Role: ptr.To("testrole"), + TargetRole: ptr.To("target-role"), + Privileges: v1alpha1.GrantPrivileges{"SELECT"}, + }, + }, + }, + }, + want: want{ + err: errors.New(errNoObjectType), + }, + }, + "PrivilegesMismatchTriggersRecreate": { + reason: "When DB has different privileges than spec, ResourceExists should be false to trigger re-create", + fields: fields{ + db: mockDB{ + MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) { + r := sqlmock.NewRows([]string{"PRIVILEGE"}). + AddRow("SELECT") + return mockRowsToSQLRows(r), nil + }, + }, + }, + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + Database: ptr.To("testdb"), + Role: ptr.To("testrole"), + TargetRole: ptr.To("target-role"), + ObjectType: ptr.To("table"), + Privileges: v1alpha1.GrantPrivileges{"SELECT", "UPDATE"}, + }, + }, + }, + }, + want: want{ + o: managed.ExternalObservation{ + ResourceExists: false, + ResourceUpToDate: false, + }, + }, + }, } for name, tc := range cases { @@ -354,6 +499,7 @@ func mockRowsToSQLRows(mockRows *sqlmock.Rows) *sql.Rows { func TestCreate(t *testing.T) { errBoom := errors.New("boom") + gog := v1alpha1.GrantOptionGrant type fields struct { db xsql.DB @@ -361,7 +507,7 @@ func TestCreate(t *testing.T) { type args struct { ctx context.Context - mg resource.Managed + mg *v1alpha1.DefaultPrivileges } type want struct { @@ -375,15 +521,6 @@ func TestCreate(t *testing.T) { args args want want }{ - "ErrNotGrant": { - reason: "An error should be returned if the managed resource is not a *DefaultPrivileges", - args: args{ - mg: nil, - }, - want: want{ - err: errors.New(errNotDefaultPrivileges), - }, - }, "ErrExec": { reason: "Any errors encountered while creating the default grant should be returned", fields: fields{ @@ -434,6 +571,64 @@ func TestCreate(t *testing.T) { err: nil, }, }, + "SuccessVerifySQL": { + reason: "Create should execute a REVOKE followed by a GRANT in a transaction with correct role order", + fields: fields{ + db: &mockDB{ + MockExecTx: func(ctx context.Context, ql []xsql.Query) error { + if len(ql) != 2 { + t.Errorf("expected 2 queries in transaction, got %d", len(ql)) + return nil + } + // First query: REVOKE + if !strings.Contains(ql[0].String, "REVOKE ALL") { + t.Errorf("first query should be REVOKE, got: %s", ql[0].String) + } + if !strings.Contains(ql[0].String, `FOR ROLE "target-role"`) { + t.Errorf("REVOKE should use targetRole in FOR ROLE, got: %s", ql[0].String) + } + if !strings.Contains(ql[0].String, `FROM "grantee-role"`) { + t.Errorf("REVOKE should use role in FROM, got: %s", ql[0].String) + } + // Second query: GRANT + if !strings.Contains(ql[1].String, "GRANT SELECT,UPDATE") { + t.Errorf("second query should be GRANT with privileges, got: %s", ql[1].String) + } + if !strings.Contains(ql[1].String, `FOR ROLE "target-role"`) { + t.Errorf("GRANT should use targetRole in FOR ROLE, got: %s", ql[1].String) + } + if !strings.Contains(ql[1].String, `TO "grantee-role"`) { + t.Errorf("GRANT should use role in TO, got: %s", ql[1].String) + } + if !strings.Contains(ql[1].String, `IN SCHEMA "public"`) { + t.Errorf("GRANT should include IN SCHEMA, got: %s", ql[1].String) + } + if !strings.Contains(ql[1].String, "WITH GRANT OPTION") { + t.Errorf("GRANT should include WITH GRANT OPTION, got: %s", ql[1].String) + } + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + Database: ptr.To("testdb"), + Role: ptr.To("grantee-role"), + TargetRole: ptr.To("target-role"), + ObjectType: ptr.To("table"), + Schema: ptr.To("public"), + Privileges: v1alpha1.GrantPrivileges{"SELECT", "UPDATE"}, + WithOption: &gog, + }, + }, + }, + }, + want: want{ + err: nil, + }, + }, } for name, tc := range cases { @@ -457,7 +652,7 @@ func TestUpdate(t *testing.T) { type args struct { ctx context.Context - mg resource.Managed + mg *v1alpha1.DefaultPrivileges } type want struct { @@ -515,7 +710,7 @@ func TestDelete(t *testing.T) { type args struct { ctx context.Context - mg resource.Managed + mg *v1alpha1.DefaultPrivileges } cases := map[string]struct { @@ -524,13 +719,6 @@ func TestDelete(t *testing.T) { args args want error }{ - "ErrNotDefaultPrivileges": { - reason: "An error should be returned if the managed resource is not a *DefaultPrivileges", - args: args{ - mg: nil, - }, - want: errors.New(errNotDefaultPrivileges), - }, "ErrDropDefaultPrivileges": { reason: "Errors dropping default privileges should be returned", fields: fields{ @@ -577,12 +765,48 @@ func TestDelete(t *testing.T) { }, want: nil, }, + "SuccessVerifySQL": { + reason: "Delete should generate correct REVOKE SQL with proper role placement", + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + Database: ptr.To("testdb"), + Role: ptr.To("grantee-role"), + ObjectType: ptr.To("table"), + TargetRole: ptr.To("target-role"), + Schema: ptr.To("myschema"), + }, + }, + }, + }, + fields: fields{ + db: &mockDB{ + MockExec: func(ctx context.Context, q xsql.Query) error { + if !strings.Contains(q.String, `FOR ROLE "target-role"`) { + t.Errorf("REVOKE should use targetRole in FOR ROLE, got: %s", q.String) + } + if !strings.Contains(q.String, `FROM "grantee-role"`) { + t.Errorf("REVOKE should use role in FROM, got: %s", q.String) + } + if !strings.Contains(q.String, `IN SCHEMA "myschema"`) { + t.Errorf("REVOKE should include IN SCHEMA, got: %s", q.String) + } + if !strings.Contains(q.String, "REVOKE ALL ON tableS") { + t.Errorf("REVOKE should target correct object type, got: %s", q.String) + } + 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) + _, 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) } diff --git a/pkg/controller/cluster/postgresql/postgresql.go b/pkg/controller/cluster/postgresql/postgresql.go index ac20a550..f5759560 100644 --- a/pkg/controller/cluster/postgresql/postgresql.go +++ b/pkg/controller/cluster/postgresql/postgresql.go @@ -21,13 +21,13 @@ import ( "github.com/crossplane/crossplane-runtime/v2/pkg/controller" - "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" - "github.com/crossplane-contrib/provider-sql/pkg/controller/postgresql/schema" + "github.com/crossplane-contrib/provider-sql/pkg/controller/cluster/postgresql/config" + "github.com/crossplane-contrib/provider-sql/pkg/controller/cluster/postgresql/database" + "github.com/crossplane-contrib/provider-sql/pkg/controller/cluster/postgresql/default_privileges" + "github.com/crossplane-contrib/provider-sql/pkg/controller/cluster/postgresql/extension" + "github.com/crossplane-contrib/provider-sql/pkg/controller/cluster/postgresql/grant" + "github.com/crossplane-contrib/provider-sql/pkg/controller/cluster/postgresql/role" + "github.com/crossplane-contrib/provider-sql/pkg/controller/cluster/postgresql/schema" ) // Setup creates all PostgreSQL controllers with the supplied logger and adds diff --git a/pkg/controller/namespaced/postgresql/default_privileges/reconciler.go b/pkg/controller/namespaced/postgresql/default_privileges/reconciler.go new file mode 100644 index 00000000..163c0635 --- /dev/null +++ b/pkg/controller/namespaced/postgresql/default_privileges/reconciler.go @@ -0,0 +1,312 @@ +/* +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_privileges + +import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/lib/pq" + "github.com/pkg/errors" + 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/v2/apis/common/v1" + xpcontroller "github.com/crossplane/crossplane-runtime/v2/pkg/controller" + "github.com/crossplane/crossplane-runtime/v2/pkg/event" + "github.com/crossplane/crossplane-runtime/v2/pkg/feature" + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + + "github.com/crossplane-contrib/provider-sql/apis/namespaced/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" + "github.com/crossplane-contrib/provider-sql/pkg/controller/namespaced/postgresql/provider" +) + +const ( + errTrackPCUsage = "cannot track ProviderConfig usage" + + 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.DefaultPrivilegesGroupKind) + + t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{}) + + reconcilerOptions := []managed.ReconcilerOption{ + managed.WithTypedExternalConnector(&connector{kube: mgr.GetClient(), track: t.Track, newDB: postgresql.New}), + managed.WithLogger(o.Logger.WithValues("controller", name)), + managed.WithPollInterval(o.PollInterval), + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), + } + if o.Features.Enabled(feature.EnableBetaManagementPolicies) { + reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) + } + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1alpha1.DefaultPrivilegesGroupVersionKind), + reconcilerOptions..., + ) + return ctrl.NewControllerManagedBy(mgr). + Named(name). + For(&v1alpha1.DefaultPrivileges{}). + WithOptions(controller.Options{ + MaxConcurrentReconciles: maxConcurrency, + }). + Complete(r) +} + +type connector struct { + kube client.Client + track func(ctx context.Context, mg resource.ModernManaged) error + newDB func(creds map[string][]byte, database string, sslmode string) xsql.DB +} + +func (c *connector) Connect(ctx context.Context, mg *v1alpha1.DefaultPrivileges) (managed.TypedExternalClient[*v1alpha1.DefaultPrivileges], error) { + if err := c.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. + providerInfo, err := provider.GetProviderConfig(ctx, c.kube, mg) + if err != nil { + return nil, err + } + + // Connect to the specific database if provided, otherwise use the default. + // ALTER DEFAULT PRIVILEGES is per-database, so we must connect to the target database. + database := providerInfo.DefaultDatabase + if mg.Spec.ForProvider.Database != nil { + database = *mg.Spec.ForProvider.Database + } + + return &external{db: c.newDB(providerInfo.SecretData, database, clients.ToString(providerInfo.SSLMode))}, nil +} + +type external struct { + db xsql.DB +} + +var ( + objectTypes = map[string]string{ + "table": "r", + "sequence": "S", + "function": "f", + "type": "T", + "schema": "n", + } +) + +func selectDefaultPrivilegesQuery(gp v1alpha1.DefaultPrivilegesParameters, q *xsql.Query) { + 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, + } + +} + +func withOption(option *v1alpha1.GrantOption) string { + if option != nil { + return fmt.Sprintf("WITH %s OPTION", string(*option)) + } + return "" +} + +func inSchema(params *v1alpha1.DefaultPrivilegesParameters) string { + if params.Schema != nil { + return fmt.Sprintf("IN SCHEMA %s", pq.QuoteIdentifier(*params.Schema)) + } + return "" +} + +func createDefaultPrivilegesQuery(gp v1alpha1.DefaultPrivilegesParameters, q *xsql.Query) { + + roleName := pq.QuoteIdentifier(*gp.Role) + + targetRoleName := pq.QuoteIdentifier(*gp.TargetRole) + + query := strings.TrimSpace(fmt.Sprintf( + "ALTER DEFAULT PRIVILEGES FOR ROLE %s %s GRANT %s ON %sS TO %s %s", + targetRoleName, + inSchema(&gp), + strings.Join(gp.Privileges.ToStringSlice(), ","), + *gp.ObjectType, + roleName, + withOption(gp.WithOption), + )) + + q.String = query +} + +func deleteDefaultPrivilegesQuery(gp v1alpha1.DefaultPrivilegesParameters, q *xsql.Query) { + roleName := pq.QuoteIdentifier(*gp.Role) + targetRoleName := pq.QuoteIdentifier(*gp.TargetRole) + + query := strings.TrimSpace(fmt.Sprintf( + "ALTER DEFAULT PRIVILEGES FOR ROLE %s %s REVOKE ALL ON %sS FROM %s", + targetRoleName, + inSchema(&gp), + *gp.ObjectType, + roleName, + )) + + q.String = query + +} + +func matchingGrants(currentGrants []string, specGrants []string) bool { + if len(currentGrants) != len(specGrants) { + return false + } + + sort.Strings(currentGrants) + sort.Strings(specGrants) + + for i, g := range currentGrants { + if g != specGrants[i] { + return false + } + } + + return true +} + +func (c *external) Disconnect(ctx context.Context) error { + return nil +} + +func (c *external) Observe(ctx context.Context, mg *v1alpha1.DefaultPrivileges) (managed.ExternalObservation, error) { //nolint:gocyclo + if mg.Spec.ForProvider.Role == nil { + return managed.ExternalObservation{}, errors.New(errNoRole) + } + + if mg.Spec.ForProvider.TargetRole == nil { + return managed.ExternalObservation{}, errors.New(errNoTargetRole) + } + + if mg.Spec.ForProvider.ObjectType == nil { + return managed.ExternalObservation{}, errors.New(errNoObjectType) + } + + gp := mg.Spec.ForProvider + var query xsql.Query + selectDefaultPrivilegesQuery(gp, &query) + + 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() //nolint:errcheck + 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 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 + } + + mg.SetConditions(xpv1.Available()) + + resourceMatches := matchingGrants(defaultPrivileges, 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. + // 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: resourceMatches, + ResourceUpToDate: resourceMatches, + }, nil +} + +func (c *external) Create(ctx context.Context, mg *v1alpha1.DefaultPrivileges) (managed.ExternalCreation, error) { + + mg.SetConditions(xpv1.Creating()) + + var createQuery xsql.Query + createDefaultPrivilegesQuery(mg.Spec.ForProvider, &createQuery) + + var deleteQuery xsql.Query + deleteDefaultPrivilegesQuery(mg.Spec.ForProvider, &deleteQuery) + + err := c.db.ExecTx(ctx, []xsql.Query{ + deleteQuery, createQuery, + }) + + return managed.ExternalCreation{}, errors.Wrap(err, errCreateDefaultPrivileges) +} + +func (c *external) Update( + ctx context.Context, mg *v1alpha1.DefaultPrivileges) (managed.ExternalUpdate, error) { + // Update is a no-op, as permissions are fully revoked and then granted in the Create function, + // inside a transaction. Same approach as the grant resource. + return managed.ExternalUpdate{}, nil +} + +func (c *external) Delete(ctx context.Context, mg *v1alpha1.DefaultPrivileges) (managed.ExternalDelete, error) { + var query xsql.Query + + mg.SetConditions(xpv1.Deleting()) + + deleteDefaultPrivilegesQuery(mg.Spec.ForProvider, &query) + + return managed.ExternalDelete{}, errors.Wrap(c.db.Exec(ctx, query), errRevokeDefaultPrivileges) +} diff --git a/pkg/controller/namespaced/postgresql/default_privileges/reconciler_test.go b/pkg/controller/namespaced/postgresql/default_privileges/reconciler_test.go new file mode 100644 index 00000000..33614cb5 --- /dev/null +++ b/pkg/controller/namespaced/postgresql/default_privileges/reconciler_test.go @@ -0,0 +1,865 @@ +/* +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_privileges + +import ( + "context" + "database/sql" + "strings" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/crossplane-contrib/provider-sql/apis/namespaced/postgresql/v1alpha1" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/crossplane/crossplane-runtime/v2/apis/common" + xpv2 "github.com/crossplane/crossplane-runtime/v2/apis/common/v2" + + "github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/v2/pkg/resource" + "github.com/crossplane/crossplane-runtime/v2/pkg/test" + + "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" + provErrors "github.com/crossplane-contrib/provider-sql/pkg/controller/namespaced/errors" +) + +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 + track func(context.Context, resource.ModernManaged) error + newDB func(creds map[string][]byte, database string, sslmode string) xsql.DB + } + + type args struct { + ctx context.Context + mg *v1alpha1.DefaultPrivileges + } + + cases := map[string]struct { + reason string + fields fields + args args + want error + }{ + "ErrTrackProviderConfigUsage": { + reason: "An error should be returned if we can't track our ProviderConfig usage", + fields: fields{ + track: func(ctx context.Context, mg resource.ModernManaged) error { return errBoom }, + }, + args: args{ + mg: &v1alpha1.DefaultPrivileges{}, + }, + want: errors.Wrap(errBoom, errTrackPCUsage), + }, + "InvalideProviderConfigKind": { + reason: "An error should be returned if our ProviderConfig kind is invalid", + fields: fields{ + track: func(ctx context.Context, mg resource.ModernManaged) error { return nil }, + }, + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ManagedResourceSpec: xpv2.ManagedResourceSpec{ + ProviderConfigReference: &common.ProviderConfigReference{Kind: "Invalid"}, + }, + }, + }, + }, + want: provErrors.InvalidProviderConfigKindError("Invalid"), + }, + "ErrGetProviderConfig": { + reason: "An error should be returned if we can't get our ProviderConfig", + fields: fields{ + kube: &test.MockClient{ + MockGet: test.NewMockGetFn(errBoom), + }, + track: func(ctx context.Context, mg resource.ModernManaged) error { return nil }, + }, + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: v1alpha1.DefaultPrivilegesSpec{ + ManagedResourceSpec: xpv2.ManagedResourceSpec{ + ProviderConfigReference: &common.ProviderConfigReference{ + Kind: v1alpha1.ProviderConfigKind, + Name: "example", + }, + }, + }, + }, + }, + want: provErrors.GetProviderConfigError(errBoom), + }, + "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), + }, + track: func(ctx context.Context, mg resource.ModernManaged) error { return nil }, + }, + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: v1alpha1.DefaultPrivilegesSpec{ + ManagedResourceSpec: xpv2.ManagedResourceSpec{ + ProviderConfigReference: &common.ProviderConfigReference{ + Kind: v1alpha1.ProviderConfigKind, + Name: "example", + }, + }, + }, + }, + }, + want: provErrors.MissingSecretRefError(), + }, + "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 = common.LocalSecretReference{Name: "example"} + case *corev1.Secret: + return errBoom + } + return nil + }), + }, + track: func(ctx context.Context, mg resource.ModernManaged) error { return nil }, + }, + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: v1alpha1.DefaultPrivilegesSpec{ + ManagedResourceSpec: xpv2.ManagedResourceSpec{ + ProviderConfigReference: &common.ProviderConfigReference{ + Kind: v1alpha1.ProviderConfigKind, + Name: "example", + }, + }, + }, + }, + }, + want: provErrors.GetSecretError(errBoom), + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + e := &connector{kube: tc.fields.kube, track: tc.fields.track, 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 TestConnectDatabaseSelection(t *testing.T) { + type args struct { + mg *v1alpha1.DefaultPrivileges + } + + type want struct { + database string + } + + cases := map[string]struct { + reason string + args args + want want + }{ + "UsesForProviderDatabase": { + reason: "Connect should use forProvider.database when specified, not the ProviderConfig default", + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: v1alpha1.DefaultPrivilegesSpec{ + ManagedResourceSpec: xpv2.ManagedResourceSpec{ + ProviderConfigReference: &common.ProviderConfigReference{ + Kind: v1alpha1.ProviderConfigKind, + Name: "example", + }, + }, + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + Database: ptr.To("mydb"), + }, + }, + }, + }, + want: want{database: "mydb"}, + }, + "FallsBackToProviderConfigDefault": { + reason: "Connect should use the ProviderConfig's DefaultDatabase when forProvider.database is nil", + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: v1alpha1.DefaultPrivilegesSpec{ + ManagedResourceSpec: xpv2.ManagedResourceSpec{ + ProviderConfigReference: &common.ProviderConfigReference{ + Kind: v1alpha1.ProviderConfigKind, + Name: "example", + }, + }, + ForProvider: v1alpha1.DefaultPrivilegesParameters{}, + }, + }, + }, + want: want{database: "default-db"}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + var gotDatabase string + e := &connector{ + kube: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + switch o := obj.(type) { + case *v1alpha1.ProviderConfig: + o.Spec.DefaultDatabase = "default-db" + o.Spec.Credentials.ConnectionSecretRef = common.LocalSecretReference{Name: "secret"} + case *corev1.Secret: + // Return empty secret data + } + return nil + }), + }, + track: func(ctx context.Context, mg resource.ModernManaged) error { return nil }, + newDB: func(creds map[string][]byte, database string, sslmode string) xsql.DB { + gotDatabase = database + return mockDB{} + }, + } + _, err := e.Connect(context.Background(), tc.args.mg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if diff := cmp.Diff(tc.want.database, gotDatabase); diff != "" { + t.Errorf("\n%s\ne.Connect(...) database: -want, +got:\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 *v1alpha1.DefaultPrivileges + } + + type want struct { + o managed.ExternalObservation + err error + } + + cases := map[string]struct { + reason string + fields fields + args args + want want + }{ + "SuccessNoGrant": { + 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 + }, + }, + }, + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + Database: ptr.To("test-example"), + Role: ptr.To("test-example"), + TargetRole: ptr.To("target-role"), + ObjectType: ptr.To("TABLE"), + Privileges: v1alpha1.GrantPrivileges{"ALL"}, + }, + }, + }, + }, + want: want{ + o: managed.ExternalObservation{ResourceExists: false}, + }, + }, + "ErrSelectGrant": { + 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 + }, + }, + }, + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + 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, + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errBoom, errSelectDefaultPrivileges), + }, + }, + "DefaultPrivilegesFound": { + reason: "We should return no error if we can find the right permissions in 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), nil + }, + }, + }, + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + Database: ptr.To("testdb"), + Role: ptr.To("testrole"), + TargetRole: ptr.To("target-role"), + ObjectType: ptr.To("TABLE"), + Privileges: v1alpha1.GrantPrivileges{"SELECT", "UPDATE"}, + WithOption: &gog, + }, + }, + }, + }, + want: want{ + o: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + }, + err: nil, + }, + }, + "ErrNoRole": { + reason: "An error should be returned when role is not set", + fields: fields{ + db: mockDB{}, + }, + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + TargetRole: ptr.To("target-role"), + ObjectType: ptr.To("table"), + Privileges: v1alpha1.GrantPrivileges{"SELECT"}, + }, + }, + }, + }, + want: want{ + err: errors.New(errNoRole), + }, + }, + "ErrNoTargetRole": { + reason: "An error should be returned when targetRole is not set", + fields: fields{ + db: mockDB{}, + }, + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + Role: ptr.To("testrole"), + ObjectType: ptr.To("table"), + Privileges: v1alpha1.GrantPrivileges{"SELECT"}, + }, + }, + }, + }, + want: want{ + err: errors.New(errNoTargetRole), + }, + }, + "ErrNoObjectType": { + reason: "An error should be returned when objectType is nil", + fields: fields{ + db: mockDB{}, + }, + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + Role: ptr.To("testrole"), + TargetRole: ptr.To("target-role"), + Privileges: v1alpha1.GrantPrivileges{"SELECT"}, + }, + }, + }, + }, + want: want{ + err: errors.New(errNoObjectType), + }, + }, + "PrivilegesMismatchTriggersRecreate": { + reason: "When DB has different privileges than spec, ResourceExists should be false to trigger re-create", + fields: fields{ + db: mockDB{ + MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) { + r := sqlmock.NewRows([]string{"PRIVILEGE"}). + AddRow("SELECT") + return mockRowsToSQLRows(r), nil + }, + }, + }, + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + Database: ptr.To("testdb"), + Role: ptr.To("testrole"), + TargetRole: ptr.To("target-role"), + ObjectType: ptr.To("table"), + Privileges: v1alpha1.GrantPrivileges{"SELECT", "UPDATE"}, + }, + }, + }, + }, + want: want{ + o: managed.ExternalObservation{ + ResourceExists: false, + ResourceUpToDate: false, + }, + }, + }, + } + + 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 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") + gog := v1alpha1.GrantOptionGrant + + type fields struct { + db xsql.DB + } + + type args struct { + ctx context.Context + mg *v1alpha1.DefaultPrivileges + } + + type want struct { + c managed.ExternalCreation + err error + } + + cases := map[string]struct { + reason string + fields fields + args args + want want + }{ + "ErrExec": { + 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.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + Database: ptr.To("test-example"), + Role: ptr.To("test-example"), + TargetRole: ptr.To("target-role"), + ObjectType: ptr.To("TABLE"), + Privileges: v1alpha1.GrantPrivileges{"ALL"}, + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errBoom, errCreateDefaultPrivileges), + }, + }, + "Success": { + 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.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + Database: ptr.To("test-example"), + Role: ptr.To("test-example"), + TargetRole: ptr.To("target-role"), + Privileges: v1alpha1.GrantPrivileges{"SELECT", "UPDATE"}, + ObjectType: ptr.To("TABLE"), + }, + }, + }, + }, + want: want{ + err: nil, + }, + }, + "SuccessVerifySQL": { + reason: "Create should execute a REVOKE followed by a GRANT in a transaction with correct role order", + fields: fields{ + db: &mockDB{ + MockExecTx: func(ctx context.Context, ql []xsql.Query) error { + if len(ql) != 2 { + t.Errorf("expected 2 queries in transaction, got %d", len(ql)) + return nil + } + // First query: REVOKE + if !strings.Contains(ql[0].String, "REVOKE ALL") { + t.Errorf("first query should be REVOKE, got: %s", ql[0].String) + } + if !strings.Contains(ql[0].String, `FOR ROLE "target-role"`) { + t.Errorf("REVOKE should use targetRole in FOR ROLE, got: %s", ql[0].String) + } + if !strings.Contains(ql[0].String, `FROM "grantee-role"`) { + t.Errorf("REVOKE should use role in FROM, got: %s", ql[0].String) + } + // Second query: GRANT + if !strings.Contains(ql[1].String, "GRANT SELECT,UPDATE") { + t.Errorf("second query should be GRANT with privileges, got: %s", ql[1].String) + } + if !strings.Contains(ql[1].String, `FOR ROLE "target-role"`) { + t.Errorf("GRANT should use targetRole in FOR ROLE, got: %s", ql[1].String) + } + if !strings.Contains(ql[1].String, `TO "grantee-role"`) { + t.Errorf("GRANT should use role in TO, got: %s", ql[1].String) + } + if !strings.Contains(ql[1].String, `IN SCHEMA "public"`) { + t.Errorf("GRANT should include IN SCHEMA, got: %s", ql[1].String) + } + if !strings.Contains(ql[1].String, "WITH GRANT OPTION") { + t.Errorf("GRANT should include WITH GRANT OPTION, got: %s", ql[1].String) + } + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + Database: ptr.To("testdb"), + Role: ptr.To("grantee-role"), + TargetRole: ptr.To("target-role"), + ObjectType: ptr.To("table"), + Schema: ptr.To("public"), + Privileges: v1alpha1.GrantPrivileges{"SELECT", "UPDATE"}, + WithOption: &gog, + }, + }, + }, + }, + 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 *v1alpha1.DefaultPrivileges + } + + 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 *DefaultPrivileges", + 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{ + 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 *v1alpha1.DefaultPrivileges + } + + cases := map[string]struct { + reason string + fields fields + args args + want error + }{ + "ErrDropDefaultPrivileges": { + reason: "Errors dropping default privileges should be returned", + fields: fields{ + db: &mockDB{ + MockExec: func(ctx context.Context, q xsql.Query) error { + return errBoom + }, + }, + }, + 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"}, + ObjectType: ptr.To("SEQUENCE"), + TargetRole: ptr.To("target-role"), + }, + }, + }, + }, + want: errors.Wrap(errBoom, errRevokeDefaultPrivileges), + }, + "Success": { + reason: "No error should be returned if the default grant was revoked", + 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"}, + ObjectType: ptr.To("SEQUENCE"), + TargetRole: ptr.To("target-role"), + }, + }, + }, + }, + fields: fields{ + db: &mockDB{ + MockExec: func(ctx context.Context, q xsql.Query) error { return nil }, + }, + }, + want: nil, + }, + "SuccessVerifySQL": { + reason: "Delete should generate correct REVOKE SQL with proper role placement", + args: args{ + mg: &v1alpha1.DefaultPrivileges{ + Spec: v1alpha1.DefaultPrivilegesSpec{ + ForProvider: v1alpha1.DefaultPrivilegesParameters{ + Database: ptr.To("testdb"), + Role: ptr.To("grantee-role"), + ObjectType: ptr.To("table"), + TargetRole: ptr.To("target-role"), + Schema: ptr.To("myschema"), + }, + }, + }, + }, + fields: fields{ + db: &mockDB{ + MockExec: func(ctx context.Context, q xsql.Query) error { + if !strings.Contains(q.String, `FOR ROLE "target-role"`) { + t.Errorf("REVOKE should use targetRole in FOR ROLE, got: %s", q.String) + } + if !strings.Contains(q.String, `FROM "grantee-role"`) { + t.Errorf("REVOKE should use role in FROM, got: %s", q.String) + } + if !strings.Contains(q.String, `IN SCHEMA "myschema"`) { + t.Errorf("REVOKE should include IN SCHEMA, got: %s", q.String) + } + if !strings.Contains(q.String, "REVOKE ALL ON tableS") { + t.Errorf("REVOKE should target correct object type, got: %s", q.String) + } + 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) + } + }) + } +} diff --git a/pkg/controller/namespaced/postgresql/postgresql.go b/pkg/controller/namespaced/postgresql/postgresql.go index ac511a9f..beb952da 100644 --- a/pkg/controller/namespaced/postgresql/postgresql.go +++ b/pkg/controller/namespaced/postgresql/postgresql.go @@ -23,6 +23,7 @@ import ( "github.com/crossplane-contrib/provider-sql/pkg/controller/namespaced/postgresql/config" "github.com/crossplane-contrib/provider-sql/pkg/controller/namespaced/postgresql/database" + "github.com/crossplane-contrib/provider-sql/pkg/controller/namespaced/postgresql/default_privileges" "github.com/crossplane-contrib/provider-sql/pkg/controller/namespaced/postgresql/extension" "github.com/crossplane-contrib/provider-sql/pkg/controller/namespaced/postgresql/grant" "github.com/crossplane-contrib/provider-sql/pkg/controller/namespaced/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