diff --git a/CHANGELOG.md b/CHANGELOG.md index cea6abce..2cca0645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [MAJOR.MINOR.PATCH] - YYYY-MM-DD +- Add kind: `ClickhouseGrant` + ## v0.20.0 - 2024-06-05 - Add kind: `ServiceIntegrationEndpoint` diff --git a/PROJECT b/PROJECT index 80daca7e..8212945a 100644 --- a/PROJECT +++ b/PROJECT @@ -271,6 +271,14 @@ resources: kind: ClickhouseRole path: github.com/aiven/aiven-operator/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: aiven.io + kind: ClickhouseGrant + path: github.com/aiven/aiven-operator/api/v1alpha1 + version: v1alpha1 - api: crdVersion: v1 namespaced: true diff --git a/api/v1alpha1/clickhousegrant_types.go b/api/v1alpha1/clickhousegrant_types.go new file mode 100644 index 00000000..18ad1490 --- /dev/null +++ b/api/v1alpha1/clickhousegrant_types.go @@ -0,0 +1,316 @@ +// Copyright (c) 2024 Aiven, Helsinki, Finland. https://aiven.io/ + +package v1alpha1 + +import ( + "context" + "fmt" + "strings" + + avngen "github.com/aiven/go-client-codegen" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/aiven/aiven-operator/utils" + chUtils "github.com/aiven/aiven-operator/utils/clickhouse" +) + +// TODO: use oneOf in Grantee if https://github.com/kubernetes-sigs/controller-tools/issues/461 is resolved + +// Grantee represents a user or a role to which privileges or roles are granted. +type Grantee struct { + User string `json:"user,omitempty"` + Role string `json:"role,omitempty"` +} + +// PrivilegeGrant represents the privileges to be granted to users or roles. +// See https://clickhouse.com/docs/en/sql-reference/statements/grant#granting-privilege-syntax. +// +kubebuilder:validation:XValidation:rule="!has(self.columns) || (has(self.columns) && has(self.table))",message="`table` must be set if `columns` are set" +type PrivilegeGrant struct { + // List of grantees (users or roles) to grant the privilege to. + // +kubebuilder:validation:MinItems=1 + Grantees []Grantee `json:"grantees"` + // The privileges to grant, i.e. `INSERT`, `SELECT`. + // See https://clickhouse.com/docs/en/sql-reference/statements/grant#assigning-role-syntax. + Privileges []string `json:"privileges"` + // The database that the grant refers to. + Database string `json:"database"` + // The tables that the grant refers to. + Table string `json:"table,omitempty"` + // The column that the grant refers to. + Columns []string `json:"columns,omitempty"` + // If true, then the grantee (user or role) get the permission to execute the `GRANT`` query. + // Users can grant privileges of the same scope they have and less. + // See https://clickhouse.com/docs/en/sql-reference/statements/grant#granting-privilege-syntax + WithGrantOption bool `json:"withGrantOption,omitempty"` +} + +// RoleGrant represents the roles to be assigned to users or roles. +// See https://clickhouse.com/docs/en/sql-reference/statements/grant#assigning-role-syntax. +type RoleGrant struct { + // List of grantees (users or roles) to grant the privilege to. + // +kubebuilder:validation:MinItems=1 + Grantees []Grantee `json:"grantees"` + // List of roles to grant to the grantees. + // +kubebuilder:validation:MinItems=1 + Roles []string `json:"roles"` + // If true, the grant is executed with `ADMIN OPTION` privilege. + // See https://clickhouse.com/docs/en/sql-reference/statements/grant#admin-option. + WithAdminOption bool `json:"withAdminOption,omitempty"` +} + +// ClickhouseGrantSpec defines the desired state of ClickhouseGrant +type ClickhouseGrantSpec struct { + ServiceDependant `json:",inline,omitempty"` + + // Configuration to grant a privilege. + PrivilegeGrants []PrivilegeGrant `json:"privilegeGrants,omitempty"` + // Configuration to grant a role. + RoleGrants []RoleGrant `json:"roleGrants,omitempty"` +} + +// ClickhouseGrantStatus defines the observed state of ClickhouseGrant +type ClickhouseGrantStatus struct { + Conditions []metav1.Condition `json:"conditions"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// ClickhouseGrant is the Schema for the ClickhouseGrants API +// +kubebuilder:printcolumn:name="Project",type="string",JSONPath=".spec.project" +// +kubebuilder:printcolumn:name="Service Name",type="string",JSONPath=".spec.serviceName" +type ClickhouseGrant struct { + metav1.TypeMeta `json:",inline,omitempty"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ClickhouseGrantSpec `json:"spec,omitempty"` + Status ClickhouseGrantStatus `json:"status,omitempty"` +} + +func (in ClickhouseGrantSpec) buildStatements(statementType chUtils.StatementType) []string { + stmts := make([]string, 0, len(in.PrivilegeGrants)+len(in.RoleGrants)) + for _, g := range in.PrivilegeGrants { + stmts = append(stmts, buildStatement(statementType, g)) + } + for _, g := range in.RoleGrants { + stmts = append(stmts, buildStatement(statementType, g)) + } + return stmts +} + +func (in ClickhouseGrantSpec) ExecuteStatements(ctx context.Context, avnGen avngen.Client, statementType chUtils.StatementType) (bool, error) { + statements := in.buildStatements(statementType) + for _, stmt := range statements { + _, err := chUtils.ExecuteClickHouseQuery(ctx, avnGen, in.Project, in.ServiceName, stmt) + if err != nil { + return false, err + } + } + return true, nil +} + +func (in ClickhouseGrantSpec) CollectGrantees() []string { + allGrantees := []string{} + processGrantee := func(grantees []Grantee) { + for _, grantee := range grantees { + allGrantees = append(allGrantees, userOrRole(grantee)) + } + } + for _, grant := range in.PrivilegeGrants { + processGrantee(grant.Grantees) + } + for _, grant := range in.RoleGrants { + processGrantee(grant.Grantees) + } + + return utils.UniqueSliceElements(allGrantees) +} + +func (in ClickhouseGrantSpec) CollectDatabases() []string { + allDatabases := []string{} + for _, grant := range in.PrivilegeGrants { + if grant.Database != "" { + allDatabases = append(allDatabases, grant.Database) + } + } + return utils.UniqueSliceElements(allDatabases) +} + +func (in ClickhouseGrantSpec) CollectTables() []chUtils.DatabaseAndTable { + allTables := []chUtils.DatabaseAndTable{} + for _, grant := range in.PrivilegeGrants { + if grant.Table != "" { + allTables = append(allTables, chUtils.DatabaseAndTable{Database: grant.Database, Table: grant.Table}) + } + } + return utils.UniqueSliceElements(allTables) +} + +func (in *ClickhouseGrant) AuthSecretRef() *AuthSecretReference { + return in.Spec.AuthSecretRef +} + +func (in *ClickhouseGrant) Conditions() *[]metav1.Condition { + return &in.Status.Conditions +} + +func (in *ClickhouseGrant) NoSecret() bool { + return true +} + +var _ AivenManagedObject = &ClickhouseGrant{} + +//+kubebuilder:object:root=true + +// ClickhouseGrantList contains a list of ClickhouseGrant +type ClickhouseGrantList struct { + metav1.TypeMeta `json:",inline,omitempty"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ClickhouseGrant `json:"items,omitempty"` +} + +func init() { + SchemeBuilder.Register(&ClickhouseGrant{}, &ClickhouseGrantList{}) +} + +// Takes a slice of PrivilegeGrant and returns a new slice +// where each grantee has its own PrivilegeGrant entry. +func FlattenPrivilegeGrants(grants []PrivilegeGrant) []PrivilegeGrant { + var flattened []PrivilegeGrant + for _, grant := range grants { + for _, grantee := range grant.Grantees { + newGrant := PrivilegeGrant{ + Grantees: []Grantee{grantee}, + Privileges: grant.Privileges, + Database: grant.Database, + Table: grant.Table, + Columns: grant.Columns, + WithGrantOption: grant.WithGrantOption, + } + flattened = append(flattened, newGrant) + } + } + return flattened +} + +// Takes a slice of RoleGrant and returns a new slice +// where each grantee has its own RoleGrant entry. +func FlattenRoleGrants(grants []RoleGrant) []RoleGrant { + var flattened []RoleGrant + for _, grant := range grants { + for _, grantee := range grant.Grantees { + newGrant := RoleGrant{ + Grantees: []Grantee{grantee}, + Roles: grant.Roles, + WithAdminOption: grant.WithAdminOption, + } + flattened = append(flattened, newGrant) + } + } + return flattened +} + +func (g PrivilegeGrant) ConstructParts(t chUtils.StatementType) (string, string, string) { + privilegesPart := constructPrivilegesPart(g) + granteesPart := constructGranteePart(g.Grantees) + options := "" + if g.WithGrantOption && t == chUtils.GRANT { + options = "WITH GRANT OPTION" + } + return privilegesPart, granteesPart, options +} + +// Helper function to construct the privileges part of the statement +func constructPrivilegesPart(g PrivilegeGrant) string { + privileges := make([]string, 0, len(g.Privileges)) + for _, privilege := range g.Privileges { + if (privilege == "SELECT" || privilege == "INSERT") && len(g.Columns) > 0 { + columnList := strings.Join(utils.MapSlice(g.Columns, escape), ", ") + privileges = append(privileges, fmt.Sprintf("%s(%s)", privilege, columnList)) + } else { + privileges = append(privileges, privilege) + } + } + return strings.Join(privileges, ", ") +} + +func (g RoleGrant) ConstructParts(t chUtils.StatementType) (string, string, string) { + rolesPart := strings.Join(g.Roles, ", ") + granteesPart := constructGranteePart(g.Grantees) + options := "" + if g.WithAdminOption && t == chUtils.GRANT { + options = "WITH ADMIN OPTION" + } + return rolesPart, granteesPart, options +} + +func ExecuteGrant[T chUtils.Grant](ctx context.Context, avnGen avngen.Client, t chUtils.StatementType, grant T, projectName string, serviceName string) error { + stmt := buildStatement(t, grant) + _, err := chUtils.ExecuteClickHouseQuery(ctx, avnGen, projectName, serviceName, stmt) + if err != nil { + return err + } + return nil +} + +// Generates a ClickHouse GRANT or REVOKE statement for privilege or role grants. See https://clickhouse.com/docs/en/sql-reference/statements/grant. +func buildStatement[T chUtils.Grant](t chUtils.StatementType, grant T) string { + mainPart, granteesPart, options := grant.ConstructParts(t) + + fmtString := "" + switch t { + case chUtils.GRANT: + fmtString = "GRANT %s TO %s %s" + case chUtils.REVOKE: + fmtString = "REVOKE %s FROM %s %s" + } + + // Adjust the format string based on the type of grant (PrivilegeGrant needs "ON %s" part) + if p, ok := any(grant).(PrivilegeGrant); ok { + // ON part is constructed only for PrivilegeGrant + onPart := constructOnPart(p) + if t == chUtils.GRANT { + fmtString = strings.Replace(fmtString, "TO", "ON %s TO", 1) + } else { + fmtString = strings.Replace(fmtString, "FROM", "ON %s FROM", 1) + } + return fmt.Sprintf(fmtString, mainPart, onPart, granteesPart, options) + } + + return fmt.Sprintf(fmtString, mainPart, granteesPart, options) +} + +// Helper function to construct the ON part of the statement +func constructOnPart(grant PrivilegeGrant) string { + switch { + case grant.Table != "": + return escape(grant.Database) + "." + escape(grant.Table) + default: + return escape(grant.Database) + ".*" + } +} + +// Helper function to construct the TO part of the statement +func constructGranteePart(grantees []Grantee) string { + return strings.Join(utils.MapSlice(grantees, granteeToString), ", ") +} + +// Converts a grantee to an escaped string +func granteeToString(grantee Grantee) string { + return escape(userOrRole(grantee)) +} + +func userOrRole(grantee Grantee) string { + if grantee.User != "" { + return grantee.User + } + return grantee.Role +} + +// Escapes database identifiers like table or column names +func escape(identifier string) string { + // See https://github.com/ClickHouse/clickhouse-go/blob/8ad6ec6b95d8b0c96d00115bc2d69ff13083f94b/lib/column/column.go#L32 + replacer := strings.NewReplacer("`", "\\`", "\\", "\\\\") + return "`" + replacer.Replace(identifier) + "`" +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 16deef36..83500160 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -331,6 +331,117 @@ func (in *ClickhouseDatabaseStatus) DeepCopy() *ClickhouseDatabaseStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickhouseGrant) DeepCopyInto(out *ClickhouseGrant) { + *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 ClickhouseGrant. +func (in *ClickhouseGrant) DeepCopy() *ClickhouseGrant { + if in == nil { + return nil + } + out := new(ClickhouseGrant) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClickhouseGrant) 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 *ClickhouseGrantList) DeepCopyInto(out *ClickhouseGrantList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClickhouseGrant, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickhouseGrantList. +func (in *ClickhouseGrantList) DeepCopy() *ClickhouseGrantList { + if in == nil { + return nil + } + out := new(ClickhouseGrantList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClickhouseGrantList) 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 *ClickhouseGrantSpec) DeepCopyInto(out *ClickhouseGrantSpec) { + *out = *in + in.ServiceDependant.DeepCopyInto(&out.ServiceDependant) + if in.PrivilegeGrants != nil { + in, out := &in.PrivilegeGrants, &out.PrivilegeGrants + *out = make([]PrivilegeGrant, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.RoleGrants != nil { + in, out := &in.RoleGrants, &out.RoleGrants + *out = make([]RoleGrant, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickhouseGrantSpec. +func (in *ClickhouseGrantSpec) DeepCopy() *ClickhouseGrantSpec { + if in == nil { + return nil + } + out := new(ClickhouseGrantSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickhouseGrantStatus) DeepCopyInto(out *ClickhouseGrantStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickhouseGrantStatus. +func (in *ClickhouseGrantStatus) DeepCopy() *ClickhouseGrantStatus { + if in == nil { + return nil + } + out := new(ClickhouseGrantStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClickhouseList) DeepCopyInto(out *ClickhouseList) { *out = *in @@ -888,6 +999,21 @@ func (in *GrafanaSpec) DeepCopy() *GrafanaSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Grantee) DeepCopyInto(out *Grantee) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Grantee. +func (in *Grantee) DeepCopy() *Grantee { + if in == nil { + return nil + } + out := new(Grantee) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Kafka) DeepCopyInto(out *Kafka) { *out = *in @@ -1968,6 +2094,36 @@ func (in *PostgreSQLSpec) DeepCopy() *PostgreSQLSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PrivilegeGrant) DeepCopyInto(out *PrivilegeGrant) { + *out = *in + if in.Grantees != nil { + in, out := &in.Grantees, &out.Grantees + *out = make([]Grantee, len(*in)) + copy(*out, *in) + } + if in.Privileges != nil { + in, out := &in.Privileges, &out.Privileges + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Columns != nil { + in, out := &in.Columns, &out.Columns + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrivilegeGrant. +func (in *PrivilegeGrant) DeepCopy() *PrivilegeGrant { + if in == nil { + return nil + } + out := new(PrivilegeGrant) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Project) DeepCopyInto(out *Project) { *out = *in @@ -2324,6 +2480,31 @@ func (in *ResourceReferenceObject) DeepCopy() *ResourceReferenceObject { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RoleGrant) DeepCopyInto(out *RoleGrant) { + *out = *in + if in.Grantees != nil { + in, out := &in.Grantees, &out.Grantees + *out = make([]Grantee, len(*in)) + copy(*out, *in) + } + if in.Roles != nil { + in, out := &in.Roles, &out.Roles + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleGrant. +func (in *RoleGrant) DeepCopy() *RoleGrant { + if in == nil { + return nil + } + out := new(RoleGrant) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretFields) DeepCopyInto(out *SecretFields) { *out = *in diff --git a/charts/aiven-operator-crds/templates/aiven.io_clickhousegrants.yaml b/charts/aiven-operator-crds/templates/aiven.io_clickhousegrants.yaml new file mode 100644 index 00000000..92c23a9d --- /dev/null +++ b/charts/aiven-operator-crds/templates/aiven.io_clickhousegrants.yaml @@ -0,0 +1,259 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: clickhousegrants.aiven.io +spec: + group: aiven.io + names: + kind: ClickhouseGrant + listKind: ClickhouseGrantList + plural: clickhousegrants + singular: clickhousegrant + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.project + name: Project + type: string + - jsonPath: .spec.serviceName + name: Service Name + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: ClickhouseGrant is the Schema for the ClickhouseGrants API + 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: ClickhouseGrantSpec defines the desired state of ClickhouseGrant + properties: + authSecretRef: + description: Authentication reference to Aiven token in a secret + properties: + key: + minLength: 1 + type: string + name: + minLength: 1 + type: string + required: + - key + - name + type: object + privilegeGrants: + description: Configuration to grant a privilege. + items: + description: |- + PrivilegeGrant represents the privileges to be granted to users or roles. + See https://clickhouse.com/docs/en/sql-reference/statements/grant#granting-privilege-syntax. + properties: + columns: + description: The column that the grant refers to. + items: + type: string + type: array + database: + description: The database that the grant refers to. + type: string + grantees: + description: + List of grantees (users or roles) to grant the + privilege to. + items: + description: + Grantee represents a user or a role to which + privileges or roles are granted. + properties: + role: + type: string + user: + type: string + type: object + minItems: 1 + type: array + privileges: + description: |- + The privileges to grant, i.e. `INSERT`, `SELECT`. + See https://clickhouse.com/docs/en/sql-reference/statements/grant#assigning-role-syntax. + items: + type: string + type: array + table: + description: The tables that the grant refers to. + type: string + withGrantOption: + description: |- + If true, then the grantee (user or role) get the permission to execute the `GRANT`` query. + Users can grant privileges of the same scope they have and less. + See https://clickhouse.com/docs/en/sql-reference/statements/grant#granting-privilege-syntax + type: boolean + required: + - database + - grantees + - privileges + type: object + x-kubernetes-validations: + - message: "`table` must be set if `columns` are set" + rule: "!has(self.columns) || (has(self.columns) && has(self.table))" + type: array + project: + description: Identifies the project this resource belongs to + maxLength: 63 + pattern: ^[a-zA-Z0-9_-]+$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + roleGrants: + description: Configuration to grant a role. + items: + description: |- + RoleGrant represents the roles to be assigned to users or roles. + See https://clickhouse.com/docs/en/sql-reference/statements/grant#assigning-role-syntax. + properties: + grantees: + description: + List of grantees (users or roles) to grant the + privilege to. + items: + description: + Grantee represents a user or a role to which + privileges or roles are granted. + properties: + role: + type: string + user: + type: string + type: object + minItems: 1 + type: array + roles: + description: List of roles to grant to the grantees. + items: + type: string + minItems: 1 + type: array + withAdminOption: + description: |- + If true, the grant is executed with `ADMIN OPTION` privilege. + See https://clickhouse.com/docs/en/sql-reference/statements/grant#admin-option. + type: boolean + required: + - grantees + - roles + type: object + type: array + serviceName: + description: + Specifies the name of the service that this resource + belongs to + maxLength: 63 + pattern: ^[a-z][-a-z0-9]+$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + required: + - project + - serviceName + type: object + status: + description: ClickhouseGrantStatus defines the observed state of ClickhouseGrant + properties: + conditions: + items: + description: + "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + 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 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/aiven-operator/templates/cluster_role.yaml b/charts/aiven-operator/templates/cluster_role.yaml index 78a8559d..b8947b9f 100644 --- a/charts/aiven-operator/templates/cluster_role.yaml +++ b/charts/aiven-operator/templates/cluster_role.yaml @@ -82,6 +82,34 @@ rules: - get - patch - update + - apiGroups: + - aiven.io + resources: + - clickhousegrants + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - aiven.io + resources: + - clickhousegrants/finalizers + verbs: + - create + - get + - update + - apiGroups: + - aiven.io + resources: + - clickhousegrants/status + verbs: + - get + - patch + - update - apiGroups: - aiven.io resources: diff --git a/config/crd/bases/aiven.io_clickhousegrants.yaml b/config/crd/bases/aiven.io_clickhousegrants.yaml new file mode 100644 index 00000000..92c23a9d --- /dev/null +++ b/config/crd/bases/aiven.io_clickhousegrants.yaml @@ -0,0 +1,259 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: clickhousegrants.aiven.io +spec: + group: aiven.io + names: + kind: ClickhouseGrant + listKind: ClickhouseGrantList + plural: clickhousegrants + singular: clickhousegrant + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.project + name: Project + type: string + - jsonPath: .spec.serviceName + name: Service Name + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: ClickhouseGrant is the Schema for the ClickhouseGrants API + 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: ClickhouseGrantSpec defines the desired state of ClickhouseGrant + properties: + authSecretRef: + description: Authentication reference to Aiven token in a secret + properties: + key: + minLength: 1 + type: string + name: + minLength: 1 + type: string + required: + - key + - name + type: object + privilegeGrants: + description: Configuration to grant a privilege. + items: + description: |- + PrivilegeGrant represents the privileges to be granted to users or roles. + See https://clickhouse.com/docs/en/sql-reference/statements/grant#granting-privilege-syntax. + properties: + columns: + description: The column that the grant refers to. + items: + type: string + type: array + database: + description: The database that the grant refers to. + type: string + grantees: + description: + List of grantees (users or roles) to grant the + privilege to. + items: + description: + Grantee represents a user or a role to which + privileges or roles are granted. + properties: + role: + type: string + user: + type: string + type: object + minItems: 1 + type: array + privileges: + description: |- + The privileges to grant, i.e. `INSERT`, `SELECT`. + See https://clickhouse.com/docs/en/sql-reference/statements/grant#assigning-role-syntax. + items: + type: string + type: array + table: + description: The tables that the grant refers to. + type: string + withGrantOption: + description: |- + If true, then the grantee (user or role) get the permission to execute the `GRANT`` query. + Users can grant privileges of the same scope they have and less. + See https://clickhouse.com/docs/en/sql-reference/statements/grant#granting-privilege-syntax + type: boolean + required: + - database + - grantees + - privileges + type: object + x-kubernetes-validations: + - message: "`table` must be set if `columns` are set" + rule: "!has(self.columns) || (has(self.columns) && has(self.table))" + type: array + project: + description: Identifies the project this resource belongs to + maxLength: 63 + pattern: ^[a-zA-Z0-9_-]+$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + roleGrants: + description: Configuration to grant a role. + items: + description: |- + RoleGrant represents the roles to be assigned to users or roles. + See https://clickhouse.com/docs/en/sql-reference/statements/grant#assigning-role-syntax. + properties: + grantees: + description: + List of grantees (users or roles) to grant the + privilege to. + items: + description: + Grantee represents a user or a role to which + privileges or roles are granted. + properties: + role: + type: string + user: + type: string + type: object + minItems: 1 + type: array + roles: + description: List of roles to grant to the grantees. + items: + type: string + minItems: 1 + type: array + withAdminOption: + description: |- + If true, the grant is executed with `ADMIN OPTION` privilege. + See https://clickhouse.com/docs/en/sql-reference/statements/grant#admin-option. + type: boolean + required: + - grantees + - roles + type: object + type: array + serviceName: + description: + Specifies the name of the service that this resource + belongs to + maxLength: 63 + pattern: ^[a-z][-a-z0-9]+$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + required: + - project + - serviceName + type: object + status: + description: ClickhouseGrantStatus defines the observed state of ClickhouseGrant + properties: + conditions: + items: + description: + "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + 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 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index c07e8e6b..4e22cb22 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -25,6 +25,7 @@ resources: - bases/aiven.io_clickhousedatabases.yaml - bases/aiven.io_kafkaschemaregistryacls.yaml - bases/aiven.io_clickhouseroles.yaml + - bases/aiven.io_clickhousegrants.yaml - bases/aiven.io_serviceintegrationendpoints.yaml #+kubebuilder:scaffold:crdkustomizeresource diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 419dfac4..beccaf6a 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -79,6 +79,34 @@ rules: - get - patch - update + - apiGroups: + - aiven.io + resources: + - clickhousegrants + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - aiven.io + resources: + - clickhousegrants/finalizers + verbs: + - create + - get + - update + - apiGroups: + - aiven.io + resources: + - clickhousegrants/status + verbs: + - get + - patch + - update - apiGroups: - aiven.io resources: diff --git a/controllers/clickhousegrant_controller.go b/controllers/clickhousegrant_controller.go new file mode 100644 index 00000000..10fc82cd --- /dev/null +++ b/controllers/clickhousegrant_controller.go @@ -0,0 +1,462 @@ +// Copyright (c) 2024 Aiven, Helsinki, Finland. https://aiven.io/ + +package controllers + +import ( + "context" + "fmt" + "slices" + "strconv" + + "github.com/aiven/aiven-go-client/v2" + avngen "github.com/aiven/go-client-codegen" + "github.com/aiven/go-client-codegen/handler/clickhouse" + "github.com/google/go-cmp/cmp" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/aiven/aiven-operator/api/v1alpha1" + "github.com/aiven/aiven-operator/utils" + chUtils "github.com/aiven/aiven-operator/utils/clickhouse" +) + +// ClickhouseGrantReconciler reconciles a ClickhouseGrant object +type ClickhouseGrantReconciler struct { + Controller +} + +func newClickhouseGrantReconciler(c Controller) reconcilerType { + return &ClickhouseGrantReconciler{Controller: c} +} + +// ClickhouseGrantHandler handles an Aiven ClickhouseGrant +type ClickhouseGrantHandler struct{} + +//+kubebuilder:rbac:groups=aiven.io,resources=clickhousegrants,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=aiven.io,resources=clickhousegrants/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=aiven.io,resources=clickhousegrants/finalizers,verbs=get;create;update + +func (r *ClickhouseGrantReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + return r.reconcileInstance(ctx, req, &ClickhouseGrantHandler{}, &v1alpha1.ClickhouseGrant{}) +} + +func (r *ClickhouseGrantReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.ClickhouseGrant{}). + Complete(r) +} + +func (h *ClickhouseGrantHandler) createOrUpdate(ctx context.Context, avn *aiven.Client, avnGen avngen.Client, obj client.Object, refs []client.Object) error { + g, err := h.convert(obj) + if err != nil { + return err + } + + flatPrivilegeGrants := v1alpha1.FlattenPrivilegeGrants(g.Spec.PrivilegeGrants) + flatRoleGrants := v1alpha1.FlattenRoleGrants(g.Spec.RoleGrants) + apiPrivilegeGrants, apiRoleGrants, err := getGrantsFromApi(ctx, avnGen, g) + if err != nil { + return err + } + + privilegeGrantsToRevoke, _, roleGrantsToRevoke, _ := diffClickhouseGrantSpecToApi(flatPrivilegeGrants, apiPrivilegeGrants, flatRoleGrants, apiRoleGrants) + + // Issue revoke grant statements for privilege and role grants not found in the spec + projectName := g.Spec.Project + serviceName := g.Spec.ServiceName + for _, grantToRevoke := range privilegeGrantsToRevoke { + v1alpha1.ExecuteGrant(ctx, avnGen, chUtils.REVOKE, grantToRevoke, projectName, serviceName) //nolint:errcheck + } + for _, grantToRevoke := range roleGrantsToRevoke { + v1alpha1.ExecuteGrant(ctx, avnGen, chUtils.REVOKE, grantToRevoke, projectName, serviceName) //nolint:errcheck + } + + _, err = g.Spec.ExecuteStatements(ctx, avnGen, chUtils.GRANT) + if err != nil { + return err + } + + meta.SetStatusCondition(&g.Status.Conditions, + getInitializedCondition("Created", + "Instance was created or update on Aiven side")) + + metav1.SetMetaDataAnnotation(&g.ObjectMeta, + processedGenerationAnnotation, strconv.FormatInt(g.GetGeneration(), formatIntBaseDecimal)) + + return nil +} + +func (h *ClickhouseGrantHandler) delete(ctx context.Context, avn *aiven.Client, avnGen avngen.Client, obj client.Object) (bool, error) { + g, err := h.convert(obj) + if err != nil { + return false, err + } + + _, err = g.Spec.ExecuteStatements(ctx, avnGen, chUtils.REVOKE) + if err != nil { + return false, err + } + + return true, nil +} + +func (h *ClickhouseGrantHandler) get(ctx context.Context, avn *aiven.Client, avnGen avngen.Client, obj client.Object) (*corev1.Secret, error) { + g, err := h.convert(obj) + if err != nil { + return nil, err + } + + flatPrivilegeGrants := v1alpha1.FlattenPrivilegeGrants(g.Spec.PrivilegeGrants) + flatRoleGrants := v1alpha1.FlattenRoleGrants(g.Spec.RoleGrants) + apiPrivilegeGrants, apiRoleGrants, err := getGrantsFromApi(ctx, avnGen, g) + if err != nil { + return nil, err + } + + _, privilegeGrantsToAd, _, roleGrantsToAdd := diffClickhouseGrantSpecToApi(flatPrivilegeGrants, apiPrivilegeGrants, flatRoleGrants, apiRoleGrants) + + if len(privilegeGrantsToAd) > 0 || len(roleGrantsToAdd) > 0 { + return nil, fmt.Errorf("missing grants defined in spec: %+v (privilege grants) %+v (role grants)", privilegeGrantsToAd, roleGrantsToAdd) + } + + meta.SetStatusCondition(&g.Status.Conditions, + getRunningCondition(metav1.ConditionTrue, "CheckRunning", + "Instance is running on Aiven side")) + + metav1.SetMetaDataAnnotation(&g.ObjectMeta, instanceIsRunningAnnotation, "true") + return nil, nil +} + +func getGrantsFromApi(ctx context.Context, avnGen avngen.Client, g *v1alpha1.ClickhouseGrant) ([]v1alpha1.PrivilegeGrant, []v1alpha1.RoleGrant, error) { + privileges, err := chUtils.QueryPrivileges(ctx, avnGen, g.Spec.Project, g.Spec.ServiceName) + if err != nil { + return nil, nil, err + } + roleGrants, err := chUtils.QueryRoleGrants(ctx, avnGen, g.Spec.Project, g.Spec.ServiceName) + if err != nil { + return nil, nil, err + } + + apiPrivilegeGrants, err := processGrantsFromApiResponse(privileges, PrivilegeGrantType, convertPrivilegeGrantFromApiStruct) + if err != nil { + return nil, nil, err + } + apiRoleGrants, err := processGrantsFromApiResponse(roleGrants, RoleGrantType, convertRoleGrantFromApiStruct) + if err != nil { + return nil, nil, err + } + return apiPrivilegeGrants, apiRoleGrants, nil +} + +func diffClickhouseGrantSpecToApi(specPrivilegeGrants []v1alpha1.PrivilegeGrant, apiPrivilegeGrants []v1alpha1.PrivilegeGrant, specRoleGrants []v1alpha1.RoleGrant, apiRoleGrants []v1alpha1.RoleGrant) ([]v1alpha1.PrivilegeGrant, []v1alpha1.PrivilegeGrant, []v1alpha1.RoleGrant, []v1alpha1.RoleGrant) { + var privilegeGrantsToRevoke, privilegeGrantsToAdd []v1alpha1.PrivilegeGrant + var roleGrantsToRevoke, roleGrantsToAdd []v1alpha1.RoleGrant + + for _, apiGrant := range apiPrivilegeGrants { + if !containsPrivilegeGrant(specPrivilegeGrants, apiGrant) { + privilegeGrantsToRevoke = append(privilegeGrantsToRevoke, apiGrant) + } + } + + for _, specGrant := range specPrivilegeGrants { + if !containsPrivilegeGrant(apiPrivilegeGrants, specGrant) { + privilegeGrantsToAdd = append(privilegeGrantsToAdd, specGrant) + } + } + + for _, apiGrant := range apiRoleGrants { + if !containsRoleGrant(specRoleGrants, apiGrant) { + roleGrantsToRevoke = append(roleGrantsToRevoke, apiGrant) + } + } + + for _, specGrant := range specRoleGrants { + if !containsRoleGrant(apiRoleGrants, specGrant) { + roleGrantsToAdd = append(roleGrantsToAdd, specGrant) + } + } + + return privilegeGrantsToRevoke, privilegeGrantsToAdd, roleGrantsToRevoke, roleGrantsToAdd +} + +func containsPrivilegeGrant(grants []v1alpha1.PrivilegeGrant, grant chUtils.Grant) bool { + for _, g := range grants { + if cmp.Equal(g, grant) { + return true + } + } + return false +} + +func containsRoleGrant(grants []v1alpha1.RoleGrant, grant v1alpha1.RoleGrant) bool { + for _, g := range grants { + if cmp.Equal(g, grant) { + return true + } + } + return false +} + +func (h *ClickhouseGrantHandler) checkPreconditions(ctx context.Context, avn *aiven.Client, avnGen avngen.Client, obj client.Object) (bool, error) { + /** Preconditions for ClickhouseGrant: + * + * 1. The service is running + * 2. All users and roles specified in spec exist + * 3. All databases specified in spec exist + * 4. All tables specified in spec exist + */ + + g, err := h.convert(obj) + if err != nil { + return false, err + } + + meta.SetStatusCondition(&g.Status.Conditions, + getInitializedCondition("Preconditions", "Checking preconditions")) + + serviceIsRunning, err := checkServiceIsRunning(ctx, avn, avnGen, g.Spec.Project, g.Spec.ServiceName) + if !serviceIsRunning || err != nil { + return false, err + } + + // Service is running, check users and roles specified in spec exist + _, err = checkPrecondition(ctx, g, avnGen, g.Spec.CollectGrantees, chUtils.QueryGrantees, "missing users or roles defined in spec: %v") + if err != nil { + return false, err + } + + // Check that databases specified in spec exist + _, err = checkPrecondition(ctx, g, avnGen, g.Spec.CollectDatabases, chUtils.QueryDatabases, "missing databases defined in spec: %v") + if err != nil { + return false, err + } + + // Check that tables specified in spec exist + _, err = checkPrecondition(ctx, g, avnGen, g.Spec.CollectTables, chUtils.QueryTables, "missing tables defined in spec: %+v") + if err != nil { + return false, err + } + + // Remove previous error conditions + meta.RemoveStatusCondition(&g.Status.Conditions, "Error") + + meta.SetStatusCondition(&g.Status.Conditions, + getInitializedCondition("Preconditions", "Preconditions met")) + + return true, nil +} + +func checkPrecondition[T comparable](ctx context.Context, g *v1alpha1.ClickhouseGrant, avnGen avngen.Client, collectFunc func() []T, queryFunc func(context.Context, avngen.Client, string, string) ([]T, error), errorMsgFormat string) (bool, error) { + itemsInSpec := collectFunc() + itemsInDb, err := queryFunc(ctx, avnGen, g.Spec.Project, g.Spec.ServiceName) + if err != nil { + return false, err + } + missingItems := utils.CheckSliceContainment(itemsInSpec, itemsInDb) + if len(missingItems) > 0 { + err = fmt.Errorf(errorMsgFormat, missingItems) + meta.SetStatusCondition(&g.Status.Conditions, getErrorCondition(errConditionPreconditions, err)) + return false, err + } + return true, nil +} + +func (h *ClickhouseGrantHandler) convert(i client.Object) (*v1alpha1.ClickhouseGrant, error) { + g, ok := i.(*v1alpha1.ClickhouseGrant) + if !ok { + return nil, fmt.Errorf("cannot convert object to ClickhouseGrant") + } + + return g, nil +} + +type ApiPrivilegeGrant struct { + Grantee v1alpha1.Grantee + Privilege string + Database string + Table string + Column string + WithGrantOption bool +} + +type ApiRoleGrant struct { + Grantee v1alpha1.Grantee + Role string + WithAdminOption bool +} + +type GrantType string + +const ( + PrivilegeGrantType GrantType = "PrivilegeGrant" + RoleGrantType GrantType = "RoleGrant" +) + +// GrantColumns defines the columns required for each grant type +var GrantColumns = map[GrantType][]string{ + PrivilegeGrantType: {"user_name", "role_name", "access_type", "database", "table", "column", "is_partial_revoke", "grant_option"}, + RoleGrantType: {"user_name", "role_name", "granted_role_name", "with_admin_option"}, +} + +// Process either privilege or role grants from the API response +func processGrantsFromApiResponse[U any, T any](r *clickhouse.ServiceClickHouseQueryOut, grantType GrantType, processFn func([]U) []T) ([]T, error) { + requiredColumns := GrantColumns[grantType] + columnNameMap, err := validateColumns(r.Meta, requiredColumns) + if err != nil { + return nil, err + } + + var grants []U + for _, dataRow := range r.Data { + grant, err := extractGrant(dataRow, columnNameMap, grantType) + if err != nil { + return nil, err + } + g, ok := grant.(U) + if !ok { + return nil, fmt.Errorf("failed to convert grant to type %T", grant) + } + grants = append(grants, g) + } + + return processFn(grants), nil +} + +// validateColumns checks if all required columns are present in the metadata +func validateColumns(meta []clickhouse.MetaOut, requiredColumns []string) (map[string]int, error) { + columnNameMap := make(map[string]int) + for i, md := range meta { + columnNameMap[md.Name] = i + } + for _, columnName := range requiredColumns { + if _, ok := columnNameMap[columnName]; !ok { + return nil, fmt.Errorf("'system.grants' metadata is missing the '%s' column", columnName) + } + } + return columnNameMap, nil +} + +func extractGrant(dataRow []interface{}, columnNameMap map[string]int, grantType GrantType) (interface{}, error) { + switch grantType { + case PrivilegeGrantType: + grant := ApiPrivilegeGrant{ + Grantee: v1alpha1.Grantee{ + User: getString(dataRow, columnNameMap, "user_name"), + Role: getString(dataRow, columnNameMap, "role_name"), + }, + Privilege: getString(dataRow, columnNameMap, "access_type"), + Database: getString(dataRow, columnNameMap, "database"), + Table: getString(dataRow, columnNameMap, "table"), + Column: getString(dataRow, columnNameMap, "column"), + WithGrantOption: getBool(dataRow, columnNameMap, "grant_option"), + } + return grant, nil + case RoleGrantType: + grant := ApiRoleGrant{ + Grantee: v1alpha1.Grantee{ + User: getString(dataRow, columnNameMap, "user_name"), + Role: getString(dataRow, columnNameMap, "role_name"), + }, + Role: getString(dataRow, columnNameMap, "granted_role_name"), + WithAdminOption: getBool(dataRow, columnNameMap, "with_admin_option"), + } + return grant, nil + default: + return nil, fmt.Errorf("unsupported grant type: %s", grantType) + } +} + +// getString and getBool are helper functions to extract string and boolean values from dataRow based on columnNameMap +func getString(dataRow []interface{}, columnNameMap map[string]int, columnName string) string { + if index, ok := columnNameMap[columnName]; ok && index < len(dataRow) { + if value, ok := dataRow[index].(string); ok { + return value + } + } + return "" +} + +func getBool(dataRow []interface{}, columnNameMap map[string]int, columnName string) bool { + if index, ok := columnNameMap[columnName]; ok && index < len(dataRow) { + return dataRow[index] != float64(0) + } + return false +} + +func convertPrivilegeGrantFromApiStruct(clickhouseGrants []ApiPrivilegeGrant) []v1alpha1.PrivilegeGrant { + grantMap := make(map[string]*v1alpha1.PrivilegeGrant) + for _, chGrant := range clickhouseGrants { + key := chGrant.Grantee.User + chGrant.Grantee.Role + chGrant.Database + chGrant.Table + if grant, exists := grantMap[key]; exists { + // If the grant already exists, append the privilege and column if not empty. + grant.Privileges = appendUnique(grant.Privileges, chGrant.Privilege) + if chGrant.Column != "" { + grant.Columns = appendUnique(grant.Columns, chGrant.Column) + } + } else { + // Create a new PrivilegeGrant if it does not exist. + newGrant := v1alpha1.PrivilegeGrant{ + Grantees: []v1alpha1.Grantee{{User: chGrant.Grantee.User, Role: chGrant.Grantee.Role}}, + Privileges: []string{chGrant.Privilege}, + Database: chGrant.Database, + Table: chGrant.Table, + Columns: nil, + WithGrantOption: chGrant.WithGrantOption, + } + if chGrant.Column != "" { + newGrant.Columns = append(newGrant.Columns, chGrant.Column) + } + grantMap[key] = &newGrant + } + } + + // Extract the values from the map to a slice + var privilegeGrants []v1alpha1.PrivilegeGrant + for _, grant := range grantMap { + privilegeGrants = append(privilegeGrants, *grant) + } + + return privilegeGrants +} + +func appendUnique(slice []string, item string) []string { + for _, elem := range slice { + if elem == item { + return slice + } + } + return append(slice, item) +} + +func convertRoleGrantFromApiStruct(clickhouseRoleGrants []ApiRoleGrant) []v1alpha1.RoleGrant { + grantMap := make(map[string]*v1alpha1.RoleGrant) + + for _, chRoleGrant := range clickhouseRoleGrants { + key := chRoleGrant.Grantee.User + chRoleGrant.Grantee.Role + if grant, exists := grantMap[key]; exists { + if !slices.Contains(grant.Roles, chRoleGrant.Role) { + grant.Roles = append(grant.Roles, chRoleGrant.Role) + } + } else { + // Create a new RoleGrant and add it to the map + grantMap[key] = &v1alpha1.RoleGrant{ + Grantees: []v1alpha1.Grantee{{ + User: chRoleGrant.Grantee.User, + Role: chRoleGrant.Grantee.Role, + }}, + Roles: []string{chRoleGrant.Role}, + WithAdminOption: chRoleGrant.WithAdminOption, + } + } + } + + var roleGrants []v1alpha1.RoleGrant + for _, grant := range grantMap { + roleGrants = append(roleGrants, *grant) + } + + return roleGrants +} diff --git a/controllers/setup.go b/controllers/setup.go index 27d3613a..1650214d 100644 --- a/controllers/setup.go +++ b/controllers/setup.go @@ -29,6 +29,7 @@ func SetupControllers(mgr ctrl.Manager, defaultToken, kubeVersion, operatorVersi "ClickhouseDatabase": newClickhouseDatabaseReconciler, "ClickhouseRole": newClickhouseRoleReconciler, "ClickhouseUser": newClickhouseUserReconciler, + "ClickhouseGrant": newClickhouseGrantReconciler, "ConnectionPool": newConnectionPoolReconciler, "Database": newDatabaseReconciler, "Grafana": newGrafanaReconciler, diff --git a/docs/docs/api-reference/clickhousegrant.md b/docs/docs/api-reference/clickhousegrant.md new file mode 100644 index 00000000..72546a0c --- /dev/null +++ b/docs/docs/api-reference/clickhousegrant.md @@ -0,0 +1,146 @@ +--- +title: "ClickhouseGrant" +--- + +## Usage example + +??? example + ```yaml + apiVersion: aiven.io/v1alpha1 + kind: ClickhouseGrant + metadata: + name: demo-ch-grant + spec: + authSecretRef: + name: aiven-token + key: token + + project: my-aiven-project + serviceName: my-clickhouse + + privilegeGrants: + - grantees: + - user: user1 + - user: my-clickhouse-user-🦄 + privileges: + - SELECT + - INSERT + database: my-db + # If table is not specified, the privileges are granted on all tables in the database + # If columns is not specified, the privileges are granted on all columns in the table + - grantees: + - role: my-role + privileges: + - SELECT + database: my-db + table: my-table + columns: + - col1 + - col2 + + roleGrants: + - roles: + - other-role + grantees: + - user: my-user + - role: my-role + ``` + +## ClickhouseGrant {: #ClickhouseGrant } + +ClickhouseGrant is the Schema for the ClickhouseGrants API. + +**Required** + +- [`apiVersion`](#apiVersion-property){: name='apiVersion-property'} (string). Value `aiven.io/v1alpha1`. +- [`kind`](#kind-property){: name='kind-property'} (string). Value `ClickhouseGrant`. +- [`metadata`](#metadata-property){: name='metadata-property'} (object). Data that identifies the object, including a `name` string and optional `namespace`. +- [`spec`](#spec-property){: name='spec-property'} (object). ClickhouseGrantSpec defines the desired state of ClickhouseGrant. See below for [nested schema](#spec). + +## spec {: #spec } + +_Appears on [`ClickhouseGrant`](#ClickhouseGrant)._ + +ClickhouseGrantSpec defines the desired state of ClickhouseGrant. + +**Required** + +- [`project`](#spec.project-property){: name='spec.project-property'} (string, Immutable, Pattern: `^[a-zA-Z0-9_-]+$`, MaxLength: 63). Identifies the project this resource belongs to. +- [`serviceName`](#spec.serviceName-property){: name='spec.serviceName-property'} (string, Immutable, Pattern: `^[a-z][-a-z0-9]+$`, MaxLength: 63). Specifies the name of the service that this resource belongs to. + +**Optional** + +- [`authSecretRef`](#spec.authSecretRef-property){: name='spec.authSecretRef-property'} (object). Authentication reference to Aiven token in a secret. See below for [nested schema](#spec.authSecretRef). +- [`privilegeGrants`](#spec.privilegeGrants-property){: name='spec.privilegeGrants-property'} (array of objects). Configuration to grant a privilege. See below for [nested schema](#spec.privilegeGrants). +- [`roleGrants`](#spec.roleGrants-property){: name='spec.roleGrants-property'} (array of objects). Configuration to grant a role. See below for [nested schema](#spec.roleGrants). + +## authSecretRef {: #spec.authSecretRef } + +_Appears on [`spec`](#spec)._ + +Authentication reference to Aiven token in a secret. + +**Required** + +- [`key`](#spec.authSecretRef.key-property){: name='spec.authSecretRef.key-property'} (string, MinLength: 1). +- [`name`](#spec.authSecretRef.name-property){: name='spec.authSecretRef.name-property'} (string, MinLength: 1). + +## privilegeGrants {: #spec.privilegeGrants } + +_Appears on [`spec`](#spec)._ + +Configuration to grant a privilege. + +**Required** + +- [`database`](#spec.privilegeGrants.database-property){: name='spec.privilegeGrants.database-property'} (string). The database that the grant refers to. +- [`grantees`](#spec.privilegeGrants.grantees-property){: name='spec.privilegeGrants.grantees-property'} (array of objects, MinItems: 1). List of grantees (users or roles) to grant the privilege to. See below for [nested schema](#spec.privilegeGrants.grantees). +- [`privileges`](#spec.privilegeGrants.privileges-property){: name='spec.privilegeGrants.privileges-property'} (array of strings). The privileges to grant, i.e. `INSERT`, `SELECT`. +See https://clickhouse.com/docs/en/sql-reference/statements/grant#assigning-role-syntax. + +**Optional** + +- [`columns`](#spec.privilegeGrants.columns-property){: name='spec.privilegeGrants.columns-property'} (array of strings). The column that the grant refers to. +- [`table`](#spec.privilegeGrants.table-property){: name='spec.privilegeGrants.table-property'} (string). The tables that the grant refers to. +- [`withGrantOption`](#spec.privilegeGrants.withGrantOption-property){: name='spec.privilegeGrants.withGrantOption-property'} (boolean). If true, then the grantee (user or role) get the permission to execute the `GRANT`` query. +Users can grant privileges of the same scope they have and less. +See https://clickhouse.com/docs/en/sql-reference/statements/grant#granting-privilege-syntax. + +### grantees {: #spec.privilegeGrants.grantees } + +_Appears on [`spec.privilegeGrants`](#spec.privilegeGrants)._ + +List of grantees (users or roles) to grant the privilege to. + +**Optional** + +- [`role`](#spec.privilegeGrants.grantees.role-property){: name='spec.privilegeGrants.grantees.role-property'} (string). +- [`user`](#spec.privilegeGrants.grantees.user-property){: name='spec.privilegeGrants.grantees.user-property'} (string). + +## roleGrants {: #spec.roleGrants } + +_Appears on [`spec`](#spec)._ + +Configuration to grant a role. + +**Required** + +- [`grantees`](#spec.roleGrants.grantees-property){: name='spec.roleGrants.grantees-property'} (array of objects, MinItems: 1). List of grantees (users or roles) to grant the privilege to. See below for [nested schema](#spec.roleGrants.grantees). +- [`roles`](#spec.roleGrants.roles-property){: name='spec.roleGrants.roles-property'} (array of strings, MinItems: 1). List of roles to grant to the grantees. + +**Optional** + +- [`withAdminOption`](#spec.roleGrants.withAdminOption-property){: name='spec.roleGrants.withAdminOption-property'} (boolean). If true, the grant is executed with `ADMIN OPTION` privilege. +See https://clickhouse.com/docs/en/sql-reference/statements/grant#admin-option. + +### grantees {: #spec.roleGrants.grantees } + +_Appears on [`spec.roleGrants`](#spec.roleGrants)._ + +List of grantees (users or roles) to grant the privilege to. + +**Optional** + +- [`role`](#spec.roleGrants.grantees.role-property){: name='spec.roleGrants.grantees.role-property'} (string). +- [`user`](#spec.roleGrants.grantees.user-property){: name='spec.roleGrants.grantees.user-property'} (string). + diff --git a/docs/docs/api-reference/examples/clickhousegrant.yaml b/docs/docs/api-reference/examples/clickhousegrant.yaml new file mode 100644 index 00000000..47f59b55 --- /dev/null +++ b/docs/docs/api-reference/examples/clickhousegrant.yaml @@ -0,0 +1,40 @@ + +apiVersion: aiven.io/v1alpha1 +kind: ClickhouseGrant +metadata: + name: demo-ch-grant +spec: + authSecretRef: + name: aiven-token + key: token + + project: my-aiven-project + serviceName: my-clickhouse + + privilegeGrants: + - grantees: + - user: user1 + - user: my-clickhouse-user-🦄 + privileges: + - SELECT + - INSERT + database: my-db + # If table is not specified, the privileges are granted on all tables in the database + # If columns is not specified, the privileges are granted on all columns in the table + - grantees: + - role: my-role + privileges: + - SELECT + database: my-db + table: my-table + columns: + - col1 + - col2 + + roleGrants: + - roles: + - other-role + grantees: + - user: my-user + - role: my-role + diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 6da64676..d9bd0f36 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -92,6 +92,7 @@ nav: - api-reference/clickhousedatabase.md - api-reference/clickhouserole.md - api-reference/clickhouseuser.md + - api-reference/clickhousegrant.md - api-reference/connectionpool.md - api-reference/database.md - api-reference/grafana.md diff --git a/go.mod b/go.mod index b46261ad..b2426c93 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,10 @@ module github.com/aiven/aiven-operator go 1.22 require ( + github.com/ClickHouse/clickhouse-go/v2 v2.25.0 github.com/aiven/aiven-go-client/v2 v2.23.0 github.com/aiven/go-api-schemas v1.73.0 - github.com/aiven/go-client-codegen v0.9.0 + github.com/aiven/go-client-codegen v0.10.1-0.20240614150227-f366a68fc5a3 github.com/dave/jennifer v1.7.0 github.com/docker/go-units v0.5.0 github.com/ghodss/yaml v1.0.0 @@ -29,12 +30,16 @@ require ( ) require ( + github.com/ClickHouse/ch-go v0.61.5 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-faster/city v1.0.1 // indirect + github.com/go-faster/errors v0.7.1 // indirect github.com/go-logr/zapr v1.2.3 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect @@ -44,15 +49,16 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect github.com/google/gofuzz v1.1.0 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-retryablehttp v0.7.6 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-version v1.6.0 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.7 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -61,23 +67,28 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/ginkgo/v2 v2.15.0 // indirect + github.com/paulmach/orb v0.11.1 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect - github.com/rs/zerolog v1.32.0 // indirect + github.com/rs/zerolog v1.33.0 // indirect + github.com/segmentio/asm v1.2.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.6.0 // indirect - go.uber.org/zap v1.24.0 // indirect + go.opentelemetry.io/otel v1.26.0 // indirect + go.opentelemetry.io/otel/trace v1.26.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect - golang.org/x/sys v0.20.0 // indirect + golang.org/x/sys v0.21.0 // indirect golang.org/x/term v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect golang.org/x/time v0.3.0 // indirect diff --git a/go.sum b/go.sum index a903399f..3e471df6 100644 --- a/go.sum +++ b/go.sum @@ -33,18 +33,23 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4= +github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg= +github.com/ClickHouse/clickhouse-go/v2 v2.25.0 h1:rKscwqgQHzWBTZySZDcHKxgs0Ad+xFULfZvo26W5UlY= +github.com/ClickHouse/clickhouse-go/v2 v2.25.0/go.mod h1:iDTViXk2Fgvf1jn2dbJd1ys+fBkdD1UMRnXlwmhijhQ= github.com/aiven/aiven-go-client/v2 v2.23.0 h1:vNxae4tzkFTPQcAYwJ1e6XU9uOfqS3spgjyWHk5Nhwo= github.com/aiven/aiven-go-client/v2 v2.23.0/go.mod h1:Ox/NZj1lJPT8LQya3kYq6bjQ4GdHpcauB/f+6DJygEE= github.com/aiven/go-api-schemas v1.73.0 h1:zdg0787LogcLwbHnc+K6/Ei6si4EsGK0c1LzdvkQIJQ= github.com/aiven/go-api-schemas v1.73.0/go.mod h1:q1aut0Kfpf1TAbL7Cum9kso9y/xvnNXnq79Sp3aAhzw= -github.com/aiven/go-client-codegen v0.9.0 h1:2X6JOsyfUb7tNdVBLYSYLMuIwmLdkH+ebwel+5HRwlo= -github.com/aiven/go-client-codegen v0.9.0/go.mod h1:6fa3VkprxBFcWZQnxFs6hYMUmzoE/o+Phgq8i36l7sk= +github.com/aiven/go-client-codegen v0.10.1-0.20240614150227-f366a68fc5a3 h1:sEFq1FgA+zBWTyHABg6cbTwHOns1LRuzk5XDxsZD9gA= +github.com/aiven/go-client-codegen v0.10.1-0.20240614150227-f366a68fc5a3/go.mod h1:Sajbdpjn1/m5g2D6EDfiSnxl9pj9hxe8+hpG1CkCkhs= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -86,6 +91,10 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= +github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= +github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= +github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -149,6 +158,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= @@ -160,6 +170,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -179,8 +190,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -192,8 +203,8 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.7.6 h1:TwRYfx2z2C4cLbXmT8I5PgP/xmuqASDyiVuGYfs9GZM= -github.com/hashicorp/go-retryablehttp v0.7.6/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -218,11 +229,16 @@ github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dv github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -249,11 +265,11 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -267,6 +283,11 @@ github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= +github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= +github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= +github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -300,9 +321,15 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= -github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -326,37 +353,48 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= +go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= +go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -421,6 +459,7 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= @@ -444,6 +483,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -489,8 +529,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= @@ -638,14 +678,16 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= diff --git a/tests/clickhousegrant_test.go b/tests/clickhousegrant_test.go new file mode 100644 index 00000000..853b050e --- /dev/null +++ b/tests/clickhousegrant_test.go @@ -0,0 +1,333 @@ +package tests + +import ( + "context" + "crypto/tls" + "fmt" + "slices" + "testing" + + "github.com/ClickHouse/clickhouse-go/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + + "github.com/aiven/aiven-operator/api/v1alpha1" + chUtils "github.com/aiven/aiven-operator/utils/clickhouse" +) + +func chConnFromSecret(ctx context.Context, secret *corev1.Secret) (clickhouse.Conn, error) { + c, err := clickhouse.Open(&clickhouse.Options{ + Addr: []string{fmt.Sprintf("%s:%s", string(secret.Data["HOST"]), string(secret.Data["PORT"]))}, + Auth: clickhouse.Auth{ + Username: string(secret.Data["CLICKHOUSE_USER"]), + Password: string(secret.Data["CLICKHOUSE_PASSWORD"]), + }, + TLS: &tls.Config{ + InsecureSkipVerify: true, + }, + }) + return c, err +} + +func getClickhouseGrantYaml(project, chName, cloudName, dbName, userName string) string { + return fmt.Sprintf(` +apiVersion: aiven.io/v1alpha1 +kind: Clickhouse +metadata: + name: %[2]s +spec: + authSecretRef: + name: aiven-token + key: token + + project: %[1]s + cloudName: %[3]s + plan: startup-16 +--- +apiVersion: aiven.io/v1alpha1 +kind: ClickhouseDatabase +metadata: + name: %[4]s +spec: + authSecretRef: + name: aiven-token + key: token + + project: %[1]s + serviceName: %[2]s +--- +apiVersion: aiven.io/v1alpha1 +kind: ClickhouseUser +metadata: + name: %[5]s +spec: + authSecretRef: + name: aiven-token + key: token + + project: %[1]s + serviceName: %[2]s +--- +apiVersion: aiven.io/v1alpha1 +kind: ClickhouseRole +metadata: + name: writer +spec: + authSecretRef: + name: aiven-token + key: token + + project: %[1]s + serviceName: %[2]s + role: writer +--- +apiVersion: aiven.io/v1alpha1 +kind: ClickhouseGrant +metadata: + name: test-grant +spec: + authSecretRef: + name: aiven-token + key: token + + project: %[1]s + serviceName: %[2]s + + privilegeGrants: + - grantees: + - user: %[5]s + - role: writer + privileges: + - SELECT + - INSERT + database: %[4]s + table: example-table + columns: + - col1 + withGrantOption: true + + roleGrants: + - roles: + - writer + grantees: + - user: %[5]s + withAdminOption: true +`, project, chName, cloudName, dbName, userName) +} + +func TestClickhouseGrant(t *testing.T) { + t.Parallel() + defer recoverPanic(t) + + // GIVEN + ctx, cancel := testCtx() + defer cancel() + + chName := randName("clickhouse") + userName := "clickhouse-user" + dbName := "clickhouse-db" + + yml := getClickhouseGrantYaml(cfg.Project, chName, cfg.PrimaryCloudName, dbName, userName) + s := NewSession(ctx, k8sClient, cfg.Project) + + // Cleans test afterward + defer s.Destroy() + + // WHEN + // Applies given manifest + require.NoError(t, s.Apply(yml)) + + // Waits kube objects + ch := new(v1alpha1.Clickhouse) + // user := new(v1alpha1.ClickhouseUser) + db := new(v1alpha1.ClickhouseDatabase) + grant := new(v1alpha1.ClickhouseGrant) + + require.NoError(t, s.GetRunning(ch, chName)) + // require.NoError(t, s.GetRunning(user, userName)) + require.NoError(t, s.GetRunning(db, dbName)) + + // Constructs connection to ClickHouse from service secret + secret, err := s.GetSecret(ch.GetName()) + require.NoError(t, err, "Failed to get secret") + conn, err := chConnFromSecret(ctx, secret) + require.NoError(t, err, "failed to connect to ClickHouse") + + // THEN + // Initially grant isn't created because the table that it is targeting doesn't exist + err = s.GetRunning(grant, "test-grant") + require.ErrorContains(t, err, "unable to wait for preconditions: missing tables defined in spec: [{Database:clickhouse-db Table:example-table}]") + + // Create example-table in clickhouse-db + createTableQuery := fmt.Sprintf("CREATE TABLE `%s`.`example-table` (col1 String, col2 Int32) ENGINE = MergeTree() ORDER BY col1", dbName) + _, err = conn.Query(ctx, createTableQuery) + require.NoError(t, err) + + // Clear conditions to stop erroring out in GetRunning. The condition is also removed in ClickhouseGrant + // checkPreconditions but due to timing issues the tests may fail if we don't remove it here. + meta.RemoveStatusCondition(grant.Conditions(), "Error") + errStatus := k8sClient.Status().Update(ctx, grant) + require.NoError(t, errStatus) + + grant = new(v1alpha1.ClickhouseGrant) + // ... and wait for the grant to be created and running + require.NoError(t, s.GetRunning(grant, "test-grant")) + + // THEN + // Query and collect ClickhouseGrant results + results, err := queryAndCollectResults[ClickhouseGrant](ctx, conn, chUtils.QueryNonAivenPrivileges) + require.NoError(t, err) + + filteredResults := filterPrivilegeGrantResults(results) + assert.Len(t, filteredResults, 4) + assert.ElementsMatch(t, filteredResults, expectedPrivilegeGrants) + + // Query and collect ClickhouseRoleGrant results + roleGrantResults, err := queryAndCollectResults[ClickhouseRoleGrant](ctx, conn, chUtils.RoleGrantsQuery) + require.NoError(t, err) + + // Override GrantedRoleId to nil, it changes between test runs + for i := range roleGrantResults { + roleGrantResults[i].GrantedRoleId = nil + } + + assert.Len(t, roleGrantResults, 1) + assert.ElementsMatch(t, roleGrantResults, expectedRoleGrants) +} + +func queryAndCollectResults[T any](ctx context.Context, conn clickhouse.Conn, query string) ([]T, error) { + var results []T + rows, err := conn.Query(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var result T + err := rows.ScanStruct(&result) + if err != nil { + return nil, err + } + results = append(results, result) + } + + return results, nil +} + +// Removes Aiven roles from the results +func filterPrivilegeGrantResults(results []ClickhouseGrant) []ClickhouseGrant { + var filteredResults []ClickhouseGrant + for _, r := range results { + isAivenUser := r.UserName != nil && slices.Contains(chUtils.InternalAivenRoles, *r.UserName) + isRole := r.UserName == nil && r.RoleName != nil + if isRole || !isAivenUser { + filteredResults = append(filteredResults, r) + } + } + return filteredResults +} + +type ClickhouseGrant struct { + UserName *string `ch:"user_name"` + RoleName *string `ch:"role_name"` + AccessType string `ch:"access_type"` + Database *string `ch:"database"` + Table *string `ch:"table"` + Column *string `ch:"column"` + IsPartialRevoke bool `ch:"is_partial_revoke"` + GrantOption bool `ch:"grant_option"` +} + +/** + * Expected privilege grants are constructed from this part of the manifest: + * + * privilegeGrants: + * - grantees: + * - user: %[5]s + * - role: writer + * privileges: + * - SELECT + * - INSERT + * database: %[4]s + * table: example-table + * columns: + * - col1 + * withGrantOption: true + */ +var expectedPrivilegeGrants = []ClickhouseGrant{ + { + UserName: ptr("clickhouse-user"), + RoleName: nil, + AccessType: "SELECT", + Database: ptr("clickhouse-db"), + Table: ptr("example-table"), + Column: ptr("col1"), + IsPartialRevoke: false, + GrantOption: true, + }, + { + UserName: ptr("clickhouse-user"), + RoleName: nil, + AccessType: "INSERT", + Database: ptr("clickhouse-db"), + Table: ptr("example-table"), + Column: ptr("col1"), + IsPartialRevoke: false, + GrantOption: true, + }, + { + UserName: nil, + RoleName: ptr("writer"), + AccessType: "SELECT", + Database: ptr("clickhouse-db"), + Table: ptr("example-table"), + Column: ptr("col1"), + IsPartialRevoke: false, + GrantOption: true, + }, + { + UserName: nil, + RoleName: ptr("writer"), + AccessType: "INSERT", + Database: ptr("clickhouse-db"), + Table: ptr("example-table"), + Column: ptr("col1"), + IsPartialRevoke: false, + GrantOption: true, + }, +} + +type ClickhouseRoleGrant struct { + UserName *string `ch:"user_name"` + RoleName *string `ch:"role_name"` + GrantedRoleName *string `ch:"granted_role_name"` + GrantedRoleId *string `ch:"granted_role_id"` + GrantedRoleIsDefault bool `ch:"granted_role_is_default"` + WithAdminOption bool `ch:"with_admin_option"` +} + +/** + * Expected role grants are constructed from this part of the manifest: + * + * roleGrants: + * - roles: + * - writer + * grantees: + * - user: %[5]s + * withAdminOption: true + */ +var expectedRoleGrants = []ClickhouseRoleGrant{ + { + UserName: ptr("clickhouse-user"), + RoleName: nil, + GrantedRoleName: ptr("writer"), + GrantedRoleId: nil, // Not actually nil, changes between test runs. We override this in the test. + GrantedRoleIsDefault: true, + WithAdminOption: true, + }, +} + +func ptr(s string) *string { return &s } diff --git a/utils/clickhouse/clickhouse_grant.go b/utils/clickhouse/clickhouse_grant.go new file mode 100644 index 00000000..536e9ae3 --- /dev/null +++ b/utils/clickhouse/clickhouse_grant.go @@ -0,0 +1,120 @@ +package chUtils + +import ( + "context" + "fmt" + "strings" + + avngen "github.com/aiven/go-client-codegen" + "github.com/aiven/go-client-codegen/handler/clickhouse" +) + +type StatementType string + +const ( + GRANT StatementType = "GRANT" + REVOKE StatementType = "REVOKE" +) + +// TODO: Move to clickhousegrant_types.go once the issue below is resolved +// See: https://github.com/kubernetes-sigs/controller-tools/issues/383 +type Grant interface { + // Returns the 1. main part (privileges or roles), 2. grantees part and 3. query options + ConstructParts(t StatementType) (string, string, string) +} + +var ( + // 'aiven', 'aiven_monitoring', and 'avnadmin' are Aiven-managed users + InternalAivenRoles = []string{"aiven", "aiven_monitoring", "avnadmin"} + // "OR user_name IS NULL" is needed to include roles in the result + QueryNonAivenPrivileges = fmt.Sprintf("SELECT * FROM system.grants WHERE user_name NOT IN('%s') OR user_name IS NULL", strings.Join(InternalAivenRoles, "', '")) + queryNonAivenUsers = fmt.Sprintf("SELECT name FROM system.users WHERE name NOT IN('%s')", strings.Join(InternalAivenRoles, "', '")) + + internalDatabases = []string{"default", "INFORMATION_SCHEMA", "information_schema", "system"} + queryAllDatabases = fmt.Sprintf("SELECT name FROM system.databases WHERE name NOT IN('%s')", strings.Join(internalDatabases, "', '")) + queryAllTables = fmt.Sprintf("SELECT database, name FROM system.tables WHERE database NOT IN('%s')", strings.Join(internalDatabases, "', '")) +) + +const ( + rolesQuery = "SELECT name FROM system.roles" + RoleGrantsQuery = "SELECT * FROM system.role_grants" +) + +func QueryPrivileges(ctx context.Context, avnGen avngen.Client, projectName, serviceName string) (*clickhouse.ServiceClickHouseQueryOut, error) { + res, err := ExecuteClickHouseQuery(ctx, avnGen, projectName, serviceName, QueryNonAivenPrivileges) + if err != nil { + return nil, err + } + return res, nil +} + +func QueryRoleGrants(ctx context.Context, avnGen avngen.Client, projectName, serviceName string) (*clickhouse.ServiceClickHouseQueryOut, error) { + res, err := ExecuteClickHouseQuery(ctx, avnGen, projectName, serviceName, RoleGrantsQuery) + if err != nil { + return nil, err + } + return res, nil +} + +func QueryGrantees(ctx context.Context, avnGen avngen.Client, projectName, serviceName string) ([]string, error) { + resUsers, err := ExecuteClickHouseQuery(ctx, avnGen, projectName, serviceName, queryNonAivenUsers) + if err != nil { + return nil, err + } + resRoles, err := ExecuteClickHouseQuery(ctx, avnGen, projectName, serviceName, rolesQuery) + if err != nil { + return nil, err + } + + users := extractColumnValues(resUsers.Data, 0) + roles := extractColumnValues(resRoles.Data, 0) + + return append(users, roles...), nil +} + +func QueryDatabases(ctx context.Context, avnGen avngen.Client, projectName, serviceName string) ([]string, error) { + resDatabases, err := ExecuteClickHouseQuery(ctx, avnGen, projectName, serviceName, queryAllDatabases) + if err != nil { + return nil, err + } + + databases := extractColumnValues(resDatabases.Data, 0) + + return databases, nil +} + +type DatabaseAndTable struct { + Database string + Table string +} + +func QueryTables(ctx context.Context, avnGen avngen.Client, projectName, serviceName string) ([]DatabaseAndTable, error) { + resDatabases, err := ExecuteClickHouseQuery(ctx, avnGen, projectName, serviceName, queryAllTables) + if err != nil { + return nil, err + } + + databases := extractColumnValues(resDatabases.Data, 0) + tables := extractColumnValues(resDatabases.Data, 1) + + databaseAndTables := make([]DatabaseAndTable, 0, len(databases)) + for i := range len(databases) { + databaseAndTables = append(databaseAndTables, DatabaseAndTable{ + Database: databases[i], + Table: tables[i], + }) + } + + return databaseAndTables, nil +} + +// Helper function to extract column values from a nested array +func extractColumnValues(data [][]interface{}, columnIndex int) []string { + values := make([]string, 0, len(data)) + for _, row := range data { + if value, ok := row[columnIndex].(string); ok && value != "" { + values = append(values, value) + } + } + return values +} diff --git a/utils/clickhouse/clickhouse_query.go b/utils/clickhouse/clickhouse_query.go new file mode 100644 index 00000000..e34c4270 --- /dev/null +++ b/utils/clickhouse/clickhouse_query.go @@ -0,0 +1,26 @@ +package chUtils + +import ( + "context" + "fmt" + + avngen "github.com/aiven/go-client-codegen" + "github.com/aiven/go-client-codegen/handler/clickhouse" +) + +const ( + defaultDatabase = "system" +) + +func ExecuteClickHouseQuery(ctx context.Context, avnGen avngen.Client, project, serviceName, statement string) (*clickhouse.ServiceClickHouseQueryOut, error) { + res, err := avnGen.ServiceClickHouseQuery(ctx, project, serviceName, &clickhouse.ServiceClickHouseQueryIn{ + // We are running GRANT and REVOKE which don't need to be ran against a + // specific database. Here "system" is used as its guaranteed to exist. + Database: defaultDatabase, + Query: statement, + }) + if err != nil { + return nil, fmt.Errorf("ClickHouse query error: %w", err) + } + return res, nil +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 00000000..1e65ca76 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,47 @@ +package utils + +import ( + "github.com/google/go-cmp/cmp" +) + +// Applies a function to each element of a slice, returning a new slice with the results +func MapSlice[T any, U any](slice []T, f func(T) U) []U { + mapped := make([]U, len(slice)) + for i, v := range slice { + mapped[i] = f(v) + } + return mapped +} + +// Removes duplicate elements from a slice +func UniqueSliceElements[T comparable](inputSlice []T) []T { + uniqueSlice := make([]T, 0, len(inputSlice)) + seen := make(map[T]bool, len(inputSlice)) + for _, element := range inputSlice { + if !seen[element] { + uniqueSlice = append(uniqueSlice, element) + seen[element] = true + } + } + return uniqueSlice +} + +func CheckSliceContainment[T comparable](needles, haystack []T) []T { + missingElements := []T{} + + // Check if each element in needles is contained in haystack + for _, needle := range needles { + found := false + for _, hay := range haystack { + if cmp.Equal(needle, hay) { + found = true + break + } + } + if !found { + missingElements = append(missingElements, needle) + } + } + + return missingElements +}