diff --git a/Makefile b/Makefile index 59a0e58..660ae4b 100644 --- a/Makefile +++ b/Makefile @@ -68,8 +68,12 @@ test: manifests generate fmt vet envtest ## Run tests. .PHONY: create-kind-cluster create-kind-cluster: - docker network inspect $(KIND_CLUSTER) >/dev/null || docker network create $(KIND_CLUSTER) - kind create cluster --wait=60s --name=$(KIND_CLUSTER) --config=kind-config.yaml + @if ! kind get clusters | grep -q $(KIND_CLUSTER); then \ + docker network inspect $(KIND_CLUSTER) >/dev/null 2>&1 || docker network create $(KIND_CLUSTER); \ + kind create cluster --wait=60s --name=$(KIND_CLUSTER) --config=kind-config.yaml; \ + else \ + echo "Cluster $(KIND_CLUSTER) already exists"; \ + fi .PHONY: delete-kind-cluster delete-kind-cluster: diff --git a/PROJECT b/PROJECT index 4c9c403..d5c137b 100644 --- a/PROJECT +++ b/PROJECT @@ -19,10 +19,20 @@ resources: version: v1alpha1 - api: crdVersion: v1 + namespaced: true + controller: true + domain: lagoon.sh + group: crd + kind: RelationalDatabaseProvider + path: github.com/uselagoon/dbaas-controller/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true controller: true domain: lagoon.sh group: crd - kind: DatabaseMySQLProvider + kind: MongoDBProvider path: github.com/uselagoon/dbaas-controller/api/v1alpha1 version: v1alpha1 version: "3" diff --git a/README.md b/README.md index e6852e8..2b96a32 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,13 @@ It allows for provisiong and deprovisioning of shared MySQL/MariaDB, PostgreSQL, ## Current Status of the Project WIP - Work in Progress -There is still a lot of work to be done on this project. The current status is that the controller is able to provision and deprovision MySQL databases. But there is still a lot of work to be done to make it production ready. +There is still a some work to be done on this project. The current status is that the controller is able to provision and deprovision MySQL databases. But there is still a lot of work to be done to make it production ready. - [x] Setup e2e tests - [x] Provision MySQL databases (basic) - no support for additional users, seeding, etc. - [x] Deprovision MySQL databases -- [ ] Provision PostgreSQL databases -- [ ] Deprovision PostgreSQL databases +- [x] Provision PostgreSQL databases +- [x] Deprovision PostgreSQL databases - [ ] Provision MongoDB databases - [ ] Deprovision MongoDB databases - [ ] Plan to migrate from old `dbaaas-operator` to `dbaas-controller` @@ -58,8 +58,8 @@ Key Features: To interact with the dbaas-controller, the following CRDs are introduced: -- DatabaseXProvider - - This CRD is used to define a database provider, such as MySQL, PostgreSQL, or MongoDB. +- RelationalDatabaseProvider + - This CRD is used to define a database provider, such as MySQL and PostgreSQL. - DatabaseRequest - DatabaseMigration @@ -67,48 +67,50 @@ Basic usage of the CRs in combination with the dbaas-controller is outlined belo - DatabaseRequest: Lagoon creates a DatabaseRequest CR to request a database instance - The dbaas-controller processes the request and provisions the database instance based on the request - - The controller uses the relevant DatabaseXProvider CR to determine how it should provision the database + - The controller uses the relevant RelationalDatabaseProvider CR to determine how it should provision the database -## DatabaseMySQLProvider CRD Documentation +## RelationalDatabaseProvider CRD Documentation -The `DatabaseMySQLProvider` CRD defines a Kubernetes-native way to manage MySQL database connections and configurations. This custom resource allows to define MySQL databases. +The `RelationalDatabaseProvider` CRD defines a Kubernetes-native way to manage relational database connections and configurations. This custom resource allows to define MySQL and PostgreSQL databases. -Use the status mysqlConnectionStatus field to check the status of the MySQL connections defined in the spec. +Use the status connectionStatus field to check the status of the MySQL connections defined in the spec. -### DatabaseMySQLProvider Spec Fields +### RelationalDatabaseProvider Spec Fields +- kind (required): The type of database provider, which can be either mysql or postgresql. - scope (required): Defines the scope of the database request, which influences the environment setup. Valid values are production, development, and custom. Defaults to development if not specified. -- mysqlConnections (required): A list of `MySQLConnection` objects that detail the connection parameters to MySQL databases. At least one connection must be defined. +- connections (required): A list of `connection` objects that detail the connection parameters to MySQL or PostgreSQL databases. At least one connection must be defined. -- MySQLConnection Fields - - name (required): A unique name for the MySQL database connection, used to identify and reference the connection in database requests. - - hostname (required): The hostname of the MySQL database server. - - replicaHostnames (optional): A list of hostnames for the MySQL replica databases. +- connection Fields + - name (required): A unique name for the MySQL or PostgreSQL database connection, used to identify and reference the connection in database requests. + - hostname (required): The hostname of the MySQL or PostgreSQL database server. + - replicaHostnames (optional): A list of hostnames for the MySQLi or PostgreSQL replica databases. - passwordSecretRef (required): A reference to a Kubernetes Secret containing the password for the database connection. - - port (required): The port on which the MySQL database server is listening. Must be between 1 and 65535. - - username (required): The username for logging into the MySQL database. + - port (required): The port on which the MySQLi or PostgreSQL database server is listening. Must be between 1 and 65535. + - username (required): The username for logging into the MySQLi or PostgreSQL database. - enabled (required): A flag indicating whether this database connection is enabled. Defaults to true. -### DatabaseMySQLProvider Status Fields +### RelationalDatabaseProvider Status Fields -- conditions: Provides detailed conditions of the MySQLProvider like readiness, errors, etc. -- mysqlConnectionStatus: A list of statuses for the MySQL connections defined in the spec. +- conditions: Provides detailed conditions of the `RelationalDatabaseProvider` like readiness, errors, etc. +- connectionStatus: A list of statuses for the MySQL or PostgreSQL connections defined in the spec. -- MySQLConnectionStatus Fields - - hostname (required): The hostname of the MySQL database server. - - mysqlVersion (required): The version of the MySQL server. +- connectionStatus Fields + - hostname (required): The hostname of the MySQL or PostgreSQL database server. + - mysqlVersion (required): The version of the MySQL or PostgreSQL server. - enabled (required): Indicates if the database connection is enabled. - status (required): The current status of the database connection, with valid values being available and unavailable. -- observedGeneration: Reflects the generation of the most recently observed DatabaseMySQLProvider object. +- observedGeneration: Reflects the generation of the most recently observed RelationalDatabaseProvider object. -### DatabaseMySQLProvider Example +### RelationalDatabaseProvider Example ```yaml apiVersion: v1alpha1 -kind: DatabaseMySQLProvider +kind: RelationalDatabaseProvider metadata: name: example-mysql-provider spec: + kind: mysql scope: development mysqlConnections: - name: primary-db diff --git a/api/v1alpha1/database_types.go b/api/v1alpha1/database_types.go new file mode 100644 index 0000000..30a0098 --- /dev/null +++ b/api/v1alpha1/database_types.go @@ -0,0 +1,47 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +// ConnectionStatus defines the status of a database connection, it can be either relational database or mongodb +type ConnectionStatus struct { + //+kubebuilder:required + // Name is the name of the database connection + // it is used to identify the connection. Please use a unique name + // for each connection. This field will be used in the DatabaseRequest + // to reference the connection. The relationaldatabaseprovider and mongodbprovider + // controllers will error if the name is not unique. + Name string `json:"name"` + + //+kubebuilder:required + // Hostname is the hostname of the database + Hostname string `json:"hostname"` + + //+kubebuilder:required + // DatabaseVersion is the version of the database + DatabaseVersion string `json:"databaseVersion"` + + //+kubebuilder:required + //+kubebuilder:validation:Required + // Enabled is a flag to indicate whether a database is enabled or not + Enabled bool `json:"enabled"` + + //+kubebuilder:required + //+kubebuilder:validation:Required + //+kubebuilder:validation:Enum=available;unavailable + // Status is the status of the database + Status string `json:"status"` +} diff --git a/api/v1alpha1/databasemysqlprovider_types.go b/api/v1alpha1/databasemysqlprovider_types.go deleted file mode 100644 index af04f03..0000000 --- a/api/v1alpha1/databasemysqlprovider_types.go +++ /dev/null @@ -1,143 +0,0 @@ -/* -Copyright 2024. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1alpha1 - -import ( - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// MySQLConnection defines the connection to a MySQL database -type MySQLConnection struct { - // Name is the name of the MySQL database connection - // it is used to identify the connection. Please use a unique name - // for each connection. This field will be used in the DatabaseRequest - // to reference the connection. The databasemysqlprovider controller will - // error if the name is not unique. - Name string `json:"name"` - - //+kubebuilder:required - // Hostname is the hostname of the MySQL database - Hostname string `json:"hostname"` - - //+kubebuilder:optional - // ReplicaHostnames is the list of hostnames of the MySQL database replicas - ReplicaHostnames []string `json:"replicaHostnames,omitempty"` - - //+kubebuilder:required - // PasswordSecretRef is the reference to the secret containing the password - PasswordSecretRef v1.SecretReference `json:"passwordSecretRef"` - - //+kubebuilder:required - //+kubebuilder:validation:Required - //+kubebuilder:validation:Minimum=1 - //+kubebuilder:validation:Maximum=65535 - // Port is the port of the MySQL database - Port int `json:"port"` - - //+kubebuilder:required - // Username is the username of the MySQL database - Username string `json:"username"` - - //+kubebuilder:required - //+kubebuilder:default:=true - // Enabled is a flag to enable or disable the MySQL database - Enabled bool `json:"enabled"` -} - -// DatabaseMySQLProviderSpec defines the desired state of DatabaseMySQLProvider -type DatabaseMySQLProviderSpec struct { - //+kubebuilder:required - //+kubebuilder:validation:Required - //+kubebuilder:validation:Enum=production;development;custom - //+kubebuilder:default:=development - // Scope is the scope of the database request - // it can be either "production" or "development" or "custom" - Scope string `json:"scope"` - - //+kubebuilder:validation:MinItems=1 - // MySQLConnections defines the connection to a MySQL database - MySQLConnections []MySQLConnection `json:"mysqlConnections"` -} - -// MySQLConnectionStatus defines the status of a MySQL database connection -type MySQLConnectionStatus struct { - //+kubebuilder:required - // Name is the name of the MySQL database connection - // it is used to identify the connection. Please use a unique name - // for each connection. This field will be used in the DatabaseRequest - // to reference the connection. The databasemysqlprovider controller will - // error if the name is not unique. - Name string `json:"name"` - - //+kubebuilder:required - // Hostname is the hostname of the MySQL database - Hostname string `json:"hostname"` - - //+kubebuilder:required - // MySQLVersion is the version of the MySQL database - MySQLVersion string `json:"mysqlVersion"` - - //+kubebuilder:required - //+kubebuilder:validation:Required - // Enabled is a flag to indicate whether a MySQL database is enabled or not - Enabled bool `json:"enabled"` - - //+kubebuilder:required - //+kubebuilder:validation:Required - //+kubebuilder:validation:Enum=available;unavailable - // Status is the status of the MySQL database - Status string `json:"status"` -} - -// DatabaseMySQLProviderStatus defines the observed state of DatabaseMySQLProvider -type DatabaseMySQLProviderStatus struct { - // Conditions defines the status conditions - Conditions []metav1.Condition `json:"conditions,omitempty"` - - // MySQLConnectionStatus provides the status of the MySQL database - MySQLConnectionStatus []MySQLConnectionStatus `json:"mysqlConnectionStatus,omitempty"` - - // ObservedGeneration is the last observed generation - ObservedGeneration int64 `json:"observedGeneration,omitempty"` -} - -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status -//+kubebuilder:resource:scope=Cluster - -// DatabaseMySQLProvider is the Schema for the databasemysqlproviders API -type DatabaseMySQLProvider struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec DatabaseMySQLProviderSpec `json:"spec,omitempty"` - Status DatabaseMySQLProviderStatus `json:"status,omitempty"` -} - -//+kubebuilder:object:root=true - -// DatabaseMySQLProviderList contains a list of DatabaseMySQLProvider -type DatabaseMySQLProviderList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []DatabaseMySQLProvider `json:"items"` -} - -func init() { - SchemeBuilder.Register(&DatabaseMySQLProvider{}, &DatabaseMySQLProviderList{}) -} diff --git a/api/v1alpha1/mongodbprovider_types.go b/api/v1alpha1/mongodbprovider_types.go new file mode 100644 index 0000000..2945648 --- /dev/null +++ b/api/v1alpha1/mongodbprovider_types.go @@ -0,0 +1,138 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// MongoDBAuth defines the authorisation mechanisms that mongo can use +type MongoDBAuth struct { + //+kubebuilder:required + //+kubebuilder:validation:Required + //+kubebuilder:validation:Enum=SCRAM-SHA-1;SCRAM-SHA-256;MONGODB-CR;MongoDB-AWS;X509 + // Mechanism is the authentication mechanism for the MongoDB connection + // https://www.mongodb.com/docs/drivers/go/current/fundamentals/auth/#std-label-golang-authentication-mechanisms + Mechanism string `json:"mechanism"` + + //+kubebuilder:optional + //+kubebuilder:default:admin + // Source is the source of the authentication mechanism for the MongoDB connection + Source string `json:"source,omitempty"` + + //+kubebuilder:optional + //+kubebuilder:default:true + // TLS is the flag to enable or disable TLS for the MongoDB connection + TLS bool `json:"tls,omitempty"` +} + +type MongoDBConnection struct { + //+kubebuilder:required + // Name is the name of the MongoDB connection + // it is used to identify the connection. Please use a unique name + // for each connection. This field will be used in the MongoDBProvider + // to reference the connection. The MongoDBProvider controller will + // error if the name is not unique. + Name string `json:"name"` + + //+kubebuilder:required + // Hostname is the hostname of the relational database + Hostname string `json:"hostname"` + + //+kubebuilder:optional + // ReplicaHostnames is the list of hostnames of the relational database replicas + ReplicaHostnames []string `json:"replicaHostnames,omitempty"` + + //+kubebuilder:required + // PasswordSecretRef is the reference to the secret containing the password + PasswordSecretRef v1.SecretReference `json:"passwordSecretRef"` + + //+kubebuilder:optional + //+kubebuilder:default:=27017 + //+kubebuilder:validation:Required + //+kubebuilder:validation:Minimum=1 + //+kubebuilder:validation:Maximum=65535 + // Port is the port of the relational database + Port int `json:"port,omitempty"` + + //+kubebuilder:optional + //+kubebuilder:default:=root + // Username is the username of the relational database + Username string `json:"username,omitempty"` + + //+kubebuilder:required + // Auth is the authentication mechanism for the MongoDB connection + Auth MongoDBAuth `json:"auth"` + + //+kubebuilder:optional + //+kubebuilder:default:=true + // Enabled is a flag to enable or disable the relational database + Enabled bool `json:"enabled,omitempty"` +} + +// MongoDBProviderSpec defines the desired state of MongoDBProvider +type MongoDBProviderSpec struct { + //+kubebuilder:required + //+kubebuilder:validation:Required + //+kubebuilder:validation:Enum=production;development;custom + //+kubebuilder:default:=development + // Scope is the scope of the database request + // it can be either "production" or "development" or "custom" + Scope string `json:"scope"` + + //+kubebuilder:validation:MinItems=1 + // Connections defines the connection to a relational database + Connections []MongoDBConnection `json:"connections"` +} + +// MongoDBProviderStatus defines the observed state of MongoDBProvider +type MongoDBProviderStatus struct { + // Conditions defines the status conditions + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // ConnectionStatus provides the status of the relational database + ConnectionStatus []ConnectionStatus `json:"connectionStatus,omitempty"` // nolint:lll + + // ObservedGeneration is the last observed generation + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// MongoDBDProvider is the Schema for the mongodbproviders API +type MongoDBDProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec MongoDBProviderSpec `json:"spec,omitempty"` + Status MongoDBProviderStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// MongoDBProviderList contains a list of MongoDBProvider +type MongoDBProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []MongoDBDProvider `json:"items"` +} + +func init() { + SchemeBuilder.Register(&MongoDBDProvider{}, &MongoDBProviderList{}) +} diff --git a/api/v1alpha1/relationaldatabaseprovider_types.go b/api/v1alpha1/relationaldatabaseprovider_types.go new file mode 100644 index 0000000..f26c235 --- /dev/null +++ b/api/v1alpha1/relationaldatabaseprovider_types.go @@ -0,0 +1,121 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Connection defines the connection to a relational database like MySQL or PostgreSQL +type Connection struct { + //+kubebuilder:required + // Name is the name of the relational database like MySQL or PostgreSQL connection + // it is used to identify the connection. Please use a unique name + // for each connection. This field will be used in the DatabaseRequest + // to reference the connection. The relationaldatabaseprovider controller will + // error if the name is not unique. + Name string `json:"name"` + + //+kubebuilder:required + // Hostname is the hostname of the relational database + Hostname string `json:"hostname"` + + //+kubebuilder:optional + // ReplicaHostnames is the list of hostnames of the relational database replicas + ReplicaHostnames []string `json:"replicaHostnames,omitempty"` + + //+kubebuilder:required + // PasswordSecretRef is the reference to the secret containing the password + PasswordSecretRef v1.SecretReference `json:"passwordSecretRef"` + + //+kubebuilder:required + //+kubebuilder:validation:Required + //+kubebuilder:validation:Minimum=1 + //+kubebuilder:validation:Maximum=65535 + // Port is the port of the relational database + Port int `json:"port"` + + //+kubebuilder:required + // Username is the username of the relational database + Username string `json:"username"` + + //+kubebuilder:optional + //+kubebuilder:default:=true + // Enabled is a flag to enable or disable the relational database + Enabled bool `json:"enabled,omitempty"` +} + +// RelationalDatabaseProviderSpec defines the desired state of RelationalDatabaseProvider +type RelationalDatabaseProviderSpec struct { + //+kubebuilder:required + //+kubebuilder:validation:Required + //+kubebuilder:validation:Enum=mysql;postgres + // Type is the type of the relational database provider + // it can be either "mysql" or "postgres" + Type string `json:"type"` + + //+kubebuilder:required + //+kubebuilder:validation:Required + //+kubebuilder:validation:Enum=production;development;custom + //+kubebuilder:default:=development + // Scope is the scope of the database request + // it can be either "production" or "development" or "custom" + Scope string `json:"scope"` + + //+kubebuilder:validation:MinItems=1 + // Connections defines the connection to a relational database + Connections []Connection `json:"connections"` +} + +// RelationalDatabaseProviderStatus defines the observed state of RelationalDatabaseProvider +type RelationalDatabaseProviderStatus struct { + // Conditions defines the status conditions + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // ConnectionStatus provides the status of the relational database + ConnectionStatus []ConnectionStatus `json:"connectionStatus,omitempty"` // nolint:lll + + // ObservedGeneration is the last observed generation + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:resource:scope=Cluster + +// RelationalDatabaseProvider is the Schema for the relationaldatabaseprovider API +type RelationalDatabaseProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RelationalDatabaseProviderSpec `json:"spec,omitempty"` + Status RelationalDatabaseProviderStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// RelationalDatabaseProviderList contains a list of RelationalDatabaseProvider +type RelationalDatabaseProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []RelationalDatabaseProvider `json:"items"` +} + +func init() { + SchemeBuilder.Register(&RelationalDatabaseProvider{}, &RelationalDatabaseProviderList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 5fbe96c..9e4de90 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -21,8 +21,8 @@ limitations under the License. package v1alpha1 import ( - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -41,6 +41,42 @@ func (in *AdditionalUser) DeepCopy() *AdditionalUser { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Connection) DeepCopyInto(out *Connection) { + *out = *in + if in.ReplicaHostnames != nil { + in, out := &in.ReplicaHostnames, &out.ReplicaHostnames + *out = make([]string, len(*in)) + copy(*out, *in) + } + out.PasswordSecretRef = in.PasswordSecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Connection. +func (in *Connection) DeepCopy() *Connection { + if in == nil { + return nil + } + out := new(Connection) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnectionStatus) DeepCopyInto(out *ConnectionStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionStatus. +func (in *ConnectionStatus) DeepCopy() *ConnectionStatus { + if in == nil { + return nil + } + out := new(ConnectionStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DatabaseConnectionReference) DeepCopyInto(out *DatabaseConnectionReference) { *out = *in @@ -73,7 +109,7 @@ func (in *DatabaseInfo) DeepCopy() *DatabaseInfo { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DatabaseMySQLProvider) DeepCopyInto(out *DatabaseMySQLProvider) { +func (in *DatabaseRequest) DeepCopyInto(out *DatabaseRequest) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) @@ -81,18 +117,18 @@ func (in *DatabaseMySQLProvider) DeepCopyInto(out *DatabaseMySQLProvider) { in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseMySQLProvider. -func (in *DatabaseMySQLProvider) DeepCopy() *DatabaseMySQLProvider { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseRequest. +func (in *DatabaseRequest) DeepCopy() *DatabaseRequest { if in == nil { return nil } - out := new(DatabaseMySQLProvider) + out := new(DatabaseRequest) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DatabaseMySQLProvider) DeepCopyObject() runtime.Object { +func (in *DatabaseRequest) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -100,31 +136,31 @@ func (in *DatabaseMySQLProvider) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DatabaseMySQLProviderList) DeepCopyInto(out *DatabaseMySQLProviderList) { +func (in *DatabaseRequestList) DeepCopyInto(out *DatabaseRequestList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]DatabaseMySQLProvider, len(*in)) + *out = make([]DatabaseRequest, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseMySQLProviderList. -func (in *DatabaseMySQLProviderList) DeepCopy() *DatabaseMySQLProviderList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseRequestList. +func (in *DatabaseRequestList) DeepCopy() *DatabaseRequestList { if in == nil { return nil } - out := new(DatabaseMySQLProviderList) + out := new(DatabaseRequestList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DatabaseMySQLProviderList) DeepCopyObject() runtime.Object { +func (in *DatabaseRequestList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -132,56 +168,110 @@ func (in *DatabaseMySQLProviderList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DatabaseMySQLProviderSpec) DeepCopyInto(out *DatabaseMySQLProviderSpec) { +func (in *DatabaseRequestSpec) DeepCopyInto(out *DatabaseRequestSpec) { *out = *in - if in.MySQLConnections != nil { - in, out := &in.MySQLConnections, &out.MySQLConnections - *out = make([]MySQLConnection, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } + if in.Seed != nil { + in, out := &in.Seed, &out.Seed + *out = new(v1.SecretReference) + **out = **in + } + if in.AdditionalUsers != nil { + in, out := &in.AdditionalUsers, &out.AdditionalUsers + *out = make([]AdditionalUser, len(*in)) + copy(*out, *in) + } + if in.DatabaseConnectionReference != nil { + in, out := &in.DatabaseConnectionReference, &out.DatabaseConnectionReference + *out = new(DatabaseConnectionReference) + **out = **in + } + if in.ForcedReconcilation != nil { + in, out := &in.ForcedReconcilation, &out.ForcedReconcilation + *out = (*in).DeepCopy() } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseMySQLProviderSpec. -func (in *DatabaseMySQLProviderSpec) DeepCopy() *DatabaseMySQLProviderSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseRequestSpec. +func (in *DatabaseRequestSpec) DeepCopy() *DatabaseRequestSpec { if in == nil { return nil } - out := new(DatabaseMySQLProviderSpec) + out := new(DatabaseRequestSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DatabaseMySQLProviderStatus) DeepCopyInto(out *DatabaseMySQLProviderStatus) { +func (in *DatabaseRequestStatus) DeepCopyInto(out *DatabaseRequestStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.MySQLConnectionStatus != nil { - in, out := &in.MySQLConnectionStatus, &out.MySQLConnectionStatus - *out = make([]MySQLConnectionStatus, len(*in)) + if in.ObservedDatabaseConnectionReference != nil { + in, out := &in.ObservedDatabaseConnectionReference, &out.ObservedDatabaseConnectionReference + *out = new(DatabaseConnectionReference) + **out = **in + } + if in.DatabaseInfo != nil { + in, out := &in.DatabaseInfo, &out.DatabaseInfo + *out = new(DatabaseInfo) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseRequestStatus. +func (in *DatabaseRequestStatus) DeepCopy() *DatabaseRequestStatus { + if in == nil { + return nil + } + out := new(DatabaseRequestStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MongoDBAuth) DeepCopyInto(out *MongoDBAuth) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoDBAuth. +func (in *MongoDBAuth) DeepCopy() *MongoDBAuth { + if in == nil { + return nil + } + out := new(MongoDBAuth) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MongoDBConnection) DeepCopyInto(out *MongoDBConnection) { + *out = *in + if in.ReplicaHostnames != nil { + in, out := &in.ReplicaHostnames, &out.ReplicaHostnames + *out = make([]string, len(*in)) copy(*out, *in) } + out.PasswordSecretRef = in.PasswordSecretRef + out.Auth = in.Auth } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseMySQLProviderStatus. -func (in *DatabaseMySQLProviderStatus) DeepCopy() *DatabaseMySQLProviderStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoDBConnection. +func (in *MongoDBConnection) DeepCopy() *MongoDBConnection { if in == nil { return nil } - out := new(DatabaseMySQLProviderStatus) + out := new(MongoDBConnection) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DatabaseRequest) DeepCopyInto(out *DatabaseRequest) { +func (in *MongoDBDProvider) DeepCopyInto(out *MongoDBDProvider) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) @@ -189,18 +279,18 @@ func (in *DatabaseRequest) DeepCopyInto(out *DatabaseRequest) { in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseRequest. -func (in *DatabaseRequest) DeepCopy() *DatabaseRequest { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoDBDProvider. +func (in *MongoDBDProvider) DeepCopy() *MongoDBDProvider { if in == nil { return nil } - out := new(DatabaseRequest) + out := new(MongoDBDProvider) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DatabaseRequest) DeepCopyObject() runtime.Object { +func (in *MongoDBDProvider) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -208,31 +298,31 @@ func (in *DatabaseRequest) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DatabaseRequestList) DeepCopyInto(out *DatabaseRequestList) { +func (in *MongoDBProviderList) DeepCopyInto(out *MongoDBProviderList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]DatabaseRequest, len(*in)) + *out = make([]MongoDBDProvider, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseRequestList. -func (in *DatabaseRequestList) DeepCopy() *DatabaseRequestList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoDBProviderList. +func (in *MongoDBProviderList) DeepCopy() *MongoDBProviderList { if in == nil { return nil } - out := new(DatabaseRequestList) + out := new(MongoDBProviderList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DatabaseRequestList) DeepCopyObject() runtime.Object { +func (in *MongoDBProviderList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -240,103 +330,158 @@ func (in *DatabaseRequestList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DatabaseRequestSpec) DeepCopyInto(out *DatabaseRequestSpec) { +func (in *MongoDBProviderSpec) DeepCopyInto(out *MongoDBProviderSpec) { *out = *in - if in.Seed != nil { - in, out := &in.Seed, &out.Seed - *out = new(corev1.SecretReference) - **out = **in - } - if in.AdditionalUsers != nil { - in, out := &in.AdditionalUsers, &out.AdditionalUsers - *out = make([]AdditionalUser, len(*in)) - copy(*out, *in) - } - if in.DatabaseConnectionReference != nil { - in, out := &in.DatabaseConnectionReference, &out.DatabaseConnectionReference - *out = new(DatabaseConnectionReference) - **out = **in - } - if in.ForcedReconcilation != nil { - in, out := &in.ForcedReconcilation, &out.ForcedReconcilation - *out = (*in).DeepCopy() + if in.Connections != nil { + in, out := &in.Connections, &out.Connections + *out = make([]MongoDBConnection, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseRequestSpec. -func (in *DatabaseRequestSpec) DeepCopy() *DatabaseRequestSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoDBProviderSpec. +func (in *MongoDBProviderSpec) DeepCopy() *MongoDBProviderSpec { if in == nil { return nil } - out := new(DatabaseRequestSpec) + out := new(MongoDBProviderSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DatabaseRequestStatus) DeepCopyInto(out *DatabaseRequestStatus) { +func (in *MongoDBProviderStatus) DeepCopyInto(out *MongoDBProviderStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.ObservedDatabaseConnectionReference != nil { - in, out := &in.ObservedDatabaseConnectionReference, &out.ObservedDatabaseConnectionReference - *out = new(DatabaseConnectionReference) - **out = **in + if in.ConnectionStatus != nil { + in, out := &in.ConnectionStatus, &out.ConnectionStatus + *out = make([]ConnectionStatus, len(*in)) + copy(*out, *in) } - if in.DatabaseInfo != nil { - in, out := &in.DatabaseInfo, &out.DatabaseInfo - *out = new(DatabaseInfo) - **out = **in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoDBProviderStatus. +func (in *MongoDBProviderStatus) DeepCopy() *MongoDBProviderStatus { + if in == nil { + return nil } + out := new(MongoDBProviderStatus) + in.DeepCopyInto(out) + return out } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseRequestStatus. -func (in *DatabaseRequestStatus) DeepCopy() *DatabaseRequestStatus { +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RelationalDatabaseProvider) DeepCopyInto(out *RelationalDatabaseProvider) { + *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 RelationalDatabaseProvider. +func (in *RelationalDatabaseProvider) DeepCopy() *RelationalDatabaseProvider { if in == nil { return nil } - out := new(DatabaseRequestStatus) + out := new(RelationalDatabaseProvider) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RelationalDatabaseProvider) 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 *MySQLConnection) DeepCopyInto(out *MySQLConnection) { +func (in *RelationalDatabaseProviderList) DeepCopyInto(out *RelationalDatabaseProviderList) { *out = *in - if in.ReplicaHostnames != nil { - in, out := &in.ReplicaHostnames, &out.ReplicaHostnames - *out = make([]string, len(*in)) - copy(*out, *in) + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]RelationalDatabaseProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } - out.PasswordSecretRef = in.PasswordSecretRef } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MySQLConnection. -func (in *MySQLConnection) DeepCopy() *MySQLConnection { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RelationalDatabaseProviderList. +func (in *RelationalDatabaseProviderList) DeepCopy() *RelationalDatabaseProviderList { if in == nil { return nil } - out := new(MySQLConnection) + out := new(RelationalDatabaseProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RelationalDatabaseProviderList) 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 *RelationalDatabaseProviderSpec) DeepCopyInto(out *RelationalDatabaseProviderSpec) { + *out = *in + if in.Connections != nil { + in, out := &in.Connections, &out.Connections + *out = make([]Connection, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RelationalDatabaseProviderSpec. +func (in *RelationalDatabaseProviderSpec) DeepCopy() *RelationalDatabaseProviderSpec { + if in == nil { + return nil + } + out := new(RelationalDatabaseProviderSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *MySQLConnectionStatus) DeepCopyInto(out *MySQLConnectionStatus) { +func (in *RelationalDatabaseProviderStatus) DeepCopyInto(out *RelationalDatabaseProviderStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ConnectionStatus != nil { + in, out := &in.ConnectionStatus, &out.ConnectionStatus + *out = make([]ConnectionStatus, len(*in)) + copy(*out, *in) + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MySQLConnectionStatus. -func (in *MySQLConnectionStatus) DeepCopy() *MySQLConnectionStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RelationalDatabaseProviderStatus. +func (in *RelationalDatabaseProviderStatus) DeepCopy() *RelationalDatabaseProviderStatus { if in == nil { return nil } - out := new(MySQLConnectionStatus) + out := new(RelationalDatabaseProviderStatus) in.DeepCopyInto(out) return out } diff --git a/cmd/main.go b/cmd/main.go index a3a72aa..e52df93 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -18,7 +18,6 @@ package main import ( "crypto/tls" - "database/sql" "flag" "os" @@ -37,7 +36,7 @@ import ( crdv1alpha1 "github.com/uselagoon/dbaas-controller/api/v1alpha1" "github.com/uselagoon/dbaas-controller/internal/controller" - "github.com/uselagoon/dbaas-controller/internal/database/mysql" + "github.com/uselagoon/dbaas-controller/internal/database" //+kubebuilder:scaffold:imports ) @@ -127,28 +126,32 @@ func main() { os.Exit(1) } - mysqlClient := &mysql.MySQLImpl{ - ConnectionCache: make(map[string]*sql.DB), - } + relDBClient := database.New() if err = (&controller.DatabaseRequestReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - MySQLClient: mysqlClient, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + RelationalDatabaseClient: relDBClient, }).SetupWithManager(mgr, maxConcurrentReconciles); err != nil { setupLog.Error(err, "unable to create controller", "controller", "DatabaseRequest") os.Exit(1) } - if err = (&controller.DatabaseMySQLProviderReconciler{ + if err = (&controller.RelationalDatabaseProviderReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - MySQLClient: mysqlClient, + RelDBClient: relDBClient, }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "DatabaseMySQLProvider") + setupLog.Error(err, "unable to create controller", "controller", "RelationalDatabaseProvider") + os.Exit(1) + } + if err = (&controller.MongoDBProviderReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "MongoDBProvider") os.Exit(1) } //+kubebuilder:scaffold:builder - if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") os.Exit(1) diff --git a/config/crd/bases/crd.lagoon.sh_mongodbdproviders.yaml b/config/crd/bases/crd.lagoon.sh_mongodbdproviders.yaml new file mode 100644 index 0000000..fc307f0 --- /dev/null +++ b/config/crd/bases/crd.lagoon.sh_mongodbdproviders.yaml @@ -0,0 +1,261 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: mongodbdproviders.crd.lagoon.sh +spec: + group: crd.lagoon.sh + names: + kind: MongoDBDProvider + listKind: MongoDBDProviderList + plural: mongodbdproviders + singular: mongodbdprovider + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: MongoDBDProvider is the Schema for the mongodbproviders 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: MongoDBProviderSpec defines the desired state of MongoDBProvider + properties: + connections: + description: Connections defines the connection to a relational database + items: + properties: + auth: + description: Auth is the authentication mechanism for the MongoDB + connection + properties: + mechanism: + description: |- + Mechanism is the authentication mechanism for the MongoDB connection + https://www.mongodb.com/docs/drivers/go/current/fundamentals/auth/#std-label-golang-authentication-mechanisms + enum: + - SCRAM-SHA-1 + - SCRAM-SHA-256 + - MONGODB-CR + - MongoDB-AWS + - X509 + type: string + source: + description: Source is the source of the authentication + mechanism for the MongoDB connection + type: string + tls: + description: TLS is the flag to enable or disable TLS for + the MongoDB connection + type: boolean + required: + - mechanism + type: object + enabled: + default: true + description: Enabled is a flag to enable or disable the relational + database + type: boolean + hostname: + description: Hostname is the hostname of the relational database + type: string + name: + description: |- + Name is the name of the MongoDB connection + it is used to identify the connection. Please use a unique name + for each connection. This field will be used in the MongoDBProvider + to reference the connection. The MongoDBProvider controller will + error if the name is not unique. + type: string + passwordSecretRef: + description: PasswordSecretRef is the reference to the secret + containing the password + properties: + name: + description: name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: namespace defines the space within which the + secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + port: + default: 27017 + description: Port is the port of the relational database + maximum: 65535 + minimum: 1 + type: integer + replicaHostnames: + description: ReplicaHostnames is the list of hostnames of the + relational database replicas + items: + type: string + type: array + username: + default: root + description: Username is the username of the relational database + type: string + required: + - auth + - hostname + - name + - passwordSecretRef + type: object + minItems: 1 + type: array + scope: + default: development + description: |- + Scope is the scope of the database request + it can be either "production" or "development" or "custom" + enum: + - production + - development + - custom + type: string + required: + - connections + - scope + type: object + status: + description: MongoDBProviderStatus defines the observed state of MongoDBProvider + properties: + conditions: + description: Conditions defines the status 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 + connectionStatus: + description: ConnectionStatus provides the status of the relational + database + items: + description: ConnectionStatus defines the status of a database + connection, it can be either relational database or mongodb + properties: + databaseVersion: + description: DatabaseVersion is the version of the database + type: string + enabled: + description: Enabled is a flag to indicate whether a database + is enabled or not + type: boolean + hostname: + description: Hostname is the hostname of the database + type: string + name: + description: |- + Name is the name of the database connection + it is used to identify the connection. Please use a unique name + for each connection. This field will be used in the DatabaseRequest + to reference the connection. The relationaldatabaseprovider and mongodbprovider + controllers will error if the name is not unique. + type: string + status: + description: Status is the status of the database + enum: + - available + - unavailable + type: string + required: + - databaseVersion + - enabled + - hostname + - name + - status + type: object + type: array + observedGeneration: + description: ObservedGeneration is the last observed generation + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/crd.lagoon.sh_mongodbproviders.yaml b/config/crd/bases/crd.lagoon.sh_mongodbproviders.yaml new file mode 100644 index 0000000..fc307f0 --- /dev/null +++ b/config/crd/bases/crd.lagoon.sh_mongodbproviders.yaml @@ -0,0 +1,261 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: mongodbdproviders.crd.lagoon.sh +spec: + group: crd.lagoon.sh + names: + kind: MongoDBDProvider + listKind: MongoDBDProviderList + plural: mongodbdproviders + singular: mongodbdprovider + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: MongoDBDProvider is the Schema for the mongodbproviders 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: MongoDBProviderSpec defines the desired state of MongoDBProvider + properties: + connections: + description: Connections defines the connection to a relational database + items: + properties: + auth: + description: Auth is the authentication mechanism for the MongoDB + connection + properties: + mechanism: + description: |- + Mechanism is the authentication mechanism for the MongoDB connection + https://www.mongodb.com/docs/drivers/go/current/fundamentals/auth/#std-label-golang-authentication-mechanisms + enum: + - SCRAM-SHA-1 + - SCRAM-SHA-256 + - MONGODB-CR + - MongoDB-AWS + - X509 + type: string + source: + description: Source is the source of the authentication + mechanism for the MongoDB connection + type: string + tls: + description: TLS is the flag to enable or disable TLS for + the MongoDB connection + type: boolean + required: + - mechanism + type: object + enabled: + default: true + description: Enabled is a flag to enable or disable the relational + database + type: boolean + hostname: + description: Hostname is the hostname of the relational database + type: string + name: + description: |- + Name is the name of the MongoDB connection + it is used to identify the connection. Please use a unique name + for each connection. This field will be used in the MongoDBProvider + to reference the connection. The MongoDBProvider controller will + error if the name is not unique. + type: string + passwordSecretRef: + description: PasswordSecretRef is the reference to the secret + containing the password + properties: + name: + description: name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: namespace defines the space within which the + secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + port: + default: 27017 + description: Port is the port of the relational database + maximum: 65535 + minimum: 1 + type: integer + replicaHostnames: + description: ReplicaHostnames is the list of hostnames of the + relational database replicas + items: + type: string + type: array + username: + default: root + description: Username is the username of the relational database + type: string + required: + - auth + - hostname + - name + - passwordSecretRef + type: object + minItems: 1 + type: array + scope: + default: development + description: |- + Scope is the scope of the database request + it can be either "production" or "development" or "custom" + enum: + - production + - development + - custom + type: string + required: + - connections + - scope + type: object + status: + description: MongoDBProviderStatus defines the observed state of MongoDBProvider + properties: + conditions: + description: Conditions defines the status 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 + connectionStatus: + description: ConnectionStatus provides the status of the relational + database + items: + description: ConnectionStatus defines the status of a database + connection, it can be either relational database or mongodb + properties: + databaseVersion: + description: DatabaseVersion is the version of the database + type: string + enabled: + description: Enabled is a flag to indicate whether a database + is enabled or not + type: boolean + hostname: + description: Hostname is the hostname of the database + type: string + name: + description: |- + Name is the name of the database connection + it is used to identify the connection. Please use a unique name + for each connection. This field will be used in the DatabaseRequest + to reference the connection. The relationaldatabaseprovider and mongodbprovider + controllers will error if the name is not unique. + type: string + status: + description: Status is the status of the database + enum: + - available + - unavailable + type: string + required: + - databaseVersion + - enabled + - hostname + - name + - status + type: object + type: array + observedGeneration: + description: ObservedGeneration is the last observed generation + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/crd.lagoon.sh_databasemysqlproviders.yaml b/config/crd/bases/crd.lagoon.sh_relationaldatabaseproviders.yaml similarity index 80% rename from config/crd/bases/crd.lagoon.sh_databasemysqlproviders.yaml rename to config/crd/bases/crd.lagoon.sh_relationaldatabaseproviders.yaml index 230de27..f431cb4 100644 --- a/config/crd/bases/crd.lagoon.sh_databasemysqlproviders.yaml +++ b/config/crd/bases/crd.lagoon.sh_relationaldatabaseproviders.yaml @@ -4,20 +4,20 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.14.0 - name: databasemysqlproviders.crd.lagoon.sh + name: relationaldatabaseproviders.crd.lagoon.sh spec: group: crd.lagoon.sh names: - kind: DatabaseMySQLProvider - listKind: DatabaseMySQLProviderList - plural: databasemysqlproviders - singular: databasemysqlprovider + kind: RelationalDatabaseProvider + listKind: RelationalDatabaseProviderList + plural: relationaldatabaseproviders + singular: relationaldatabaseprovider scope: Cluster versions: - name: v1alpha1 schema: openAPIV3Schema: - description: DatabaseMySQLProvider is the Schema for the databasemysqlproviders + description: RelationalDatabaseProvider is the Schema for the relationaldatabaseprovider API properties: apiVersion: @@ -38,27 +38,29 @@ spec: metadata: type: object spec: - description: DatabaseMySQLProviderSpec defines the desired state of DatabaseMySQLProvider + description: RelationalDatabaseProviderSpec defines the desired state + of RelationalDatabaseProvider properties: - mysqlConnections: - description: MySQLConnections defines the connection to a MySQL database + connections: + description: Connections defines the connection to a relational database items: - description: MySQLConnection defines the connection to a MySQL database + description: Connection defines the connection to a relational database + like MySQL or PostgreSQL properties: enabled: default: true - description: Enabled is a flag to enable or disable the MySQL + description: Enabled is a flag to enable or disable the relational database type: boolean hostname: - description: Hostname is the hostname of the MySQL database + description: Hostname is the hostname of the relational database type: string name: description: |- - Name is the name of the MySQL database connection + Name is the name of the relational database like MySQL or PostgreSQL connection it is used to identify the connection. Please use a unique name for each connection. This field will be used in the DatabaseRequest - to reference the connection. The databasemysqlprovider controller will + to reference the connection. The relationaldatabaseprovider controller will error if the name is not unique. type: string passwordSecretRef: @@ -76,21 +78,20 @@ spec: type: object x-kubernetes-map-type: atomic port: - description: Port is the port of the MySQL database + description: Port is the port of the relational database maximum: 65535 minimum: 1 type: integer replicaHostnames: description: ReplicaHostnames is the list of hostnames of the - MySQL database replicas + relational database replicas items: type: string type: array username: - description: Username is the username of the MySQL database + description: Username is the username of the relational database type: string required: - - enabled - hostname - name - passwordSecretRef @@ -109,13 +110,22 @@ spec: - development - custom type: string + type: + description: |- + Type is the type of the relational database provider + it can be either "mysql" or "postgres" + enum: + - mysql + - postgres + type: string required: - - mysqlConnections + - connections - scope + - type type: object status: - description: DatabaseMySQLProviderStatus defines the observed state of - DatabaseMySQLProvider + description: RelationalDatabaseProviderStatus defines the observed state + of RelationalDatabaseProvider properties: conditions: description: Conditions defines the status conditions @@ -187,41 +197,41 @@ spec: - type type: object type: array - mysqlConnectionStatus: - description: MySQLConnectionStatus provides the status of the MySQL + connectionStatus: + description: ConnectionStatus provides the status of the relational database items: - description: MySQLConnectionStatus defines the status of a MySQL - database connection + description: ConnectionStatus defines the status of a database + connection, it can be either relational database or mongodb properties: + databaseVersion: + description: DatabaseVersion is the version of the database + type: string enabled: - description: Enabled is a flag to indicate whether a MySQL database + description: Enabled is a flag to indicate whether a database is enabled or not type: boolean hostname: - description: Hostname is the hostname of the MySQL database - type: string - mysqlVersion: - description: MySQLVersion is the version of the MySQL database + description: Hostname is the hostname of the database type: string name: description: |- - Name is the name of the MySQL database connection + Name is the name of the database connection it is used to identify the connection. Please use a unique name for each connection. This field will be used in the DatabaseRequest - to reference the connection. The databasemysqlprovider controller will - error if the name is not unique. + to reference the connection. The relationaldatabaseprovider and mongodbprovider + controllers will error if the name is not unique. type: string status: - description: Status is the status of the MySQL database + description: Status is the status of the database enum: - available - unavailable type: string required: + - databaseVersion - enabled - hostname - - mysqlVersion - name - status type: object diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 4a952b0..f32119f 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -3,20 +3,22 @@ # It should be run by config/default resources: - bases/crd.lagoon.sh_databaserequests.yaml -- bases/crd.lagoon.sh_databasemysqlproviders.yaml +- bases/crd.lagoon.sh_relationaldatabaseproviders.yaml +- bases/crd.lagoon.sh_mongodbproviders.yaml #+kubebuilder:scaffold:crdkustomizeresource patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD #- path: patches/webhook_in_databaserequests.yaml -#- path: patches/webhook_in_databasemysqlproviders.yaml +#- path: patches/webhook_in_relationaldatabaseproviders.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD #- path: patches/cainjection_in_databaserequests.yaml -#- path: patches/cainjection_in_databasemysqlproviders.yaml +#- path: patches/cainjection_in_relationaldatabaseproviders.yaml +#- path: patches/cainjection_in_mongodbproviders.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # [WEBHOOK] To enable webhook, uncomment the following section diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 731832a..d451ddf 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -16,3 +16,9 @@ resources: - auth_proxy_role.yaml - auth_proxy_role_binding.yaml - auth_proxy_client_clusterrole.yaml +# For each CRD, "Editor" and "Viewer" roles are scaffolded by +# default, aiding admins in cluster management. Those roles are +# not used by the Project itself. You can comment the following lines +# if you do not want those helpers be installed with your Project. +- mongodbprovider_editor_role.yaml +- mongodbprovider_viewer_role.yaml diff --git a/config/rbac/mongodbprovider_editor_role.yaml b/config/rbac/mongodbprovider_editor_role.yaml new file mode 100644 index 0000000..1f600fb --- /dev/null +++ b/config/rbac/mongodbprovider_editor_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to edit mongodbproviders. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: dbaas-controller + app.kubernetes.io/managed-by: kustomize + name: mongodbprovider-editor-role +rules: +- apiGroups: + - crd.lagoon.sh + resources: + - mongodbproviders + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - crd.lagoon.sh + resources: + - mongodbproviders/status + verbs: + - get diff --git a/config/rbac/mongodbprovider_viewer_role.yaml b/config/rbac/mongodbprovider_viewer_role.yaml new file mode 100644 index 0000000..977c2d7 --- /dev/null +++ b/config/rbac/mongodbprovider_viewer_role.yaml @@ -0,0 +1,23 @@ +# permissions for end users to view mongodbproviders. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: dbaas-controller + app.kubernetes.io/managed-by: kustomize + name: mongodbprovider-viewer-role +rules: +- apiGroups: + - crd.lagoon.sh + resources: + - mongodbproviders + verbs: + - get + - list + - watch +- apiGroups: + - crd.lagoon.sh + resources: + - mongodbproviders/status + verbs: + - get diff --git a/config/rbac/databasemysqlprovider_editor_role.yaml b/config/rbac/relationaldatabaseprovider_editor_role.yaml similarity index 65% rename from config/rbac/databasemysqlprovider_editor_role.yaml rename to config/rbac/relationaldatabaseprovider_editor_role.yaml index 9aea4e5..72fc902 100644 --- a/config/rbac/databasemysqlprovider_editor_role.yaml +++ b/config/rbac/relationaldatabaseprovider_editor_role.yaml @@ -1,20 +1,20 @@ -# permissions for end users to edit databasemysqlproviders. +# permissions for end users to edit relationaldatabaseproviders. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/name: clusterrole - app.kubernetes.io/instance: databasemysqlprovider-editor-role + app.kubernetes.io/instance: relationaldatabaseprovider-editor-role app.kubernetes.io/component: rbac app.kubernetes.io/created-by: dbaas-controller app.kubernetes.io/part-of: dbaas-controller app.kubernetes.io/managed-by: kustomize - name: databasemysqlprovider-editor-role + name: relationaldatabaseprovider-editor-role rules: - apiGroups: - crd.lagoon.sh resources: - - databasemysqlproviders + - relationaldatabaseproviders verbs: - create - delete @@ -26,6 +26,6 @@ rules: - apiGroups: - crd.lagoon.sh resources: - - databasemysqlproviders/status + - relationaldatabaseproviders/status verbs: - get diff --git a/config/rbac/databasemysqlprovider_viewer_role.yaml b/config/rbac/relationaldatabaseprovider_viewer_role.yaml similarity index 63% rename from config/rbac/databasemysqlprovider_viewer_role.yaml rename to config/rbac/relationaldatabaseprovider_viewer_role.yaml index 477005c..90a6428 100644 --- a/config/rbac/databasemysqlprovider_viewer_role.yaml +++ b/config/rbac/relationaldatabaseprovider_viewer_role.yaml @@ -1,20 +1,20 @@ -# permissions for end users to view databasemysqlproviders. +# permissions for end users to view relationaldatabaseproviders. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/name: clusterrole - app.kubernetes.io/instance: databasemysqlprovider-viewer-role + app.kubernetes.io/instance: relationaldatabaseprovider-viewer-role app.kubernetes.io/component: rbac app.kubernetes.io/created-by: dbaas-controller app.kubernetes.io/part-of: dbaas-controller app.kubernetes.io/managed-by: kustomize - name: databasemysqlprovider-viewer-role + name: relationaldatabaseprovider-viewer-role rules: - apiGroups: - crd.lagoon.sh resources: - - databasemysqlproviders + - relationaldatabaseproviders verbs: - get - list @@ -22,6 +22,6 @@ rules: - apiGroups: - crd.lagoon.sh resources: - - databasemysqlproviders/status + - relationaldatabaseproviders/status verbs: - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 4226aeb..8f0fe4c 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -38,7 +38,7 @@ rules: - apiGroups: - crd.lagoon.sh resources: - - databasemysqlproviders + - databaserequests verbs: - create - delete @@ -50,13 +50,13 @@ rules: - apiGroups: - crd.lagoon.sh resources: - - databasemysqlproviders/finalizers + - databaserequests/finalizers verbs: - update - apiGroups: - crd.lagoon.sh resources: - - databasemysqlproviders/status + - databaserequests/status verbs: - get - patch @@ -64,7 +64,7 @@ rules: - apiGroups: - crd.lagoon.sh resources: - - databaserequests + - mongodbproviders verbs: - create - delete @@ -76,13 +76,39 @@ rules: - apiGroups: - crd.lagoon.sh resources: - - databaserequests/finalizers + - mongodbproviders/finalizers verbs: - update - apiGroups: - crd.lagoon.sh resources: - - databaserequests/status + - mongodbproviders/status + verbs: + - get + - patch + - update +- apiGroups: + - crd.lagoon.sh + resources: + - relationaldatabaseproviders + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - crd.lagoon.sh + resources: + - relationaldatabaseproviders/finalizers + verbs: + - update +- apiGroups: + - crd.lagoon.sh + resources: + - relationaldatabaseproviders/status verbs: - get - patch diff --git a/config/samples/crd_v1alpha1_databaserequest_mysql.yaml b/config/samples/crd_v1alpha1_databaserequest_mysql.yaml new file mode 100644 index 0000000..320ca0c --- /dev/null +++ b/config/samples/crd_v1alpha1_databaserequest_mysql.yaml @@ -0,0 +1,14 @@ +apiVersion: crd.lagoon.sh/v1alpha1 +kind: DatabaseRequest +metadata: + labels: + app.kubernetes.io/name: databaserequest + app.kubernetes.io/instance: databaserequest-mysql-sample + app.kubernetes.io/part-of: dbaas-controller + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: dbaas-controller + name: databaserequest-mysql-sample +spec: + name: first-mysql-db + scope: development + type: mysql \ No newline at end of file diff --git a/config/samples/crd_v1alpha1_databaserequest_postgres.yaml b/config/samples/crd_v1alpha1_databaserequest_postgres.yaml new file mode 100644 index 0000000..a1c6c0b --- /dev/null +++ b/config/samples/crd_v1alpha1_databaserequest_postgres.yaml @@ -0,0 +1,14 @@ +apiVersion: crd.lagoon.sh/v1alpha1 +kind: DatabaseRequest +metadata: + labels: + app.kubernetes.io/name: databaserequest + app.kubernetes.io/instance: databaserequest-postgres-sample + app.kubernetes.io/part-of: dbaas-controller + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: dbaas-controller + name: databaserequest-postgres-sample +spec: + name: first-postgres-db + scope: development + type: postgres \ No newline at end of file diff --git a/config/samples/crd_v1alpha1_databaserequest.yaml b/config/samples/crd_v1alpha1_databaserequest_with_seed.yaml similarity index 82% rename from config/samples/crd_v1alpha1_databaserequest.yaml rename to config/samples/crd_v1alpha1_databaserequest_with_seed.yaml index 5ad53f6..dd16a53 100644 --- a/config/samples/crd_v1alpha1_databaserequest.yaml +++ b/config/samples/crd_v1alpha1_databaserequest_with_seed.yaml @@ -9,6 +9,9 @@ metadata: app.kubernetes.io/created-by: dbaas-controller name: databaserequest-sample spec: - name: first-mysql-db + name: seed-mysql-db + seed: + name: seed-mysql-secret + namespace: default scope: development type: mysql \ No newline at end of file diff --git a/config/samples/crd_v1alpha1_mongodbprovider.yaml b/config/samples/crd_v1alpha1_mongodbprovider.yaml new file mode 100644 index 0000000..80d44a2 --- /dev/null +++ b/config/samples/crd_v1alpha1_mongodbprovider.yaml @@ -0,0 +1,19 @@ +apiVersion: crd.lagoon.sh/v1alpha1 +kind: MongoDBProvider +metadata: + labels: + app.kubernetes.io/name: dbaas-controller + app.kubernetes.io/managed-by: kustomize + name: mongodbprovider-sample +spec: + scope: development + connections: + - name: primary-test-mongodb-connection + hostname: mongodb-service.mongodb + passwordSecretRef: + name: mongodb-secret + namespace: mongodb + port: 27017 + username: mongodb + auth: + mechanism: SCRAM-SHA-1 diff --git a/config/samples/crd_v1alpha1_databasemysqlprovider.yaml b/config/samples/crd_v1alpha1_relationaldatabaseprovider_mysql.yaml similarity index 50% rename from config/samples/crd_v1alpha1_databasemysqlprovider.yaml rename to config/samples/crd_v1alpha1_relationaldatabaseprovider_mysql.yaml index f697514..f27e361 100644 --- a/config/samples/crd_v1alpha1_databasemysqlprovider.yaml +++ b/config/samples/crd_v1alpha1_relationaldatabaseprovider_mysql.yaml @@ -1,16 +1,14 @@ apiVersion: crd.lagoon.sh/v1alpha1 -kind: DatabaseMySQLProvider +kind: RelationalDatabaseProvider metadata: labels: - app.kubernetes.io/name: databasemysqlprovider - app.kubernetes.io/instance: databasemysqlprovider-sample - app.kubernetes.io/part-of: dbaas-controller + app.kubernetes.io/name: dbaas-controller app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/created-by: dbaas-controller - name: databasemysqlprovider-sample + name: relationaldatabaseprovider-mysql-sample spec: + type: mysql scope: development - mysqlConnections: + connections: - name: primary-test-mysql-connection hostname: mysql-service.mysql passwordSecretRef: @@ -18,4 +16,4 @@ spec: namespace: mysql port: 3306 username: root - enabled: true \ No newline at end of file + enabled: true diff --git a/config/samples/crd_v1alpha1_relationaldatabaseprovider_postgres.yaml b/config/samples/crd_v1alpha1_relationaldatabaseprovider_postgres.yaml new file mode 100644 index 0000000..2e0a1de --- /dev/null +++ b/config/samples/crd_v1alpha1_relationaldatabaseprovider_postgres.yaml @@ -0,0 +1,19 @@ +apiVersion: crd.lagoon.sh/v1alpha1 +kind: RelationalDatabaseProvider +metadata: + labels: + app.kubernetes.io/name: dbaas-controller + app.kubernetes.io/managed-by: kustomize + name: relationaldatabaseprovider-postgres-sample +spec: + type: postgres + scope: development + connections: + - name: primary-test-postgres-connection + hostname: postgres-service.postgres + passwordSecretRef: + name: postgres-secret + namespace: postgres + port: 5432 + username: postgres + enabled: true diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index ed45112..a5485d3 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -1,5 +1,6 @@ ## Append samples of your project ## resources: - crd_v1alpha1_databaserequest.yaml -- crd_v1alpha1_databasemysqlprovider.yaml +- crd_v1alpha1_relationaldatabaseprovider.yaml +- crd_v1alpha1_mongodbprovider.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/go.mod b/go.mod index 5c1712b..46c0ae1 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,18 @@ require ( sigs.k8s.io/controller-runtime v0.17.2 ) -require filippo.io/edwards25519 v1.1.0 // indirect +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/golang/snappy v0.0.1 // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/sync v0.5.0 // indirect +) require ( github.com/beorn7/perks v1.0.1 // indirect @@ -39,6 +50,7 @@ require ( github.com/imdario/mergo v0.3.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/lib/pq v1.10.9 github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -48,6 +60,7 @@ require ( github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + go.mongodb.org/mongo-driver v1.15.0 go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect diff --git a/go.sum b/go.sum index ba91ab9..9241a23 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,8 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -64,6 +66,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kr/pretty v0.2.1/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= @@ -71,6 +75,8 @@ 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= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -78,6 +84,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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 h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= +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/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= @@ -110,8 +118,19 @@ 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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= +go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= 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.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -121,15 +140,21 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= @@ -137,17 +162,28 @@ golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.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.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= @@ -156,6 +192,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/controller/databasemysqlprovider_controller.go b/internal/controller/databasemysqlprovider_controller.go deleted file mode 100644 index 0d1d4ec..0000000 --- a/internal/controller/databasemysqlprovider_controller.go +++ /dev/null @@ -1,344 +0,0 @@ -/* -Copyright 2024. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controller - -import ( - "context" - "fmt" - - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/tools/record" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/metrics" - "sigs.k8s.io/controller-runtime/pkg/predicate" - - "github.com/prometheus/client_golang/prometheus" - crdv1alpha1 "github.com/uselagoon/dbaas-controller/api/v1alpha1" - "github.com/uselagoon/dbaas-controller/internal/database/mysql" -) - -const databaseMySQLProviderFinalizer = "databasemysqlprovider.crd.lagoon.sh/finalizer" - -var ( - // Prometheus metrics - // promDatabaseMySQLProviderReconcileCounter is the counter for the reconciled database mysql providers - promDatabaseMySQLProviderReconcileCounter = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "databasemysqlprovider_reconcile_total", - Help: "The total number of reconciled database mysql providers", - }, - []string{"name"}, - ) - - //promDatabaseMySQLProviderReconcileErrorCounter is the counter for the reconciled database mysql providers errors - promDatabaseMySQLProviderReconcileErrorCounter = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "databasemysqlprovider_reconcile_error_total", - Help: "The total number of reconciled database mysql providers errors", - }, - []string{"name", "scope", "error"}, - ) - - // promDatabaseMySQLProviderStatus is the gauge for the database mysql provider status - promDatabaseMySQLProviderStatus = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "databasemysqlprovider_status", - Help: "The status of the database mysql provider", - }, - []string{"name", "scope"}, - ) - - // promDatabaseMySQLProviderConnectionVersion is the gauge for the database mysql provider connection version - promDatabaseMySQLProviderConnectionVersion = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "databasemysqlprovider_connection_version", - Help: "The version of the database mysql provider connection", - }, - []string{"name", "scope", "hostname", "username", "version"}, - ) -) - -// DatabaseMySQLProviderReconciler reconciles a DatabaseMySQLProvider object -type DatabaseMySQLProviderReconciler struct { - client.Client - Scheme *runtime.Scheme - Recorder record.EventRecorder - MySQLClient mysql.MySQLInterface -} - -//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch -//+kubebuilder:rbac:groups=crd.lagoon.sh,resources=databasemysqlproviders,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=crd.lagoon.sh,resources=databasemysqlproviders/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=crd.lagoon.sh,resources=databasemysqlproviders/finalizers,verbs=update -//+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -func (r *DatabaseMySQLProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := log.FromContext(ctx).WithName("databasemysqlprovider_controller") - logger.Info("Reconciling DatabaseMySQLProvider") - promDatabaseMySQLProviderReconcileCounter.WithLabelValues(req.Name).Inc() - - // Fetch the DatabaseMySQLProvider instance - instance := &crdv1alpha1.DatabaseMySQLProvider{} - if err := r.Get(ctx, req.NamespacedName, instance); err != nil { - if client.IgnoreNotFound(err) == nil { - logger.Info("DatabaseMySQLProvider not found") - return ctrl.Result{}, nil - } - promDatabaseMySQLProviderReconcileErrorCounter.WithLabelValues(req.Name, "", "get-dbmysqlprovider").Inc() - return ctrl.Result{}, err - } - - if instance.DeletionTimestamp != nil && !instance.DeletionTimestamp.IsZero() { - // The object is being deleted - // To be discussed whether we need to delete all the database requests using this provider... - if controllerutil.RemoveFinalizer(instance, databaseMySQLProviderFinalizer) { - if err := r.Update(ctx, instance); err != nil { - return r.handleError(ctx, instance, "remove-finalizer", err) - } - } - return ctrl.Result{}, nil - } - - // Check if we need to reconcile based on Generation and ObservedGeneration but only if - // the status condition is not false. This makes sure that in case of an error the controller - // will try to reconcile again. - if instance.Status.Conditions != nil && meta.IsStatusConditionTrue(instance.Status.Conditions, "Ready") { - if instance.Status.ObservedGeneration >= instance.Generation { - logger.Info("No updates to reconcile") - r.Recorder.Event(instance, v1.EventTypeNormal, "ReconcileSkipped", "No updates to reconcile") - return ctrl.Result{}, nil - } - } - - if controllerutil.AddFinalizer(instance, databaseMySQLProviderFinalizer) { - if err := r.Update(ctx, instance); err != nil { - return r.handleError(ctx, instance, "add-finalizer", err) - } - } - - // Reconcile the DatabaseMySQLProvider and check the unique name of the MySQLConnections - uniqueNames := make(map[string]struct{}, len(instance.Spec.MySQLConnections)) - mySQLConns := make([]mySQLConn, 0, len(instance.Spec.MySQLConnections)) - for _, conn := range instance.Spec.MySQLConnections { - secret := &v1.Secret{} - if err := r.Get(ctx, types.NamespacedName{ - Name: conn.PasswordSecretRef.Name, - Namespace: conn.PasswordSecretRef.Namespace, - }, secret); err != nil { - return r.handleError(ctx, instance, "get-secret", err) - } - password := string(secret.Data["password"]) - if password == "" { - return r.handleError( - ctx, - instance, - "empty-password", - fmt.Errorf("mysql connection secret %s in namespace %s has empty password", secret.Name, secret.Namespace), - ) - } - mySQLConns = append(mySQLConns, mySQLConn{ - name: conn.Name, - hostname: conn.Hostname, - replicaHostnames: conn.ReplicaHostnames, - password: password, - port: conn.Port, - username: conn.Username, - enabled: conn.Enabled, - }) - uniqueNames[conn.Hostname] = struct{}{} - } - - if len(uniqueNames) != len(instance.Spec.MySQLConnections) { - return r.handleError( - ctx, - instance, - "unique-name", - fmt.Errorf("mysql connections must have unique names"), - ) - } - - mySQLStatus := make([]crdv1alpha1.MySQLConnectionStatus, 0, len(mySQLConns)) - errors := make([]error, 0, len(mySQLConns)) - foundEnabledMySQL := false - for _, conn := range mySQLConns { - // make a ping to the database to check if it's up and running and we can connect to it - // if not, we should return an error and set the status to 0 - // Note we could periodically check the status of the database and update the status accordingly... - if err := r.MySQLClient.Ping(ctx, conn.getDSN()); err != nil { - errors = append(errors, err) - mySQLStatus = append(mySQLStatus, crdv1alpha1.MySQLConnectionStatus{ - Name: conn.name, - Hostname: conn.hostname, - Status: "unavailable", - Enabled: conn.enabled, - }) - continue - } - version, err := r.MySQLClient.Version(ctx, conn.getDSN()) - if err != nil { - errors = append(errors, err) - mySQLStatus = append(mySQLStatus, crdv1alpha1.MySQLConnectionStatus{ - Name: conn.name, - Hostname: conn.hostname, - Status: "unavailable", - Enabled: conn.enabled, - }) - continue - } - - // check if the database is initialized - err = r.MySQLClient.Initialize(ctx, conn.getDSN()) - if err != nil { - errors = append(errors, err) - mySQLStatus = append(mySQLStatus, crdv1alpha1.MySQLConnectionStatus{ - Name: conn.name, - Hostname: conn.hostname, - Status: "unavailable", - Enabled: conn.enabled, - }) - continue - } - - promDatabaseMySQLProviderConnectionVersion.WithLabelValues( - req.Name, instance.Spec.Scope, conn.hostname, conn.username, version).Set(1) - mySQLStatus = append(mySQLStatus, crdv1alpha1.MySQLConnectionStatus{ - Name: conn.name, - Hostname: conn.hostname, - MySQLVersion: version, - Status: "available", - Enabled: conn.enabled, - }) - - if conn.enabled { - foundEnabledMySQL = true - } - } - - instance.Status.MySQLConnectionStatus = mySQLStatus - instance.Status.ObservedGeneration = instance.Generation - - if len(errors) == len(mySQLConns) { - return r.handleError( - ctx, - instance, - "mysql-connection", - fmt.Errorf("failed to connect to any of the MySQL databases: %v", errors), - ) - } - if !foundEnabledMySQL { - return r.handleError( - ctx, - instance, - "mysql-connection", - fmt.Errorf("no enabled working MySQL database found"), - ) - } - - // update the status condition to ready - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: "Ready", - Status: metav1.ConditionTrue, - Reason: "Reconciled", - Message: "DatabaseMySQLProvider reconciled", - }) - // update the status - if err := r.Status().Update(ctx, instance); err != nil { - promDatabaseMySQLProviderReconcileErrorCounter.WithLabelValues( - req.Name, instance.Spec.Scope, "update-status").Inc() - promDatabaseMySQLProviderStatus.WithLabelValues(req.Name, instance.Spec.Scope).Set(0) - return ctrl.Result{}, err - } - - r.Recorder.Event(instance, "Normal", "Reconciled", "DatabaseMySQLProvider reconciled") - promDatabaseMySQLProviderStatus.WithLabelValues(req.Name, instance.Spec.Scope).Set(1) - return ctrl.Result{}, nil -} - -// handleError handles the error and returns the result and the error -func (r *DatabaseMySQLProviderReconciler) handleError( - ctx context.Context, - instance *crdv1alpha1.DatabaseMySQLProvider, - promErr string, - err error, -) (ctrl.Result, error) { - promDatabaseMySQLProviderReconcileErrorCounter.WithLabelValues( - instance.Name, instance.Spec.Scope, promErr).Inc() - promDatabaseMySQLProviderStatus.WithLabelValues(instance.Name, instance.Spec.Scope).Set(0) - r.Recorder.Event(instance, v1.EventTypeWarning, errTypeToEventReason(promErr), err.Error()) - - // set the status condition to false - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: "Ready", - Status: metav1.ConditionFalse, - Reason: errTypeToEventReason(promErr), - Message: err.Error(), - }) - - // update the status - if err := r.Status().Update(ctx, instance); err != nil { - promDatabaseMySQLProviderReconcileErrorCounter.WithLabelValues( - instance.Name, instance.Spec.Scope, "update-status").Inc() - log.FromContext(ctx).Error(err, "Failed to update status") - } - - return ctrl.Result{}, err -} - -// mysqlConn is the connection to a MySQL database -type mySQLConn struct { - name string - hostname string - replicaHostnames []string - password string - port int - username string - enabled bool -} - -// getDSN constructs the DSN string for the MySQL connection. -func (mc *mySQLConn) getDSN() string { - return fmt.Sprintf("%s:%s@tcp(%s:%d)/", mc.username, mc.password, mc.hostname, mc.port) -} - -// SetupWithManager sets up the controller with the Manager. -func (r *DatabaseMySQLProviderReconciler) SetupWithManager(mgr ctrl.Manager) error { - // register metrics - metrics.Registry.MustRegister( - promDatabaseMySQLProviderReconcileCounter, - promDatabaseMySQLProviderReconcileErrorCounter, - promDatabaseMySQLProviderStatus, - promDatabaseMySQLProviderConnectionVersion, - ) - r.Recorder = mgr.GetEventRecorderFor("databasemysqlprovider_controller") - return ctrl.NewControllerManagedBy(mgr). - For(&crdv1alpha1.DatabaseMySQLProvider{}). - WithEventFilter(predicate.GenerationChangedPredicate{}). - // let's set the max concurrent reconciles to 1 as we don't want to run multiple reconciles at the same time - // although we could also change this and guard it by the name of the database provider - WithOptions(controller.Options{MaxConcurrentReconciles: 1}). - Complete(r) -} diff --git a/internal/controller/databaserequest_controller.go b/internal/controller/databaserequest_controller.go index 02c42c9..043794e 100644 --- a/internal/controller/databaserequest_controller.go +++ b/internal/controller/databaserequest_controller.go @@ -41,7 +41,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/prometheus/client_golang/prometheus" crdv1alpha1 "github.com/uselagoon/dbaas-controller/api/v1alpha1" - "github.com/uselagoon/dbaas-controller/internal/database/mysql" + "github.com/uselagoon/dbaas-controller/internal/database" ) const databaseRequestFinalizer = "databaserequest.crd.lagoon.sh/finalizer" @@ -82,10 +82,10 @@ var ( // DatabaseRequestReconciler reconciles a DatabaseRequest object type DatabaseRequestReconciler struct { client.Client - Scheme *runtime.Scheme - Recorder record.EventRecorder - MySQLClient mysql.MySQLInterface - Locks sync.Map + Scheme *runtime.Scheme + Recorder record.EventRecorder + RelationalDatabaseClient database.RelationalDatabaseInterface + Locks sync.Map } const ( @@ -173,19 +173,18 @@ func (r *DatabaseRequestReconciler) Reconcile(ctx context.Context, req ctrl.Requ // Implementing additional users would require to extend the logic here // check if the database request is already created and the secret and service exist var dbInfo dbInfo - switch databaseRequest.Spec.Type { - case mysqlType: - logger.Info("Get MySQL database information") + if databaseRequest.Spec.Type == mysqlType || databaseRequest.Spec.Type == postgresType { + logger.Info("Get relational database info") + // get the database info var err error - dbInfo, err = r.mysqlInfo(ctx, databaseRequest) + dbInfo, err = r.relDBInfo(ctx, databaseRequest) if err != nil { - return r.handleError(ctx, databaseRequest, "mysql-info", err) + return r.handleError( + ctx, databaseRequest, fmt.Sprintf("get-%s-database-info", databaseRequest.Spec.Type), err) } - case postgresType: - logger.Info("Get PostgreSQL database information") - case mongodbType: - logger.Info("Get MongoDB database information") - default: + } else if databaseRequest.Spec.Type == mongodbType { + logger.Info("Get mongodb database info") + } else { logger.Error(ErrInvalidDatabaseType, "Unsupported database type", "type", databaseRequest.Spec.Type) } @@ -367,22 +366,18 @@ func (r *DatabaseRequestReconciler) deleteDatabase( // handle deletion logic logger := log.FromContext(ctx) if databaseRequest.Spec.DropDatabaseOnDelete { - switch databaseRequest.Spec.Type { - case mysqlType: - // handle mysql deletion + if databaseRequest.Spec.Type == mysqlType || databaseRequest.Spec.Type == postgresType { + // handle relational database deletion // Note at the moment we only have one "primary" connection per database request // Implementing additional users would require to extend the logic here - logger.Info("Dropping MySQL database") - if err := r.mysqlDeletion(ctx, databaseRequest); err != nil { - return r.handleError(ctx, databaseRequest, "mysql-drop", err) + logger.Info("Dropping relational database") + if err := r.relDBDeletion(ctx, databaseRequest); err != nil { + return r.handleError(ctx, databaseRequest, fmt.Sprintf("%s-drop", databaseRequest.Spec.Type), err) } - case postgresType: - // handle postgres deletion - logger.Info("Dropping PostgreSQL database") - case mongodbType: + } else if databaseRequest.Spec.Type == mongodbType { // handle mongodb deletion logger.Info("Dropping MongoDB database") - default: + } else { // this should never happen, but just in case logger.Error(ErrInvalidDatabaseType, "Unsupported database type", "type", databaseRequest.Spec.Type) return r.handleError(ctx, databaseRequest, "invalid-database-type", ErrInvalidDatabaseType) @@ -421,28 +416,23 @@ func (r *DatabaseRequestReconciler) deleteDatabase( func (r *DatabaseRequestReconciler) createDatabase( ctx context.Context, databaseRequest *crdv1alpha1.DatabaseRequest) error { logger := log.FromContext(ctx) - switch databaseRequest.Spec.Type { - case mysqlType: - // handle mysql creation + if databaseRequest.Spec.Type == mysqlType || databaseRequest.Spec.Type == postgresType { + // handle relational database creation // Note at the moment we only have one "primary" connection per database request // Implementing additional users would require to extend the logic here - logger.Info("Creating MySQL database") - if err := r.mysqlOperation(ctx, create, databaseRequest, nil); err != nil { - return fmt.Errorf("mysql db creation failed: %w", err) + logger.Info("Creating relational database") + if err := r.relationalDatabaseOperation(ctx, create, databaseRequest, nil); err != nil { + return fmt.Errorf("%s db creation failed: %w", databaseRequest.Spec.Type, err) } if databaseRequest.Spec.DatabaseConnectionReference == nil { - return fmt.Errorf("mysql db creation failed due to missing database connection reference") + return fmt.Errorf("%s db creation failed due to missing database connection reference", databaseRequest.Spec.Type) } if databaseRequest.Status.DatabaseInfo == nil { - return fmt.Errorf("mysql db creation failed due to missing database info") + return fmt.Errorf("%s db creation failed due to missing database info", databaseRequest.Spec.Type) } - case postgresType: - // handle postgres creation - logger.Info("Creating PostgreSQL database") - case mongodbType: - // handle mongodb creation + } else if databaseRequest.Spec.Type == mongodbType { logger.Info("Creating MongoDB database") - default: + } else { // this should never happen, but just in case logger.Error(ErrInvalidDatabaseType, "Unsupported database type", "type", databaseRequest.Spec.Type) return fmt.Errorf("failed to create database: %w", ErrInvalidDatabaseType) @@ -517,51 +507,56 @@ func (m *dbInfo) getSecretData(name, serviceName string) map[string][]byte { } } -// mysqlOperation performs the MySQL operations create and drop -func (r *DatabaseRequestReconciler) mysqlOperation( +// relationalDatabaseOperation performs the relational database operations to create, drop and get database information +func (r *DatabaseRequestReconciler) relationalDatabaseOperation( ctx context.Context, operation string, databaseRequest *crdv1alpha1.DatabaseRequest, databaseInfo *dbInfo, ) error { - log.FromContext(ctx).Info("Performing MySQL operation", "operation", operation) + log.FromContext(ctx).Info("Performing relational database operation", "operation", operation) // get the database provider, for info and drop we use the reference which is already set on the database request // if not we error out. // For create we list all database providers and check if the scope matches and if // there are more than one provider with the same scope, we select the one with lower load. - databaseProvider := &crdv1alpha1.DatabaseMySQLProvider{} + databaseProvider := &crdv1alpha1.RelationalDatabaseProvider{} connectionName := "" if operation == create { var err error - databaseProvider, connectionName, err = r.findMySQLProvider(ctx, databaseRequest) + databaseProvider, connectionName, err = r.findRelationalDatabaseProvider(ctx, databaseRequest) if err != nil { - return fmt.Errorf("mysql db operation %s failed to find database provider: %w", operation, err) + return fmt.Errorf( + "%s db operation %s failed to find database provider: %w", databaseRequest.Spec.Type, operation, err) } - log.FromContext(ctx).Info("Found MySQL provider", "provider", databaseProvider.Name, "connection", connectionName) + log.FromContext(ctx).Info( + "Found relational database provider", "provider", databaseProvider.Name, "connection", connectionName) } else { if databaseRequest.Spec.DatabaseConnectionReference == nil { - return fmt.Errorf("mysql db operation %s failed due to missing database connection reference", operation) + return fmt.Errorf( + "%s db operation %s failed due to missing database connection reference", databaseRequest.Spec.Type, operation) } if err := r.Get(ctx, client.ObjectKey{ Name: databaseRequest.Spec.DatabaseConnectionReference.DatabaseObjectReference.Name, }, databaseProvider); err != nil { - return fmt.Errorf("mysql db operation %s failed to get database provider: %w", operation, err) + return fmt.Errorf( + "%s db operation %s failed to get database provider: %w", databaseRequest.Spec.Type, operation, err) } connectionName = databaseRequest.Spec.DatabaseConnectionReference.Name - log.FromContext(ctx).Info("Found MySQL provider", "provider", databaseProvider.Name, "connection", connectionName) + log.FromContext(ctx).Info( + "Found relational database provider", "provider", databaseProvider.Name, "connection", connectionName) } - var connection *crdv1alpha1.MySQLConnection - for _, c := range databaseProvider.Spec.MySQLConnections { - log.FromContext(ctx).Info("Checking MySQL provider database connection", "connection", c.Name) + var connection *crdv1alpha1.Connection + for _, c := range databaseProvider.Spec.Connections { + log.FromContext(ctx).Info("Checking relational database provider database connection", "connection", c.Name) if c.Name == connectionName { conn := c // Create a new variable and assign the value of c to it connection = &conn // Assign the address of the new variable to connection } } if connection == nil { - return fmt.Errorf("mysql db operation %s failed to find database connection", operation) + return fmt.Errorf("%s db operation %s failed to find database connection", databaseRequest.Spec.Type, operation) } secret := &v1.Secret{} @@ -569,15 +564,17 @@ func (r *DatabaseRequestReconciler) mysqlOperation( Name: connection.PasswordSecretRef.Name, Namespace: connection.PasswordSecretRef.Namespace, }, secret); err != nil { - return fmt.Errorf("mysql db operation %s failed to get connection password from secret: %w", operation, err) + return fmt.Errorf( + "%s db operation %s failed to get connection password from secret: %w", databaseRequest.Spec.Type, operation, err) } password := string(secret.Data["password"]) if password == "" { - return fmt.Errorf("mysql db operation %s failed due to empty password", operation) + return fmt.Errorf("%s db operation %s failed due to empty password", databaseRequest.Spec.Type, operation) } - conn := mySQLConn{ + conn := reldbConn{ + dbType: databaseRequest.Spec.Type, name: connection.Name, hostname: connection.Hostname, username: connection.Username, @@ -587,15 +584,16 @@ func (r *DatabaseRequestReconciler) mysqlOperation( switch operation { case create: - log.FromContext(ctx).Info("Creating MySQL database", "database", databaseRequest.Name) - info, err := r.MySQLClient.CreateDatabase( + log.FromContext(ctx).Info("Creating relational database", "database", databaseRequest.Name) + info, err := r.RelationalDatabaseClient.CreateDatabase( ctx, conn.getDSN(), databaseRequest.Name, databaseRequest.Namespace, + databaseRequest.Spec.Type, ) if err != nil { - return fmt.Errorf("mysql db operation %s failed: %w", operation, err) + return fmt.Errorf("%s db operation %s failed: %w", databaseRequest.Spec.Type, operation, err) } dbRef := &crdv1alpha1.DatabaseConnectionReference{ Name: connection.Name, @@ -612,39 +610,44 @@ func (r *DatabaseRequestReconciler) mysqlOperation( Databasename: info.Dbname, } if err := r.Status().Update(ctx, databaseRequest); err != nil { - return fmt.Errorf("mysql db operation %s failed to update database request: %w", operation, err) + return fmt.Errorf( + "%s db operation %s failed to update database request: %w", databaseRequest.Spec.Type, operation, err) } databaseRequest.Spec.DatabaseConnectionReference = dbRef return nil case drop: - if err := r.MySQLClient.DropDatabase( + if err := r.RelationalDatabaseClient.DropDatabase( ctx, conn.getDSN(), databaseRequest.Name, databaseRequest.Namespace, + databaseRequest.Spec.Type, ); err != nil { - return fmt.Errorf("mysql db opration %s failed: %w", operation, err) + return fmt.Errorf("%s db opration %s failed: %w", databaseRequest.Spec.Type, operation, err) } databaseRequest.Status.ObservedDatabaseConnectionReference = nil if err := r.Status().Update(ctx, databaseRequest); err != nil { - return fmt.Errorf("mysql db operation %s failed to update database request: %w", operation, err) + return fmt.Errorf( + "%s db operation %s failed to update database request: %w", databaseRequest.Spec.Type, operation, err) } databaseRequest.Spec.DatabaseConnectionReference = nil return nil case info: // check if the dbInfo is not nil if databaseInfo == nil { - return fmt.Errorf("mysql db operation %s failed due to missing dbInfo", operation) + return fmt.Errorf("%s db operation %s failed due to missing dbInfo", databaseRequest.Spec.Type, operation) } // get the database information - info, err := r.MySQLClient.GetDatabase( + info, err := r.RelationalDatabaseClient.GetDatabase( ctx, conn.getDSN(), databaseRequest.Name, databaseRequest.Namespace, + databaseRequest.Spec.Type, ) if err != nil { - return fmt.Errorf("mysql db operation %s failed to get database information: %w", operation, err) + return fmt.Errorf( + "%s db operation %s failed to get database information: %w", databaseRequest.Spec.Type, operation, err) } databaseInfo.userName = info.Username databaseInfo.password = info.Password @@ -653,30 +656,32 @@ func (r *DatabaseRequestReconciler) mysqlOperation( databaseInfo.port = conn.port return nil default: - return fmt.Errorf("mysql db operation %s failed due to invalid operation", operation) + return fmt.Errorf("%s db operation %s failed due to invalid operation", databaseRequest.Spec.Type, operation) } } -// findMySQLProvider finds the MySQL provider with the same scope and the lower load +// findRelationalDatabaseProvider finds the relational database provider with the same scope and the lower load // returns the provider, connection name and an error -func (r *DatabaseRequestReconciler) findMySQLProvider( +func (r *DatabaseRequestReconciler) findRelationalDatabaseProvider( ctx context.Context, databaseRequest *crdv1alpha1.DatabaseRequest, -) (*crdv1alpha1.DatabaseMySQLProvider, string, error) { - dbProviders := &crdv1alpha1.DatabaseMySQLProviderList{} +) (*crdv1alpha1.RelationalDatabaseProvider, string, error) { + dbProviders := &crdv1alpha1.RelationalDatabaseProviderList{} if err := r.List(ctx, dbProviders); err != nil { - return nil, "", fmt.Errorf("mysql db find provider failed to list database providers: %w", err) + return nil, "", fmt.Errorf("%s db find provider failed to list database providers: %w", + databaseRequest.Spec.Type, err, + ) } // find the provider with the same scope // set load to the max int value to find the provider with the lower load := int(^uint(0) >> 1) - var provider *crdv1alpha1.DatabaseMySQLProvider + var provider *crdv1alpha1.RelationalDatabaseProvider var connName string for _, dbProvider := range dbProviders.Items { - if dbProvider.Spec.Scope == databaseRequest.Spec.Scope { - log.FromContext(ctx).Info("Found MySQL provider", "provider", dbProvider.Name) - for _, dbConnection := range dbProvider.Spec.MySQLConnections { + if dbProvider.Spec.Scope == databaseRequest.Spec.Scope && dbProvider.Spec.Type == databaseRequest.Spec.Type { + log.FromContext(ctx).Info("Found provider", "provider", dbProvider.Name) + for _, dbConnection := range dbProvider.Spec.Connections { if dbConnection.Enabled { // fetch the password from the secret secret := &v1.Secret{} @@ -684,15 +689,18 @@ func (r *DatabaseRequestReconciler) findMySQLProvider( Name: dbConnection.PasswordSecretRef.Name, Namespace: dbConnection.PasswordSecretRef.Namespace, }, secret); err != nil { - return nil, "", fmt.Errorf("mysql db find provider failed to get connection password from secret: %w", err) + return nil, "", fmt.Errorf("%s db find provider failed to get connection password from secret: %w", + databaseRequest.Spec.Type, err, + ) } password := string(secret.Data["password"]) if password == "" { - return nil, "", errors.New("mysql db find provider failed due to empty password") + return nil, "", fmt.Errorf("%s db find provider failed due to empty password", databaseRequest.Spec.Type) } - conn := mySQLConn{ + conn := reldbConn{ + dbType: databaseRequest.Spec.Type, name: dbConnection.Name, hostname: dbConnection.Hostname, username: dbConnection.Username, @@ -702,17 +710,17 @@ func (r *DatabaseRequestReconciler) findMySQLProvider( // check the load of the provider connection // we select the provider with the lower load - log.FromContext(ctx).Info("Checking MySQL provider database connection", "connection", dbConnection.Name) - dbLoad, err := r.MySQLClient.Load(ctx, conn.getDSN()) + log.FromContext(ctx).Info("Checking provider database connection", "connection", dbConnection.Name) + dbLoad, err := r.RelationalDatabaseClient.Load(ctx, conn.getDSN(), databaseRequest.Spec.Type) if err != nil { - return nil, "", fmt.Errorf("mysql db find provider failed to get load: %w", err) + return nil, "", fmt.Errorf("%s db find provider failed to get load: %w", databaseRequest.Spec.Type, err) } if dbLoad < load { p := dbProvider provider = &p connName = dbConnection.Name load = dbLoad - log.FromContext(ctx).Info("Found MySQL provider", "provider", + log.FromContext(ctx).Info("Found relational database provider", "provider", dbProvider.Name, "connection", dbConnection.Name, "load", dbLoad) } } @@ -720,36 +728,36 @@ func (r *DatabaseRequestReconciler) findMySQLProvider( } } if provider == nil { - return nil, "", errors.New("mysql db find provider failed due to provider not found") + return nil, "", fmt.Errorf("%s db find provider failed due to provider not found", databaseRequest.Spec.Type) } return provider, connName, nil } -// mysqlDeletion deletes the MySQL database -func (r *DatabaseRequestReconciler) mysqlDeletion( +// relDBDeletion deletes the relational database +func (r *DatabaseRequestReconciler) relDBDeletion( ctx context.Context, databaseRequest *crdv1alpha1.DatabaseRequest, ) error { - log.FromContext(ctx).Info("Deleting MySQL database") + log.FromContext(ctx).Info("Deleting relational database") // check the status to find the object reference to the database provider if databaseRequest.Spec.DatabaseConnectionReference == nil { // if there is no reference, we can't delete the database. - return errors.New("mysql db drop failed due to connection reference is missing") + return errors.New("relational db drop failed due to connection reference is missing") } - return r.mysqlOperation(ctx, drop, databaseRequest, nil) + return r.relationalDatabaseOperation(ctx, drop, databaseRequest, nil) } -// mysqlInfo retrieves the MySQL database information -func (r *DatabaseRequestReconciler) mysqlInfo( +// relDBInfo retrieves the relational database information +func (r *DatabaseRequestReconciler) relDBInfo( ctx context.Context, databaseRequest *crdv1alpha1.DatabaseRequest, ) (dbInfo, error) { - log.FromContext(ctx).Info("Retrieving MySQL database information") + log.FromContext(ctx).Info("Retrieving relational database information") dbInfo := dbInfo{} - if err := r.mysqlOperation(ctx, info, databaseRequest, &dbInfo); err != nil { - return dbInfo, fmt.Errorf("mysql db info failed: %w", err) + if err := r.relationalDatabaseOperation(ctx, info, databaseRequest, &dbInfo); err != nil { + return dbInfo, fmt.Errorf("relational db info failed: %w", err) } return dbInfo, nil } diff --git a/internal/controller/databaserequest_controller_test.go b/internal/controller/databaserequest_controller_test.go index 0d64151..d858f74 100644 --- a/internal/controller/databaserequest_controller_test.go +++ b/internal/controller/databaserequest_controller_test.go @@ -32,7 +32,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" crdv1alpha1 "github.com/uselagoon/dbaas-controller/api/v1alpha1" - "github.com/uselagoon/dbaas-controller/internal/database/mysql" + "github.com/uselagoon/dbaas-controller/internal/database" ) var _ = Describe("DatabaseRequest Controller", func() { @@ -45,15 +45,15 @@ var _ = Describe("DatabaseRequest Controller", func() { ctx := context.Background() databaserequest := &crdv1alpha1.DatabaseRequest{} - databaseMysqlProvider := &crdv1alpha1.DatabaseMySQLProvider{} - databaseMysqlProviderSecret := &v1.Secret{} + relationalDatabaseProvider := &crdv1alpha1.RelationalDatabaseProvider{} + relationalDatabaseProviderSecret := &v1.Secret{} BeforeEach(func() { - By("creating the custom resource for the Kind DatabaseMySQLProvider") + By("creating the custom resource for the Kind RelationalDatabaseProvider") err := k8sClient.Get(ctx, types.NamespacedName{ Name: dbMySQLProviderSecretResource, Namespace: "default", - }, databaseMysqlProviderSecret) + }, relationalDatabaseProviderSecret) if err != nil && errors.IsNotFound(err) { secret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -68,15 +68,16 @@ var _ = Describe("DatabaseRequest Controller", func() { } err = k8sClient.Get(ctx, types.NamespacedName{ Name: dbMySQLProviderResource, - }, databaseMysqlProvider) + }, relationalDatabaseProvider) if err != nil && errors.IsNotFound(err) { - resource := &crdv1alpha1.DatabaseMySQLProvider{ + resource := &crdv1alpha1.RelationalDatabaseProvider{ ObjectMeta: metav1.ObjectMeta{ Name: dbMySQLProviderResource, }, - Spec: crdv1alpha1.DatabaseMySQLProviderSpec{ + Spec: crdv1alpha1.RelationalDatabaseProviderSpec{ + Type: "mysql", Scope: "development", - MySQLConnections: []crdv1alpha1.MySQLConnection{ + Connections: []crdv1alpha1.Connection{ { Name: "test-connection", Hostname: "test-hostname", @@ -117,22 +118,22 @@ var _ = Describe("DatabaseRequest Controller", func() { }) AfterEach(func() { - By("Cleanup the specific resource instance DatabaseMySQLProvider") - databaseMysqlProvider = &crdv1alpha1.DatabaseMySQLProvider{} + By("Cleanup the specific resource instance RelationalDatabaseProvider") + relationalDatabaseProvider = &crdv1alpha1.RelationalDatabaseProvider{} err := k8sClient.Get(ctx, types.NamespacedName{ Name: dbMySQLProviderResource, - }, databaseMysqlProvider) + }, relationalDatabaseProvider) Expect(err).NotTo(HaveOccurred()) - Expect(k8sClient.Delete(ctx, databaseMysqlProvider)).To(Succeed()) + Expect(k8sClient.Delete(ctx, relationalDatabaseProvider)).To(Succeed()) By("Cleanup the specific resource instance Secret") - databaseMysqlProviderSecret = &v1.Secret{} + relationalDatabaseProviderSecret = &v1.Secret{} err = k8sClient.Get(ctx, types.NamespacedName{ Name: dbMySQLProviderSecretResource, Namespace: "default", - }, databaseMysqlProviderSecret) + }, relationalDatabaseProviderSecret) Expect(err).NotTo(HaveOccurred()) - Expect(k8sClient.Delete(ctx, databaseMysqlProviderSecret)).To(Succeed()) + Expect(k8sClient.Delete(ctx, relationalDatabaseProviderSecret)).To(Succeed()) By("Cleanup the specific resource instance DatabaseRequest") databaserequest := &crdv1alpha1.DatabaseRequest{} @@ -148,10 +149,10 @@ var _ = Describe("DatabaseRequest Controller", func() { By("Reconciling the created resource") fakeRecoder := record.NewFakeRecorder(100) controllerReconciler := &DatabaseRequestReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - Recorder: fakeRecoder, - MySQLClient: &mysql.MySQLMock{}, + Client: k8sClient, + Scheme: k8sClient.Scheme(), + Recorder: fakeRecoder, + RelationalDatabaseClient: &database.RelationalDatabaseMock{}, } _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ diff --git a/internal/controller/mongodbprovider_controller.go b/internal/controller/mongodbprovider_controller.go new file mode 100644 index 0000000..28a2d9d --- /dev/null +++ b/internal/controller/mongodbprovider_controller.go @@ -0,0 +1,320 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/metrics" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + "github.com/prometheus/client_golang/prometheus" + crdv1alpha1 "github.com/uselagoon/dbaas-controller/api/v1alpha1" + "github.com/uselagoon/dbaas-controller/internal/database/mongodb" +) + +const mongoDBProviderFinalizer = "mongodbprovider.crd.lagoon.sh/finalizer" + +var ( + // Prometheus metrics + // promMongoDBProviderReconcileErrorCounter counter for the reconciled mongodb providers errors + promMongoDBProviderReconcileErrorCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "mongodbprovider_reconcile_error_total", + Help: "The total number of reconciled mongodb providers errors", + }, + []string{"name", "scope", "error"}, + ) + + // promMongoDBProviderStatus is the gauge for the mongodb provider status + promMongoDBProviderStatus = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "mongodbprovider_status", + Help: "The status of the mongodb provider", + }, + []string{"name", "scope"}, + ) + + // promMongoDBProviderConnectionVersion is the gauge for the mongodb provider connection version + promMongoDBProviderConnectionVersion = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "mongodbprovider_connection_version", + Help: "The version of the mongodb provider connection", + }, + []string{"name", "scope", "hostname", "username", "version"}, + ) +) + +// MongoDBProviderReconciler reconciles a MongoDBProvider object +type MongoDBProviderReconciler struct { + client.Client + Scheme *runtime.Scheme + Recorder record.EventRecorder + MongoDBClient mongodb.MongoDBInterface +} + +// nolint:lll +//+kubebuilder:rbac:groups=crd.lagoon.sh,resources=mongodbproviders,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=crd.lagoon.sh,resources=mongodbproviders/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=crd.lagoon.sh,resources=mongodbproviders/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the MongoDBProvider object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.3/pkg/reconcile +func (r *MongoDBProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx).WithName("mongodbprovider_controller") + logger.Info("Reconciling MongoDBProvider") + + instance := &crdv1alpha1.MongoDBDProvider{} + if err := r.Get(ctx, req.NamespacedName, instance); err != nil { + if client.IgnoreNotFound(err) == nil { + logger.Info("MongoDBProvider resource not found. Ignoring since object must be deleted") + return ctrl.Result{}, nil + } + promMongoDBProviderReconcileErrorCounter.WithLabelValues(req.Name, "", "get-mongodbprovider").Inc() + return ctrl.Result{}, err + } + logger = logger.WithValues("scope", instance.Spec.Scope) + log.IntoContext(ctx, logger) + if instance.DeletionTimestamp != nil && !instance.DeletionTimestamp.IsZero() { + // The object is being deleted + // To be discussed whether we should delete the database requests using this provider... + if controllerutil.RemoveFinalizer(instance, mongoDBProviderFinalizer) { + if err := r.Update(ctx, instance); err != nil { + return r.handleError(ctx, instance, "remove-finalizer", err) + } + } + return ctrl.Result{}, nil + } + + // Check if we need to reconcile based on Generation and ObservedGeneration but only if + // the status condition is not false. This makes sure that in case of an error the controller + // will try to reconcile again. + if instance.Status.Conditions != nil && meta.IsStatusConditionTrue(instance.Status.Conditions, "Ready") { + if instance.Status.ObservedGeneration >= instance.Generation { + logger.Info("Skipping reconcile: generation has not changed") + r.Recorder.Event(instance, v1.EventTypeNormal, "ReconcileSkipped", "No updates to reconcile") + return ctrl.Result{}, nil + } + } + + // Add finalizer if not already added + if !controllerutil.ContainsFinalizer(instance, mongoDBProviderFinalizer) { + controllerutil.AddFinalizer(instance, mongoDBProviderFinalizer) + if err := r.Update(ctx, instance); err != nil { + return r.handleError(ctx, instance, "add-finalizer", err) + } + } + + // Check the unique names of connections + uniqueNames := make(map[string]struct{}, len(instance.Spec.Connections)) + conns := make([]mongoDBConn, 0, len(instance.Spec.Connections)) + for _, conn := range instance.Spec.Connections { + uniqueNames[conn.Name] = struct{}{} + secret := &v1.Secret{} + if err := r.Get(ctx, client.ObjectKey{ + Name: conn.PasswordSecretRef.Name, Namespace: conn.PasswordSecretRef.Namespace, + }, secret); err != nil { + return r.handleError(ctx, instance, "get-secret", err) + } + password := string(secret.Data["password"]) + if password == "" { + return r.handleError(ctx, instance, "empty-password", fmt.Errorf( + "password is empty for secret %s/%s", conn.PasswordSecretRef.Namespace, conn.PasswordSecretRef.Name, + )) + } + conns = append(conns, mongoDBConn{ + options: mongodb.MongoDBClientOptions{ + Name: conn.Name, + Hostname: conn.Hostname, + Port: conn.Port, + Username: conn.Username, + Password: password, + Mechanism: conn.Auth.Mechanism, + Source: conn.Auth.Source, + TLS: conn.Auth.TLS, + }, + enabled: conn.Enabled, + }) + } + if len(uniqueNames) != len(instance.Spec.Connections) { + return r.handleError(ctx, instance, "dunique-name-error", fmt.Errorf( + "connection names are not unique for MongoDBProvider %s", instance.Name, + )) + } + + dbStatus := make([]crdv1alpha1.ConnectionStatus, 0, len(conns)) + errors := make([]error, 0, len(conns)) + foundEnabledDatabase := false + for _, conn := range conns { + // make a ping to the database to check if it's up and running and we can connect to it + // if not, we should return an error and set the status to 0 + // Note we could periodically check the status of the database and update the status accordingly... + if err := r.MongoDBClient.Ping(ctx, conn.options); err != nil { + errors = append(errors, err) + dbStatus = append(dbStatus, crdv1alpha1.ConnectionStatus{ + Name: conn.options.Name, + Hostname: conn.options.Hostname, + DatabaseVersion: "unknown", + Enabled: conn.enabled, + Status: "unavailable", + }) + promMongoDBProviderConnectionVersion.WithLabelValues( + conn.options.Name, instance.Spec.Scope, conn.options.Hostname, conn.options.Username, "", + ).Set(0) + logger.Error(err, "Failed to ping MongoDB", "hostname", conn.options.Hostname) + continue + } + version, err := r.MongoDBClient.Version(ctx, conn.options) + if err != nil { + errors = append(errors, err) + dbStatus = append(dbStatus, crdv1alpha1.ConnectionStatus{ + Name: conn.options.Name, + Hostname: conn.options.Hostname, + DatabaseVersion: "unknown", + Enabled: conn.enabled, + Status: "unavailable", + }) + promMongoDBProviderConnectionVersion.WithLabelValues( + conn.options.Name, instance.Spec.Scope, conn.options.Hostname, conn.options.Username, version, + ).Set(0) + logger.Error(err, "Failed to get MongoDB version", "hostname", conn.options.Hostname) + continue + } + + dbStatus = append(dbStatus, crdv1alpha1.ConnectionStatus{ + Name: conn.options.Name, + Hostname: conn.options.Hostname, + DatabaseVersion: version, + Status: "available", + Enabled: conn.enabled, + }) + if conn.enabled { + foundEnabledDatabase = true + promMongoDBProviderConnectionVersion.WithLabelValues( + conn.options.Name, instance.Spec.Scope, conn.options.Hostname, conn.options.Username, version, + ).Set(1) + } else { + promMongoDBProviderConnectionVersion.WithLabelValues( + conn.options.Name, instance.Spec.Scope, conn.options.Hostname, conn.options.Username, version, + ).Set(0) + } + } + + instance.Status.ConnectionStatus = dbStatus + instance.Status.ObservedGeneration = instance.Generation + + if len(errors) == len(conns) { + return r.handleError(ctx, instance, "all-connections-error", fmt.Errorf( + "all connections failed for MongoDBProvider %s", instance.Name, + )) + } + + if !foundEnabledDatabase { + return r.handleError(ctx, instance, "no-enabled-database", fmt.Errorf( + "no enabled database found for MongoDBProvider %s", instance.Name, + )) + } + + // update the status condition to ready + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: "Ready", + Status: metav1.ConditionTrue, + Reason: "Reconciled", + Message: "MongoDBProvider reconciled", + }) + // update the status + if err := r.Status().Update(ctx, instance); err != nil { + promMongoDBProviderReconcileErrorCounter.WithLabelValues( + req.Name, instance.Spec.Scope, "update-status").Inc() + promMongoDBProviderStatus.WithLabelValues(req.Name, instance.Spec.Scope).Set(0) + return ctrl.Result{}, err + } + + r.Recorder.Event(instance, v1.EventTypeNormal, "Reconciled", "MongoDBProvider reconciled") + promMongoDBProviderStatus.WithLabelValues(req.Name, instance.Spec.Scope).Set(1) + return ctrl.Result{}, nil +} + +func (r *MongoDBProviderReconciler) handleError( + ctx context.Context, + instance *crdv1alpha1.MongoDBDProvider, + promErr string, + err error, +) (ctrl.Result, error) { + promMongoDBProviderReconcileErrorCounter.WithLabelValues(instance.Name, instance.Spec.Scope, promErr).Inc() + promMongoDBProviderStatus.WithLabelValues(instance.Name, instance.Spec.Scope).Set(0) + r.Recorder.Event(instance, v1.EventTypeWarning, errTypeToEventReason(promErr), err.Error()) + + // set the status condition + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: "Ready", + Status: metav1.ConditionFalse, + Reason: errTypeToEventReason(promErr), + Message: err.Error(), + }) + + // update the status + if err := r.Status().Update(ctx, instance); err != nil { + promMongoDBProviderReconcileErrorCounter.WithLabelValues(instance.Name, instance.Spec.Scope, "update-status").Inc() + log.FromContext(ctx).Error(err, "Failed to update status") + return ctrl.Result{}, err + } + return ctrl.Result{}, err +} + +// mongoDBConn is a struct to hold the connection details to a MongoDB database +type mongoDBConn struct { + options mongodb.MongoDBClientOptions + enabled bool +} + +// SetupWithManager sets up the controller with the Manager. +func (r *MongoDBProviderReconciler) SetupWithManager(mgr ctrl.Manager) error { + // register metrics + metrics.Registry.MustRegister( + promMongoDBProviderReconcileErrorCounter, + promMongoDBProviderStatus, + promMongoDBProviderConnectionVersion, + ) + r.Recorder = mgr.GetEventRecorderFor("mongodbprovider-controller") + return ctrl.NewControllerManagedBy(mgr). + For(&crdv1alpha1.MongoDBDProvider{}). + WithEventFilter(predicate.GenerationChangedPredicate{}). + // Only allow one reconcile at a time + WithOptions(controller.Options{ + MaxConcurrentReconciles: 1, + }). + Complete(r) +} diff --git a/internal/controller/mongodbprovider_controller_test.go b/internal/controller/mongodbprovider_controller_test.go new file mode 100644 index 0000000..8c44fb1 --- /dev/null +++ b/internal/controller/mongodbprovider_controller_test.go @@ -0,0 +1,139 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + crdv1alpha1 "github.com/uselagoon/dbaas-controller/api/v1alpha1" + "github.com/uselagoon/dbaas-controller/internal/database/mongodb" +) + +var _ = Describe("MongoDBProvider Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + mongodbprovider := &crdv1alpha1.MongoDBDProvider{} + + BeforeEach(func() { + By("creating the custom resource for the Kind MongoDBProvider") + secret := &v1.Secret{} + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: "test-mongo-db-provider-secret", + Namespace: "default", + }, secret) + if err != nil && errors.IsNotFound(err) { + secret = &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-mongo-db-provider-secret", + Namespace: "default", + }, + StringData: map[string]string{ + "password": "test-password", + }, + } + err = k8sClient.Create(ctx, secret) + Expect(err).NotTo(HaveOccurred()) + } + + err = k8sClient.Get(ctx, typeNamespacedName, mongodbprovider) + if err != nil && errors.IsNotFound(err) { + resource := &crdv1alpha1.MongoDBDProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: crdv1alpha1.MongoDBProviderSpec{ + Scope: "development", + Connections: []crdv1alpha1.MongoDBConnection{ + { + Name: "test-mongodb-connection", + Auth: crdv1alpha1.MongoDBAuth{ + Mechanism: "SCRAM-SHA-256", + }, + PasswordSecretRef: v1.SecretReference{ + Name: secret.Name, + Namespace: secret.Namespace, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &crdv1alpha1.MongoDBDProvider{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance MongoDBProvider") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + fakeRecorder := record.NewFakeRecorder(100) + controllerReconciler := &MongoDBProviderReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + Recorder: fakeRecorder, + MongoDBClient: &mongodb.MongoDBMock{}, + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + Expect(err).NotTo(HaveOccurred()) + // fetch the resource to check if the status has been updated + err = k8sClient.Get(ctx, typeNamespacedName, mongodbprovider) + Expect(err).NotTo(HaveOccurred()) + // check the port of the connection is having the default value + Expect(mongodbprovider.Spec.Connections[0].Port).To(Equal(27017)) + Expect(mongodbprovider.Status.ObservedGeneration).To(Equal(mongodbprovider.Generation)) + + // check status of the resource + Expect(mongodbprovider.Status.ConnectionStatus).To(HaveLen(1)) + Expect(mongodbprovider.Status.ConnectionStatus[0].Name).To(Equal("test-mongodb-connection")) + + // check the conditions + err = k8sClient.Get(ctx, typeNamespacedName, mongodbprovider) + Expect(err).NotTo(HaveOccurred()) + Expect(mongodbprovider.Status.Conditions).To(HaveLen(1)) + Expect(mongodbprovider.Status.Conditions[0].Type).To(Equal("Ready")) + + }) + }) +}) diff --git a/internal/controller/relationaldatabaseprovider_controller.go b/internal/controller/relationaldatabaseprovider_controller.go new file mode 100644 index 0000000..c3034a3 --- /dev/null +++ b/internal/controller/relationaldatabaseprovider_controller.go @@ -0,0 +1,359 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/metrics" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + "github.com/prometheus/client_golang/prometheus" + crdv1alpha1 "github.com/uselagoon/dbaas-controller/api/v1alpha1" + "github.com/uselagoon/dbaas-controller/internal/database" +) + +const databaseProviderFinalizer = "relationaldatabaseprovider.crd.lagoon.sh/finalizer" + +var ( + // Prometheus metrics + // promRelationalDatabaseProviderReconcileErrorCounter counter for the reconciled relational database providers errors + promRelationalDatabaseProviderReconcileErrorCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "relationaldatabaseprovider_reconcile_error_total", + Help: "The total number of reconciled relational database providers errors", + }, + []string{"type", "name", "scope", "error"}, + ) + + // promRelationalDatabaseProviderStatus is the gauge for the relational database provider status + promRelationalDatabaseProviderStatus = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "relationaldatabaseprovider_status", + Help: "The status of the relational database provider", + }, + []string{"type", "name", "scope"}, + ) + + // promRelationalDatabaseProviderConnectionVersion is the gauge for the relational database provider connection version + promRelationalDatabaseProviderConnectionVersion = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "relationaldatabaseprovider_connection_version", + Help: "The version of the relational database provider connection", + }, + []string{"type", "name", "scope", "hostname", "username", "version"}, + ) +) + +// RelationalDatabaseProviderReconciler reconciles a RelationalDatabaseProvider object +type RelationalDatabaseProviderReconciler struct { + client.Client + Scheme *runtime.Scheme + Recorder record.EventRecorder + RelDBClient database.RelationalDatabaseInterface +} + +//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch +// nolint:lll +//+kubebuilder:rbac:groups=crd.lagoon.sh,resources=relationaldatabaseproviders,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=crd.lagoon.sh,resources=relationaldatabaseproviders/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=crd.lagoon.sh,resources=relationaldatabaseproviders/finalizers,verbs=update +//+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +func (r *RelationalDatabaseProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx).WithName("relationaldatabaseprovider_controller") + logger.Info("Reconciling RelationalDatabaseProvider") + + // Fetch the RelationalDatabaseProvider instance + instance := &crdv1alpha1.RelationalDatabaseProvider{} + if err := r.Get(ctx, req.NamespacedName, instance); err != nil { + if client.IgnoreNotFound(err) == nil { + logger.Info("RelationalDatabaseProvider not found") + return ctrl.Result{}, nil + } + promRelationalDatabaseProviderReconcileErrorCounter.WithLabelValues( + "", req.Name, "", "get-relationaldbprovider").Inc() + return ctrl.Result{}, err + } + logger = logger.WithValues("type", instance.Spec.Type, "scope", instance.Spec.Scope) + if instance.DeletionTimestamp != nil && !instance.DeletionTimestamp.IsZero() { + // The object is being deleted + // To be discussed whether we need to delete all the database requests using this provider... + if controllerutil.RemoveFinalizer(instance, databaseProviderFinalizer) { + if err := r.Update(ctx, instance); err != nil { + return r.handleError(ctx, instance, "remove-finalizer", err) + } + } + return ctrl.Result{}, nil + } + + // Check if we need to reconcile based on Generation and ObservedGeneration but only if + // the status condition is not false. This makes sure that in case of an error the controller + // will try to reconcile again. + if instance.Status.Conditions != nil && meta.IsStatusConditionTrue(instance.Status.Conditions, "Ready") { + if instance.Status.ObservedGeneration >= instance.Generation { + logger.Info("No updates to reconcile") + r.Recorder.Event(instance, v1.EventTypeNormal, "ReconcileSkipped", "No updates to reconcile") + return ctrl.Result{}, nil + } + } + + if controllerutil.AddFinalizer(instance, databaseProviderFinalizer) { + if err := r.Update(ctx, instance); err != nil { + return r.handleError(ctx, instance, "add-finalizer", err) + } + } + + // Reconcile the RelationalDatabaseProvider and check the unique name of the Connections + uniqueNames := make(map[string]struct{}, len(instance.Spec.Connections)) + conns := make([]reldbConn, 0, len(instance.Spec.Connections)) + for _, conn := range instance.Spec.Connections { + secret := &v1.Secret{} + if err := r.Get(ctx, types.NamespacedName{ + Name: conn.PasswordSecretRef.Name, + Namespace: conn.PasswordSecretRef.Namespace, + }, secret); err != nil { + return r.handleError(ctx, instance, "get-secret", err) + } + password := string(secret.Data["password"]) + if password == "" { + return r.handleError( + ctx, + instance, + fmt.Sprintf("%s-empty-password", instance.Spec.Type), + fmt.Errorf( + "%s connection secret %s in namespace %s has empty password", + instance.Spec.Type, secret.Name, secret.Namespace, + ), + ) + } + conns = append(conns, reldbConn{ + dbType: instance.Spec.Type, + name: conn.Name, + hostname: conn.Hostname, + replicaHostnames: conn.ReplicaHostnames, + password: password, + port: conn.Port, + username: conn.Username, + enabled: conn.Enabled, + }) + uniqueNames[conn.Name] = struct{}{} + } + + if len(uniqueNames) != len(instance.Spec.Connections) { + return r.handleError( + ctx, + instance, + fmt.Sprintf("%s-unique-name-error", instance.Spec.Type), + fmt.Errorf("%s database connections must have unique names", instance.Spec.Type), + ) + } + + dbStatus := make([]crdv1alpha1.ConnectionStatus, 0, len(conns)) + errors := make([]error, 0, len(conns)) + foundEnabledDatabase := false + for _, conn := range conns { + // make a ping to the database to check if it's up and running and we can connect to it + // if not, we should return an error and set the status to 0 + // Note we could periodically check the status of the database and update the status accordingly... + if err := r.RelDBClient.Ping(ctx, conn.getDSN(), instance.Spec.Type); err != nil { + errors = append(errors, err) + dbStatus = append(dbStatus, crdv1alpha1.ConnectionStatus{ + Name: conn.name, + Hostname: conn.hostname, + Status: "unavailable", + Enabled: conn.enabled, + }) + promRelationalDatabaseProviderConnectionVersion.WithLabelValues( + instance.Spec.Type, req.Name, instance.Spec.Scope, conn.hostname, conn.username, "").Set(0) + logger.Error(err, "Failed to ping the database", "hostname", conn.hostname) + continue + } + version, err := r.RelDBClient.Version(ctx, conn.getDSN(), instance.Spec.Type) + if err != nil { + errors = append(errors, err) + dbStatus = append(dbStatus, crdv1alpha1.ConnectionStatus{ + Name: conn.name, + Hostname: conn.hostname, + Status: "unavailable", + Enabled: conn.enabled, + }) + logger.Error(err, "Failed to get the database version", "hostname", conn.hostname) + promRelationalDatabaseProviderConnectionVersion.WithLabelValues( + instance.Spec.Type, req.Name, instance.Spec.Scope, conn.hostname, conn.username, version).Set(0) + continue + } + + // check if the database is initialized + err = r.RelDBClient.Initialize(ctx, conn.getDSN(), instance.Spec.Type) + if err != nil { + errors = append(errors, err) + dbStatus = append(dbStatus, crdv1alpha1.ConnectionStatus{ + Name: conn.name, + Hostname: conn.hostname, + Status: "unavailable", + Enabled: conn.enabled, + }) + promRelationalDatabaseProviderConnectionVersion.WithLabelValues( + instance.Spec.Type, req.Name, instance.Spec.Scope, conn.hostname, conn.username, version).Set(0) + continue + } + + dbStatus = append(dbStatus, crdv1alpha1.ConnectionStatus{ + Name: conn.name, + Hostname: conn.hostname, + DatabaseVersion: version, + Status: "available", + Enabled: conn.enabled, + }) + if conn.enabled { + foundEnabledDatabase = true + promRelationalDatabaseProviderConnectionVersion.WithLabelValues( + instance.Spec.Type, req.Name, instance.Spec.Scope, conn.hostname, conn.username, version).Set(1) + } else { + promRelationalDatabaseProviderConnectionVersion.WithLabelValues( + instance.Spec.Type, req.Name, instance.Spec.Scope, conn.hostname, conn.username, version).Set(0) + } + } + + instance.Status.ConnectionStatus = dbStatus + instance.Status.ObservedGeneration = instance.Generation + + if len(errors) == len(conns) { + return r.handleError( + ctx, + instance, + fmt.Sprintf("%s-connection-error", instance.Spec.Type), + fmt.Errorf("failed to connect to any of the %s databases: %v", instance.Spec.Type, errors), + ) + } + if !foundEnabledDatabase { + return r.handleError( + ctx, + instance, + fmt.Sprintf("%s-connection-not-any-enabled", instance.Spec.Type), + fmt.Errorf("no enabled working %s database found", instance.Spec.Type), + ) + } + + // update the status condition to ready + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: "Ready", + Status: metav1.ConditionTrue, + Reason: "Reconciled", + Message: "RelationalDatabaseProvider reconciled", + }) + // update the status + if err := r.Status().Update(ctx, instance); err != nil { + promRelationalDatabaseProviderReconcileErrorCounter.WithLabelValues( + instance.Spec.Type, req.Name, instance.Spec.Scope, "update-status").Inc() + promRelationalDatabaseProviderStatus.WithLabelValues(instance.Spec.Type, req.Name, instance.Spec.Scope).Set(0) + return ctrl.Result{}, err + } + + r.Recorder.Event(instance, "Normal", "Reconciled", "RelationalDatabaseProvider reconciled") + promRelationalDatabaseProviderStatus.WithLabelValues(instance.Spec.Type, req.Name, instance.Spec.Scope).Set(1) + return ctrl.Result{}, nil +} + +// handleError handles the error and returns the result and the error +func (r *RelationalDatabaseProviderReconciler) handleError( + ctx context.Context, + instance *crdv1alpha1.RelationalDatabaseProvider, + promErr string, + err error, +) (ctrl.Result, error) { + promRelationalDatabaseProviderReconcileErrorCounter.WithLabelValues( + instance.Spec.Type, instance.Name, instance.Spec.Scope, promErr).Inc() + promRelationalDatabaseProviderStatus.WithLabelValues(instance.Spec.Type, instance.Name, instance.Spec.Scope).Set(0) + r.Recorder.Event(instance, v1.EventTypeWarning, errTypeToEventReason(promErr), err.Error()) + + // set the status condition to false + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: "Ready", + Status: metav1.ConditionFalse, + Reason: errTypeToEventReason(promErr), + Message: err.Error(), + }) + + // update the status + if err := r.Status().Update(ctx, instance); err != nil { + promRelationalDatabaseProviderReconcileErrorCounter.WithLabelValues( + instance.Spec.Type, instance.Name, instance.Spec.Scope, "update-status").Inc() + log.FromContext(ctx).Error(err, "Failed to update status") + } + + return ctrl.Result{}, err +} + +// reldbConn is the connection to a MySQL or PostgreSQL database +type reldbConn struct { + dbType string + name string + hostname string + replicaHostnames []string + password string + port int + username string + enabled bool +} + +// getDSN constructs the DSN string for the MySQL or PostgreSQL connection. +func (rc *reldbConn) getDSN() string { + if rc.dbType == "mysql" { + return fmt.Sprintf("%s:%s@tcp(%s:%d)/", rc.username, rc.password, rc.hostname, rc.port) + } else if rc.dbType == "postgres" { + return fmt.Sprintf( + "host=%s port=%d user=%s password=%s sslmode=disable", + rc.hostname, rc.port, rc.username, rc.password, + ) + } else { + return "" + } +} + +// SetupWithManager sets up the controller with the Manager. +func (r *RelationalDatabaseProviderReconciler) SetupWithManager(mgr ctrl.Manager) error { + // register metrics + metrics.Registry.MustRegister( + promRelationalDatabaseProviderReconcileErrorCounter, + promRelationalDatabaseProviderStatus, + promRelationalDatabaseProviderConnectionVersion, + ) + r.Recorder = mgr.GetEventRecorderFor("relationaldatabaseprovider_controller") + return ctrl.NewControllerManagedBy(mgr). + For(&crdv1alpha1.RelationalDatabaseProvider{}). + WithEventFilter(predicate.GenerationChangedPredicate{}). + // let's set the max concurrent reconciles to 1 as we don't want to run multiple reconciles at the same time + // although we could also change this and guard it by the name of the database provider + WithOptions(controller.Options{MaxConcurrentReconciles: 1}). + Complete(r) +} diff --git a/internal/controller/databasemysqlprovider_controller_test.go b/internal/controller/relationaldatabaseprovider_controller_test.go similarity index 68% rename from internal/controller/databasemysqlprovider_controller_test.go rename to internal/controller/relationaldatabaseprovider_controller_test.go index cc19be5..d3f5248 100644 --- a/internal/controller/databasemysqlprovider_controller_test.go +++ b/internal/controller/relationaldatabaseprovider_controller_test.go @@ -30,12 +30,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" crdv1alpha1 "github.com/uselagoon/dbaas-controller/api/v1alpha1" - "github.com/uselagoon/dbaas-controller/internal/database/mysql" + "github.com/uselagoon/dbaas-controller/internal/database" ) -var _ = Describe("DatabaseMySQLProvider Controller", func() { +var _ = Describe("RelationalDatabaseProvider Controller", func() { Context("When reconciling a resource", func() { - const resourceName = "mysql-provider-test" + const resourceName = "relational-database-provider-test" ctx := context.Background() @@ -43,19 +43,19 @@ var _ = Describe("DatabaseMySQLProvider Controller", func() { Name: resourceName, Namespace: "default", } - databasemysqlprovider := &crdv1alpha1.DatabaseMySQLProvider{} + relationaldatabaseprovider := &crdv1alpha1.RelationalDatabaseProvider{} BeforeEach(func() { - By("creating the custom resource for the Kind DatabaseMySQLProvider") + By("creating the custom resource for the RelationalDatabaseProvider") secret := &v1.Secret{} err := k8sClient.Get(ctx, types.NamespacedName{ - Name: "test-mysql-provider-secret", + Name: "test-rel-db-provider-secret", Namespace: "default", }, secret) if err != nil && errors.IsNotFound(err) { secret = &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-mysql-provider-secret", + Name: "test-rel-db-provider-secret", Namespace: "default", }, StringData: map[string]string{ @@ -66,16 +66,17 @@ var _ = Describe("DatabaseMySQLProvider Controller", func() { Expect(err).NotTo(HaveOccurred()) } - err = k8sClient.Get(ctx, typeNamespacedName, databasemysqlprovider) + err = k8sClient.Get(ctx, typeNamespacedName, relationaldatabaseprovider) if err != nil && errors.IsNotFound(err) { - resource := &crdv1alpha1.DatabaseMySQLProvider{ + resource := &crdv1alpha1.RelationalDatabaseProvider{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: "default", }, - Spec: crdv1alpha1.DatabaseMySQLProviderSpec{ + Spec: crdv1alpha1.RelationalDatabaseProviderSpec{ + Type: "mysql", Scope: "custom", - MySQLConnections: []crdv1alpha1.MySQLConnection{ + Connections: []crdv1alpha1.Connection{ { Name: "test-connection", Hostname: "test-hostname", @@ -95,29 +96,29 @@ var _ = Describe("DatabaseMySQLProvider Controller", func() { }) AfterEach(func() { - resource := &crdv1alpha1.DatabaseMySQLProvider{} + resource := &crdv1alpha1.RelationalDatabaseProvider{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) secret := &v1.Secret{} err = k8sClient.Get(ctx, types.NamespacedName{ - Name: resource.Spec.MySQLConnections[0].PasswordSecretRef.Name, - Namespace: resource.Spec.MySQLConnections[0].PasswordSecretRef.Namespace, + Name: resource.Spec.Connections[0].PasswordSecretRef.Name, + Namespace: resource.Spec.Connections[0].PasswordSecretRef.Namespace, }, secret) Expect(err).NotTo(HaveOccurred()) - By("Cleanup the specific resource instance DatabaseMySQLProvider") + By("Cleanup the specific resource instance RelationalDatabaseProvider") Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) Expect(k8sClient.Delete(ctx, secret)).To(Succeed()) }) It("should successfully reconcile the resource", func() { By("Reconciling the created resource") fakeRecorder := record.NewFakeRecorder(100) - controllerReconciler := &DatabaseMySQLProviderReconciler{ + controllerReconciler := &RelationalDatabaseProviderReconciler{ Client: k8sClient, Scheme: k8sClient.Scheme(), Recorder: fakeRecorder, - MySQLClient: &mysql.MySQLMock{}, + RelDBClient: &database.RelationalDatabaseMock{}, } _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ @@ -125,11 +126,11 @@ var _ = Describe("DatabaseMySQLProvider Controller", func() { }) Expect(err).NotTo(HaveOccurred()) // check status of the resource - err = k8sClient.Get(ctx, typeNamespacedName, databasemysqlprovider) + err = k8sClient.Get(ctx, typeNamespacedName, relationaldatabaseprovider) Expect(err).NotTo(HaveOccurred()) - Expect(len(databasemysqlprovider.Status.Conditions)).To(Equal(1)) - Expect(databasemysqlprovider.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue)) - Expect(databasemysqlprovider.Status.Conditions[0].Type).To(Equal("Ready")) + Expect(len(relationaldatabaseprovider.Status.Conditions)).To(Equal(1)) + Expect(relationaldatabaseprovider.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue)) + Expect(relationaldatabaseprovider.Status.Conditions[0].Type).To(Equal("Ready")) }) }) }) diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..2bcff05 --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,504 @@ +package database + +import ( + "context" + "database/sql" + "fmt" + "math/rand" + + _ "github.com/go-sql-driver/mysql" + "github.com/lib/pq" + _ "github.com/lib/pq" + + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const ( + // maxUsernameLength MySQL and PostgreSQL username must use valid characters and be at most 16 characters long + maxUsernameLength = 16 + // maxPasswordLength MySQL and PostgreSQL password must use valid characters and be at most 24 characters long + maxPasswordLength = 24 + // maxDatabaseNameLength MySQL and PostgreSQL database name must use valid characters and be at most 63 characters long + maxDatabaseNameLength = 63 + // mysql is the type for MySQL + mysql = "mysql" + // postgres is the type for PostgreSQL + postgres = "postgres" +) + +// RelationalDatabaseInfo contains the username, password, and database name of a relational database +type RelationalDatabaseInfo struct { + // Username is the username for the database + Username string + // Password is the password for the database + Password string + // Dbname is the database name + Dbname string +} + +// RelationalDatabaseInterface is the interface for a relational database +// Note that the implementation of this interface should be idempotent. +type RelationalDatabaseInterface interface { + // GetConnection returns a connection to the relational database + GetConnection(ctx context.Context, dsn string, dbType string) (*sql.DB, error) + + // Ping pings the relational database + Ping(ctx context.Context, dsn string, dbType string) error + + // Version returns the version of the relational database + Version(ctx context.Context, dsn string, dbType string) (string, error) + + // Load of the database measured in MB of data and index size. + // Higher values indicate more data and indexes. + Load(ctx context.Context, dsn string, dbType string) (int, error) + + // Initialize initializes the relational database + // This is used by the database {MySQL,PostgreSQL} provider to initialize the relational database. + // It does setup the dbass_controller database. + // This function is idempotent and can be called multiple times without side effects. + Initialize(ctx context.Context, dsn string, dbType string) error + + // CreateDatabase creates a database in the relational database if it does not exist. + // It also creates a user and grants the user permissions on the database. + // This function is idempotent and can be called multiple times without side effects. + // returns the database name, username, and password + CreateDatabase(ctx context.Context, dsn, name, namespace, dbType string) (RelationalDatabaseInfo, error) + + // DropDatabase drops a database in the MySQL or PostgreSQL database if it exists. + // This function is idempotent and can be called multiple times without side effects. + DropDatabase(ctx context.Context, dsn, name, namespace, dbType string) error + + // GetDatabase returns the database name, username, and password for the given name and namespace. + GetDatabase(ctx context.Context, dsn, name, namespace, dbType string) (RelationalDatabaseInfo, error) +} + +// RelationalDatabaseImpl is the implementation of the RelationalDatabaseInterface +type RelationalDatabaseImpl struct { + connectionCache map[string]*sql.DB +} + +// Make sure RelationalDatabaseBasicImpl implements RelationalDatabaseBasicInterface +var _ RelationalDatabaseInterface = (*RelationalDatabaseImpl)(nil) + +// NewRelationalDatabaseBasicImpl creates a new RelationalDatabaseBasicImpl +func New() *RelationalDatabaseImpl { + return &RelationalDatabaseImpl{ + connectionCache: make(map[string]*sql.DB), + } +} + +// GetConnection returns a connection to the MySQL or PostgreSQL database +func (ri *RelationalDatabaseImpl) GetConnection(ctx context.Context, dsn string, dbType string) (*sql.DB, error) { + if db, ok := ri.connectionCache[dsn]; ok { + return db, nil + } + + log.FromContext(ctx).Info("Opening new database connection", "dbType", dbType) + db, err := sql.Open(dbType, dsn) + if err != nil { + return nil, fmt.Errorf("failed to open %s database: %w", dbType, err) + } + + ri.connectionCache[dsn] = db + return db, nil +} + +// Ping pings the relational database +func (ri *RelationalDatabaseImpl) Ping(ctx context.Context, dsn string, dbType string) error { + log.FromContext(ctx).Info("Pinging database", "dbType", dbType) + db, err := ri.GetConnection(ctx, dsn, dbType) + if err != nil { + return fmt.Errorf("ping failed to open %s database: %w", dbType, err) + } + + if err := db.PingContext(ctx); err != nil { + return fmt.Errorf("failed to ping %s database: %w", dbType, err) + } + + return nil +} + +// Version returns the version of the MySQL or PostgreSQL database +func (ri *RelationalDatabaseImpl) Version(ctx context.Context, dsn string, dbType string) (string, error) { + log.FromContext(ctx).Info("Getting database version", "dbType", dbType) + db, err := ri.GetConnection(ctx, dsn, dbType) + if err != nil { + return "", fmt.Errorf("version failed to open %s database: %w", dbType, err) + } + + var version string + err = db.QueryRowContext(ctx, "SELECT VERSION()").Scan(&version) + if err != nil { + return "", fmt.Errorf("version failed to get %s database version: %w", dbType, err) + } + + return version, nil +} + +// Load returns the load of the MySQL or PostgreSQL database measured in MB of data and index size. +// Note it doesn't include CPU or memory usage which could be obtained from other sources. +func (ri *RelationalDatabaseImpl) Load(ctx context.Context, dsn string, dbType string) (int, error) { + log.FromContext(ctx).Info("Getting database load", "dbType", dbType) + db, err := ri.GetConnection(ctx, dsn, dbType) + if err != nil { + return 0, fmt.Errorf("load failed to open %s database: %w", dbType, err) + } + + var totalLoad float64 + if dbType == mysql { + err = db.QueryRowContext(ctx, "SELECT data_length + index_length FROM information_schema.tables").Scan(&totalLoad) + if err != nil { + return 0, fmt.Errorf("load failed to get %s database load: %w", dbType, err) + } + } else if dbType == postgres { + err = db.QueryRowContext(ctx, "SELECT pg_database_size(current_database())").Scan(&totalLoad) + if err != nil { + return 0, fmt.Errorf("load failed to get %s database load: %w", dbType, err) + } + } else { + return 0, fmt.Errorf("load failed to get %s database load: unsupported dbType", dbType) + } + // convert bytes to MB + totalLoadMB := totalLoad / 1024 / 1024 + return int(totalLoadMB), nil +} + +// Initialize initializes the MySQL or PostgreSQL database +// This is used by the database {MySQL,PostgreSQL} provider to initialize the MySQL or PostgreSQL database. +// It does setup the dbass_controller database. +// This function is idempotent and can be called multiple times without side effects. +func (ri *RelationalDatabaseImpl) Initialize(ctx context.Context, dsn string, dbType string) error { + log.FromContext(ctx).Info("Initializing database", "dbType", dbType) + db, err := ri.GetConnection(ctx, dsn, dbType) + if err != nil { + return fmt.Errorf("initialize failed to open %s database: %w", dbType, err) + } + + if dbType == mysql { + _, err = db.ExecContext(ctx, "CREATE DATABASE IF NOT EXISTS dbaas_controller") + if err != nil { + return fmt.Errorf("initialize failed to create %s database: %w", dbType, err) + } + + _, err = db.ExecContext(ctx, "USE dbaas_controller") + if err != nil { + return fmt.Errorf("initialize failed to use %s database: %w", dbType, err) + } + + _, err = db.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + namespace VARCHAR(255) NOT NULL, + username VARCHAR(16) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + dbname VARCHAR(255) NOT NULL UNIQUE, + CONSTRAINT unique_name_namespace UNIQUE (name, namespace) + ) ENGINE=InnoDB`) + if err != nil { + return fmt.Errorf("initialize failed to create %s table: %w", dbType, err) + } + } else if dbType == postgres { + _, err := db.ExecContext(ctx, "CREATE SCHEMA IF NOT EXISTS dbaas_controller") + if err != nil { + return fmt.Errorf("initialize failed to create %s database: %w", dbType, err) + } + + _, err = db.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS dbaas_controller.users ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + namespace VARCHAR(255) NOT NULL, + username VARCHAR(16) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + dbname VARCHAR(255) NOT NULL UNIQUE, + CONSTRAINT unique_name_namespace UNIQUE (name, namespace) + )`) + if err != nil { + return fmt.Errorf("initialize failed to create %s table: %w", dbType, err) + } + } else { + return fmt.Errorf("initialize failed to initialize %s database: unsupported dbType", dbType) + } + + return nil +} + +// CreateDatabase creates a database in the MySQL or PostgreSQL server if it does not exist. +func (ri *RelationalDatabaseImpl) CreateDatabase( + ctx context.Context, + dsn, name, namespace string, + dbType string, +) (RelationalDatabaseInfo, error) { + log.FromContext(ctx).Info("Creating database", "dbType", dbType) + db, err := ri.GetConnection(ctx, dsn, dbType) + if err != nil { + return RelationalDatabaseInfo{}, fmt.Errorf("create database failed to open %s database: %w", dbType, err) + } + + var info RelationalDatabaseInfo + if dbType == mysql { + info, err = ri.databaseInfoMySQL(ctx, dsn, name, namespace) + if err != nil { + return info, fmt.Errorf("create %s database failed to get database info: %w", dbType, err) + } + // Create the database + _, err = db.ExecContext(ctx, fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`", info.Dbname)) + if err != nil { + return info, fmt.Errorf( + "create %s database error in creating the database `%s`: %w", dbType, info.Dbname, err) + } + // Create the user and grant permissions + // Use prepared statements to avoid SQL injection vulnerabilities. + _, err = db.ExecContext( + ctx, fmt.Sprintf("CREATE USER IF NOT EXISTS '%s'@'%%' IDENTIFIED BY '%s'", info.Username, info.Password)) + if err != nil { + return info, fmt.Errorf("create %s database error creating user `%s`: %w", dbType, info.Username, err) + } + + _, err = db.ExecContext(ctx, fmt.Sprintf( + "GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, REFERENCES, "+ + "INDEX, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES, "+ + "EXECUTE, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EVENT, TRIGGER ON `%s`.* TO '%s'@'%%'", + info.Dbname, info.Username)) + if err != nil { + return info, fmt.Errorf( + "create %s database error granting privileges to user `%s` on database `%s`: %w", + dbType, info.Username, info.Dbname, err) + } + + _, err = db.ExecContext(ctx, "FLUSH PRIVILEGES") + if err != nil { + return info, fmt.Errorf("create %s database error flushing privileges: %w", dbType, err) + } + } else if dbType == postgres { + info, err = ri.databaseInfoPostgreSQL(ctx, dsn, name, namespace) + if err != nil { + return info, fmt.Errorf("create database failed to get %s database info: %w", dbType, err) + } + // Create the database + _, err = db.Exec(fmt.Sprintf("CREATE DATABASE \"%s\"", info.Dbname)) + if pqErr, ok := err.(*pq.Error); !ok || ok && pqErr.Code != "42P04" { + // either the error is not a pq.Error or it is a pq.Error but not a duplicate_database error + // 42P04 is the error code for duplicate_database + return info, fmt.Errorf( + "create %s database error in creating the database `%s`: %w", dbType, info.Dbname, err) + } + + // Check if user exists and create or update the user + var userExists int + err = db.QueryRow(fmt.Sprintf("SELECT 1 FROM pg_roles WHERE rolname='%s'", info.Username)).Scan(&userExists) + if err != nil && err != sql.ErrNoRows { + return info, fmt.Errorf( + "create %s database error in check if user exists in database `%s`: %w", dbType, info.Dbname, err) + } + + if userExists == 0 { + // Create the user with encrypted password + _, err = db.Exec(fmt.Sprintf("CREATE USER \"%s\" WITH ENCRYPTED PASSWORD '%s'", info.Username, info.Password)) + if err != nil { + return info, fmt.Errorf( + "create %s database error in create user in database `%s`: %w", dbType, info.Dbname, err) + } + } + + // Grant privileges + _, err = db.Exec(fmt.Sprintf("GRANT ALL PRIVILEGES ON DATABASE \"%s\" TO \"%s\"", info.Dbname, info.Username)) + if err != nil { + return info, fmt.Errorf( + "create %s database error in grant privileges in database `%s`: %w", dbType, info.Dbname, err) + } + } else { + return RelationalDatabaseInfo{}, fmt.Errorf( + "create database failed to create %s database: unsupported dbType", dbType) + } + + return info, nil +} + +// DropDatabase drops a database in the MySQL or PostgreSQL database if it exists. +func (ri *RelationalDatabaseImpl) DropDatabase(ctx context.Context, dsn, name, namespace, dbType string) error { + log.FromContext(ctx).Info("Dropping database", "dbType", dbType) + db, err := ri.GetConnection(ctx, dsn, dbType) + if err != nil { + return fmt.Errorf("drop database failed to open %s database: %w", dbType, err) + } + + info := RelationalDatabaseInfo{} + if dbType == mysql { + info, err = ri.databaseInfoMySQL(ctx, dsn, name, namespace) + if err != nil { + return fmt.Errorf("drop database failed to get database info: %w", err) + } + // Drop the database + _, err = db.ExecContext(ctx, fmt.Sprintf("DROP DATABASE IF EXISTS `%s`", info.Dbname)) + if err != nil { + return fmt.Errorf("drop database failed to drop %s database: %w", dbType, err) + } + // Drop the user + _, err = db.ExecContext(ctx, fmt.Sprintf("DROP USER IF EXISTS '%s'@'%%'", info.Username)) + if err != nil { + return fmt.Errorf("drop database failed to drop user: %w", err) + } + // flush privileges + _, err = db.ExecContext(ctx, "FLUSH PRIVILEGES") + if err != nil { + return fmt.Errorf("drop database failed to flush privileges: %w", err) + } + } else if dbType == postgres { + info, err = ri.databaseInfoPostgreSQL(ctx, dsn, name, namespace) + if err != nil { + return fmt.Errorf("drop database failed to get database info: %w", err) + } + // Disconnect all users from the database + _, err = db.Exec( + fmt.Sprintf( + "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '%s' AND pid <> pg_backend_pid()", // nolint: lll + info.Dbname, + ), + ) + if err != nil { + return fmt.Errorf("drop database failed to disconnect users from %s database: %w", dbType, err) + } + // Drop the database + _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS \"%s\"", info.Dbname)) + if err != nil { + return fmt.Errorf("drop database failed to drop %s database: %w", dbType, err) + } + // Drop the user + _, err = db.Exec(fmt.Sprintf("DROP USER IF EXISTS \"%s\"", info.Username)) + if err != nil { + return fmt.Errorf("drop database failed to drop user: %w", err) + } + } else { + return fmt.Errorf("drop database failed to drop %s database: unsupported dbType", dbType) + } + return nil +} + +// generateRandomString generates a random string of specified length +func generateRandomString(length int) string { + const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + result := make([]byte, length) + for i := range result { + result[i] = chars[rand.Intn(len(chars))] + } + return string(result) +} + +// createUserInfo creates a random username, password, and database name +func createUserInfo(_ context.Context, namespace string) RelationalDatabaseInfo { + info := RelationalDatabaseInfo{ + Username: generateRandomString(maxUsernameLength), + Password: generateRandomString(maxPasswordLength), + } + + dbnamePrefix := namespace + if len(dbnamePrefix) > 50 { + dbnamePrefix = dbnamePrefix[:50] + } + + info.Dbname = fmt.Sprintf( + "%s_%s", dbnamePrefix, generateRandomString(maxDatabaseNameLength-len(dbnamePrefix)-1)) + + return info +} + +// databaseInfoMySQL returns the username, password, and database name for the given name and namespace. +// It also creates the user and database if they do not exist. +// This function is idempotent and can be called multiple times without side effects. +func (ri *RelationalDatabaseImpl) databaseInfoMySQL( + ctx context.Context, + dsn, name, namespace string, +) (RelationalDatabaseInfo, error) { + var info RelationalDatabaseInfo + + db, err := ri.GetConnection(ctx, dsn, mysql) + if err != nil { + return RelationalDatabaseInfo{}, fmt.Errorf("create database failed to open %s database: %w", mysql, err) + } + + _, err = db.ExecContext(ctx, "USE dbaas_controller") + if err != nil { + return info, fmt.Errorf("failed to select database: %w", err) + } + + // Check if the username and database name already exist + // Use prepared statements to avoid SQL injection vulnerabilities. + err = db.QueryRowContext( + ctx, "SELECT username, password, dbname FROM users WHERE name = ? AND namespace = ?", name, namespace).Scan( + &info.Username, + &info.Password, + &info.Dbname, + ) + if err != nil { + // check if the error is a not found error + if err != sql.ErrNoRows { + return info, fmt.Errorf("failed %s to query users table: %w", mysql, err) + } + + info = createUserInfo(ctx, namespace) + // Insert the user into the users table + _, err = db.ExecContext( + ctx, "INSERT INTO users (name, namespace, username, password, dbname) VALUES (?, ?, ?, ?, ?)", + name, namespace, info.Username, info.Password, info.Dbname) + if err != nil { + return info, fmt.Errorf("failed to insert user into users table: %w", err) + } + } + return info, nil +} + +// databaseInfoPostgreSQL returns the username, password, and database name for the given name and namespace. +// It also creates the user and database if they do not exist. +// This function is idempotent and can be called multiple times without side effects. +func (ri *RelationalDatabaseImpl) databaseInfoPostgreSQL( + ctx context.Context, + dsn, name, namespace string, +) (RelationalDatabaseInfo, error) { + var info RelationalDatabaseInfo + + db, err := ri.GetConnection(ctx, dsn, postgres) + if err != nil { + return RelationalDatabaseInfo{}, fmt.Errorf("create database failed to open %s database: %w", postgres, err) + } + + // select username, password and dbname from the users table + err = db.QueryRowContext( + ctx, "SELECT username, password, dbname FROM dbaas_controller.users WHERE name = $1 AND namespace = $2", + name, namespace).Scan( + &info.Username, + &info.Password, + &info.Dbname, + ) + if err != nil { + // check if the error is a not found error + if err != sql.ErrNoRows { + return info, fmt.Errorf("failed %s to query users table: %w", postgres, err) + } + info = createUserInfo(ctx, namespace) + // Insert the user into the users table + _, err = db.ExecContext( + ctx, "INSERT INTO dbaas_controller.users (name, namespace, username, password, dbname) VALUES ($1, $2, $3, $4, $5)", + name, namespace, info.Username, info.Password, info.Dbname) + if err != nil { + return info, fmt.Errorf("failed to insert user into users table: %w", err) + } + } + + return info, nil +} + +// GetDatabase returns the database name, username, and password for the given name and namespace. +func (ri *RelationalDatabaseImpl) GetDatabase( + ctx context.Context, + dsn, name, namespace, dbType string, +) (RelationalDatabaseInfo, error) { + log.FromContext(ctx).Info("Getting database", "dbType", dbType, "name", name, "namespace", namespace) + if dbType == "mysql" { + return ri.databaseInfoMySQL(ctx, dsn, name, namespace) + } else if dbType == "postgres" { + return ri.databaseInfoPostgreSQL(ctx, dsn, name, namespace) + } + return RelationalDatabaseInfo{}, fmt.Errorf("get database failed to get %s database: unsupported dbType", dbType) +} diff --git a/internal/database/mock.go b/internal/database/mock.go new file mode 100644 index 0000000..990fe4d --- /dev/null +++ b/internal/database/mock.go @@ -0,0 +1,49 @@ +package database + +import ( + "context" + "database/sql" +) + +// Make sure RelationalDatabaseMock implements RelationalDatabaseInterface +var _ RelationalDatabaseInterface = (*RelationalDatabaseMock)(nil) + +// RelationalDatabaseMock is a mock implementation of the RelationalDatabase database +type RelationalDatabaseMock struct{} + +// Ping pings the relational database +func (mi *RelationalDatabaseMock) Ping(ctx context.Context, dsn string, kind string) error { + return nil +} + +// Version returns the version of the RelationalDatabase database +func (mi *RelationalDatabaseMock) Version(ctx context.Context, dsn string, kind string) (string, error) { + return "5.7.34", nil +} + +func (mi *RelationalDatabaseMock) Initialize(ctx context.Context, dsn string, kind string) error { + return nil +} + +// CreateDatabase creates a database in the relational database if it does not exist. +func (mi *RelationalDatabaseMock) CreateDatabase( + ctx context.Context, dsn, name, namespace, kind string) (RelationalDatabaseInfo, error) { + return RelationalDatabaseInfo{Username: "user", Password: "pass", Dbname: "db"}, nil +} + +func (mi *RelationalDatabaseMock) DropDatabase(ctx context.Context, dsn, name, namespace, kind string) error { + return nil +} + +func (mi *RelationalDatabaseMock) GetDatabase( + ctx context.Context, dsn, name, namespace, kind string) (RelationalDatabaseInfo, error) { + return RelationalDatabaseInfo{Username: "user", Password: "pass", Dbname: "db"}, nil +} + +func (mi *RelationalDatabaseMock) Load(ctx context.Context, dsn string, kind string) (int, error) { + return 10, nil +} + +func (mi *RelationalDatabaseMock) GetConnection(ctx context.Context, dsn string, kind string) (*sql.DB, error) { + return nil, nil +} diff --git a/internal/database/mongodb/mock.go b/internal/database/mongodb/mock.go new file mode 100644 index 0000000..79627e3 --- /dev/null +++ b/internal/database/mongodb/mock.go @@ -0,0 +1,51 @@ +package mongodb + +import ( + "context" + + "go.mongodb.org/mongo-driver/mongo" +) + +// MongoDBMock is a mock implementation of the MongoDB interface +type MongoDBMock struct{} + +// Make sure MongoDBImpl implements MongoDBInterface +var _ MongoDBInterface = (*MongoDBMock)(nil) + +// GetClient returns a connected MongoDB client from the cache or creates a new one +func (m *MongoDBMock) GetClient(ctx context.Context, mop MongoDBClientOptions) (*mongo.Client, error) { + return nil, nil +} + +// GetUser retrieves a user based on name and namespace +func (m *MongoDBMock) GetUser( + ctx context.Context, + mop MongoDBClientOptions, + name, namespace string, +) (MongoDBInfo, error) { + return MongoDBInfo{Username: "mock-user", Password: "mock-pw", Dbname: "mock-db"}, nil +} + +// DropUserAndDatabase drops a user and database in MongoDB +func (m *MongoDBMock) DropUserAndDatabase(ctx context.Context, mop MongoDBClientOptions, name, namespace string) error { + return nil +} + +// CreateUserAndDatabase creates a new user and database in MongoDB +func (m *MongoDBMock) CreateUserAndDatabase( + ctx context.Context, + mop MongoDBClientOptions, + name, namespace string, +) (MongoDBInfo, error) { + return MongoDBInfo{Username: "mock-user", Password: "mock-pw", Dbname: "mock-db"}, nil +} + +// Version returns the version of the MongoDB server +func (m *MongoDBMock) Version(ctx context.Context, mop MongoDBClientOptions) (string, error) { + return "4.4.0", nil +} + +// Ping checks if the MongoDB server is reachable +func (m *MongoDBMock) Ping(ctx context.Context, mop MongoDBClientOptions) error { + return nil +} diff --git a/internal/database/mongodb/mongodb.go b/internal/database/mongodb/mongodb.go new file mode 100644 index 0000000..bd68d4c --- /dev/null +++ b/internal/database/mongodb/mongodb.go @@ -0,0 +1,341 @@ +package mongodb + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "math/rand" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const ( + // limits based on + // - https://docs.mongodb.com/manual/reference/limits/ + // - https://docs.aws.amazon.com/documentdb/latest/developerguide/limits.html#limits-naming_constraints + // maxUsernameLength mongodb username must use valid characters and be at most 16 characters long + maxUsernameLength = 16 + // maxPasswordLength mongodb password must use valid characters and be at most 24 characters long + maxPasswordLength = 24 + // maxDatabaseNameLength mongoDB database name must use valid characters and be at most 63 characters long + maxDatabaseNameLength = 63 +) + +// MongoDBInterface interface defines the operations that can be performed on a MongoDB client +type MongoDBInterface interface { + // GetClient returns a connected MongoDB client from the cache or creates a new one + GetClient(ctx context.Context, mop MongoDBClientOptions) (*mongo.Client, error) + // GetUser retrieves a user based on name and namespace + GetUser(ctx context.Context, mop MongoDBClientOptions, name, namespace string) (MongoDBInfo, error) + // DropUserAndDatabase removes a user's database, their document in the users collection, + // and their MongoDB authenticated user account. + DropUserAndDatabase(ctx context.Context, mop MongoDBClientOptions, name, namespace string) error + // CreateUserAndDatabase creates or updates a user in the dbaas_controller.users collection and + // ensures a user database with authentication is created. + CreateUserAndDatabase(ctx context.Context, mop MongoDBClientOptions, name, namespace string) (MongoDBInfo, error) + // Version retrieves the version of the MongoDB server. + Version(ctx context.Context, mop MongoDBClientOptions) (string, error) + // Ping checks if the MongoDB server is reachable + Ping(ctx context.Context, mop MongoDBClientOptions) error +} + +// MongoDBInfo contains the username, password, and database name of a MongoDB user +type MongoDBInfo struct { + // Username is the username of the MongoDB user + Username string + // Password is the password of the MongoDB user + Password string + // Dbname is the name of the MongoDB database + Dbname string +} + +// MongoDBClientOptions contains the options required to connect to a MongoDB server +type MongoDBClientOptions struct { + Name string + Hostname string + Port int + Username string + Password string + Mechanism string + Source string + TLS bool +} + +// clientOptions returns the options required to connect to a MongoDB server +func (mco *MongoDBClientOptions) clientOptions() *options.ClientOptions { + credential := options.Credential{ + AuthSource: mco.Source, + Username: mco.Username, + Password: mco.Password, + AuthMechanism: mco.Mechanism, + } + uri := fmt.Sprintf("mongodb://%s:%d", mco.Hostname, mco.Port) + clientOpts := options.Client().ApplyURI(uri). + SetAuth(credential) + if mco.TLS { + clientOpts.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) + } + return clientOpts +} + +// MongoDBImpl encapsulates the MongoDB client and operations +type MongoDBImpl struct { + connectionCache map[MongoDBClientOptions]*mongo.Client +} + +// Make sure MongoDBImpl implements MongoDBInterface +var _ MongoDBInterface = (*MongoDBImpl)(nil) + +// NewMongoDBImpl creates a new instance of MongoDBImpl with initialized properties +func NewMongoDBImpl() *MongoDBImpl { + return &MongoDBImpl{ + connectionCache: make(map[MongoDBClientOptions]*mongo.Client), + } +} + +// GetClient returns a connected MongoDB client from the cache or creates a new one +func (mi *MongoDBImpl) GetClient(ctx context.Context, mop MongoDBClientOptions) (*mongo.Client, error) { + if client, ok := mi.connectionCache[mop]; ok { + return client, nil + } + + client, err := mongo.Connect(ctx, mop.clientOptions()) + if err != nil { + return nil, fmt.Errorf("failed to connect to MongoDB: %w", err) + } + + mi.connectionCache[mop] = client + return client, nil +} + +// GetUser retrieves a user based on name and namespace +func (mi *MongoDBImpl) GetUser(ctx context.Context, mop MongoDBClientOptions, name, namespace string) (MongoDBInfo, error) { + client, err := mi.GetClient(ctx, mop) + if err != nil { + return MongoDBInfo{}, fmt.Errorf("failed to get MongoDB User: %w", err) + } + collection := client.Database("dbaas_controller").Collection("users") + var result MongoDBInfo + + filter := bson.M{"name": name, "namespace": namespace} + if err = collection.FindOne(ctx, filter).Decode(&result); err != nil { + if err == mongo.ErrNoDocuments { + return MongoDBInfo{}, fmt.Errorf("user %s in namespace %s does not exist, %w", name, namespace, err) + } + return MongoDBInfo{}, fmt.Errorf("failed to retrieve user: %w", err) + } + + return result, nil +} + +func createMongoDBUserInfo(namespace string) MongoDBInfo { + username := generateRandomString(maxUsernameLength) + password := generateRandomString(maxPasswordLength) + dbname := fmt.Sprintf("%s_%s", namespace, generateRandomString(maxDatabaseNameLength-len(namespace)-1)) + + return MongoDBInfo{ + Username: username, + Password: password, + Dbname: dbname, + } +} + +// DropUserAndDatabase removes a user's database, their document in the users collection, +// and their MongoDB authenticated user account. +func (mi *MongoDBImpl) DropUserAndDatabase(ctx context.Context, mop MongoDBClientOptions, name, namespace string) error { + // Retrieve user information to get the database name and username + log.FromContext(ctx).Info("Drop user and database", "name", name, "namespace", namespace) + client, err := mi.GetClient(ctx, mop) + if err != nil { + return fmt.Errorf("failed to drop user and database: %w", err) + } + userInfo, err := mi.GetUser(ctx, mop, name, namespace) + if err != nil { + // check if the error is mongo.ErrNoDocuments + if errors.Is(err, mongo.ErrNoDocuments) { + // if the user does not exist then return nil + log.FromContext(ctx).Info("User does not exist", "name", name, "namespace", namespace) + return nil + } + return fmt.Errorf("failed to retrieve user info: %w", err) + } + + // Drop the user's specific database + userDB := client.Database(userInfo.Dbname) + if err := userDB.Drop(ctx); err != nil { + return fmt.Errorf("failed to drop user database %s: %w", userInfo.Dbname, err) + } + + // Drop the MongoDB authenticated user + adminDB := client.Database("admin") + command := bson.D{{Key: "dropUser", Value: userInfo.Username}} + if err := adminDB.RunCommand(ctx, command).Err(); err != nil { + return fmt.Errorf("failed to drop MongoDB user %s: %w", userInfo.Username, err) + } + + // Remove the user document from the dbaas_controller.users collection + collection := client.Database("dbaas_controller").Collection("users") + filter := bson.M{"name": name, "namespace": namespace} + result, err := collection.DeleteOne(ctx, filter) + if err != nil { + return fmt.Errorf("failed to delete user document: %w", err) + } + if result.DeletedCount == 0 { + return fmt.Errorf("no user document found with name %s in namespace %s", name, namespace) + } + + return nil +} + +// CreateUserAndDatabase creates or updates a user in the dbaas_controller.users collection +// and ensures a user database with authentication is created. +// This function is idempotent. +func (mi *MongoDBImpl) CreateUserAndDatabase( + ctx context.Context, + mop MongoDBClientOptions, + name, namespace string, +) (MongoDBInfo, error) { + log.FromContext(ctx).Info("Create user and database", "name", name, "namespace", namespace) + client, err := mi.GetClient(ctx, mop) + if err != nil { + return MongoDBInfo{}, fmt.Errorf("failed to create user and database: %w", err) + } + // First try to get the user info to see if the user already exists + userInfo, err := mi.GetUser(ctx, mop, name, namespace) + if err != nil { + // check if it is mongo.ErrNoDocuments + if !errors.Is(err, mongo.ErrNoDocuments) { + return MongoDBInfo{}, fmt.Errorf("failed to get user info: %w", err) + } else { + log.FromContext(ctx).Info("User does not exist", "name", name, "namespace", namespace) + userInfo = createMongoDBUserInfo(namespace) + } + } + + collection := client.Database("dbaas_controller").Collection("users") + + // Ensure the user's specific database exists by inserting a dummy document + userDB := client.Database(userInfo.Dbname) + _, err = userDB.Collection("init").InsertOne(ctx, bson.M{"init": "true"}) + if err != nil { + return MongoDBInfo{}, fmt.Errorf("failed to ensure database exists: %w", err) + } + + // Create or update the user document in the dbaas_controller.users collection + filter := bson.M{"name": name, "namespace": namespace} + update := bson.M{"$setOnInsert": userInfo} + opts := options.FindOneAndUpdate().SetUpsert(true) + err = collection.FindOneAndUpdate(ctx, filter, update, opts).Decode(&userInfo) + if err != nil && err != mongo.ErrNoDocuments { + return MongoDBInfo{}, fmt.Errorf("failed to create or update user document: %w", err) + } + + // Attempt to create the MongoDB user for authentication + userExists, err := mi.mongoUserExists(ctx, client, userInfo.Username) + if err != nil { + return MongoDBInfo{}, fmt.Errorf("failed to check if user exists: %w", err) + } + if userExists { + log.FromContext(ctx).Info("User already exists", "user", userInfo.Username) + return userInfo, nil + } + + adminDB := client.Database("admin") + command := bson.D{ + {Key: "createUser", Value: userInfo.Username}, + {Key: "pwd", Value: userInfo.Password}, + {Key: "roles", Value: []bson.M{{"role": "readWrite", "db": userInfo.Dbname}}}, + } + err = adminDB.RunCommand(ctx, command).Err() + if err != nil { + return MongoDBInfo{}, fmt.Errorf("failed to create MongoDB user: %w", err) + } + + return userInfo, nil +} + +// CreateMongoDBInfo creates MongoDBInfo with random credentials +func CreateMongoDBInfo(namespace string) MongoDBInfo { + info := MongoDBInfo{ + Username: generateRandomString(maxUsernameLength), + Password: generateRandomString(maxPasswordLength), + } + + dbnamePrefix := namespace + if len(dbnamePrefix) > 50 { + dbnamePrefix = dbnamePrefix[:50] + } + + info.Dbname = fmt.Sprintf("%s_%s", dbnamePrefix, generateRandomString(maxDatabaseNameLength-len(dbnamePrefix)-1)) + return info +} + +// Version retrieves the version of the MongoDB server. +func (mi *MongoDBImpl) Version(ctx context.Context, mop MongoDBClientOptions) (string, error) { + log.FromContext(ctx).Info("Retrieve MongoDB version") + client, err := mi.GetClient(ctx, mop) + if err != nil { + return "", fmt.Errorf("failed to get version: %w", err) + } + var result bson.M + if err := client.Database("admin").RunCommand(ctx, bson.D{{Key: "buildInfo", Value: 1}}).Decode(&result); err != nil { + return "", fmt.Errorf("failed to execute buildInfo command: %w", err) + } + + version, ok := result["version"].(string) + if !ok { + return "", fmt.Errorf("version not found in buildInfo response") + } + return version, nil +} + +// Ping checks if the MongoDB server is reachable +func (mi *MongoDBImpl) Ping(ctx context.Context, mop MongoDBClientOptions) error { + log.FromContext(ctx).Info("Ping MongoDB server") + client, err := mi.GetClient(ctx, mop) + if err != nil { + return fmt.Errorf("failed to ping MongoDB server: %w", err) + } + return client.Ping(ctx, nil) +} + +// mongoUserExists checks if a user exists in the MongoDB admin database +// This function is idempotent. +func (mi *MongoDBImpl) mongoUserExists( + ctx context.Context, + client *mongo.Client, + username string, +) (bool, error) { + log.FromContext(ctx).Info("Check if user exists", "user", username) + adminDB := client.Database("admin") + command := bson.D{{Key: "usersInfo", Value: bson.M{"user": username, "db": "admin"}}} + + var result bson.M + err := adminDB.RunCommand(ctx, command).Decode(&result) + if err != nil { + return false, fmt.Errorf("failed to run usersInfo command: %w", err) + } + + users, ok := result["users"].(primitive.A) + // Make sure this assertion matches the actual data structure fmt.Printf("Users: %+v\n", users) + if !ok || len(users) == 0 { + return false, nil + } + + return true, nil +} + +// Helper function to generate random strings +func generateRandomString(length int) string { + const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + result := make([]byte, length) + for i := range result { + result[i] = chars[rand.Intn(len(chars))] + } + return string(result) +} diff --git a/internal/database/mysql/mock.go b/internal/database/mysql/mock.go deleted file mode 100644 index bdda914..0000000 --- a/internal/database/mysql/mock.go +++ /dev/null @@ -1,40 +0,0 @@ -package mysql - -import "context" - -// Make sure MySQLMock implements MySQLInterface -var _ MySQLInterface = (*MySQLMock)(nil) - -// MySQLMock is a mock implementation of the MySQL database -type MySQLMock struct{} - -// Ping pings the MySQL database -func (mi *MySQLMock) Ping(ctx context.Context, dsn string) error { - return nil -} - -// Version returns the version of the MySQL database -func (mi *MySQLMock) Version(ctx context.Context, dsn string) (string, error) { - return "5.7.34", nil -} - -func (mi *MySQLMock) Initialize(ctx context.Context, dsn string) error { - return nil -} - -// CreateDatabase creates a database in the MySQL database if it does not exist. -func (mi *MySQLMock) CreateDatabase(ctx context.Context, dsn, name, namespace string) (DatabaseInfo, error) { - return DatabaseInfo{Username: "user", Password: "pass", Dbname: "db"}, nil -} - -func (mi *MySQLMock) DropDatabase(ctx context.Context, dsn, name, namespace string) error { - return nil -} - -func (mi *MySQLMock) GetDatabase(ctx context.Context, dsn, name, namespace string) (DatabaseInfo, error) { - return DatabaseInfo{Username: "user", Password: "pass", Dbname: "db"}, nil -} - -func (mi *MySQLMock) Load(ctx context.Context, dsn string) (int, error) { - return 10, nil -} diff --git a/internal/database/mysql/mysql.go b/internal/database/mysql/mysql.go deleted file mode 100644 index b2f8036..0000000 --- a/internal/database/mysql/mysql.go +++ /dev/null @@ -1,356 +0,0 @@ -package mysql - -import ( - "context" - "database/sql" - "fmt" - "math/rand" - - _ "github.com/go-sql-driver/mysql" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -// DatabaseInfo contains the username, password, and database name -type DatabaseInfo struct { - // Username is the username for the database - Username string - // Password is the password for the database - Password string - // Dbname is the database name - Dbname string -} - -// MySQLInterface is the interface for the MySQL database -type MySQLInterface interface { - // Ping pings the MySQL database - Ping(ctx context.Context, dsn string) error - - // Version returns the version of the MySQL database - Version(ctx context.Context, dsn string) (string, error) - - // Load of the MySQL database measured in MB of data and index size. - // Higher values indicate more data and indexes. - Load(ctx context.Context, dsn string) (int, error) - - // Initialize initializes the MySQL database - // This is used by the database MySQL provider to initialize the MySQL database. - // It does setup the dbass_controller database. - // This function is idempotent and can be called multiple times without side effects. - Initialize(ctx context.Context, dsn string) error - - // CreateDatabase creates a database in the MySQL database if it does not exist. - // It also creates a user and grants the user permissions on the database. - // This function is idempotent and can be called multiple times without side effects. - // returns the database name, username, and password - CreateDatabase(ctx context.Context, dsn, name, namespace string) (DatabaseInfo, error) - - // DropDatabase drops a database in the MySQL database if it exists. - // This function is idempotent and can be called multiple times without side effects. - DropDatabase(ctx context.Context, dsn, name, namespace string) error - - // GetDatabase returns the database name, username, and password for the given name and namespace. - GetDatabase(ctx context.Context, dsn, name, namespace string) (DatabaseInfo, error) -} - -// MySQLImpl is the implementation of the MySQL database -// Note that we maintain a connection cache to avoid opening a new connection for each operation. -type MySQLImpl struct { - ConnectionCache map[string]*sql.DB -} - -// Make sure MySQLImpl implements MySQLInterface -var _ MySQLInterface = (*MySQLImpl)(nil) - -// getConnection returns a connection to the MySQL database -func (mi *MySQLImpl) getConnection(ctx context.Context, dsn string) (*sql.DB, error) { - if db, ok := mi.ConnectionCache[dsn]; ok { - return db, nil - } - db, err := sql.Open("mysql", dsn) - if err != nil { - return nil, fmt.Errorf("failed to open MySQL database: %w", err) - } - log.FromContext(ctx).Info("Opening MySQL database connection") - mi.ConnectionCache[dsn] = db - return db, nil -} - -// Ping pings the MySQL database -func (mi *MySQLImpl) Ping(ctx context.Context, dsn string) error { - log.FromContext(ctx).Info("Pinging MySQL database") - db, err := mi.getConnection(ctx, dsn) - if err != nil { - return fmt.Errorf("ping failed to open MySQL database: %w", err) - } - - if err := db.PingContext(ctx); err != nil { - return fmt.Errorf("failed to ping MySQL database: %w", err) - } - - return nil -} - -// Version returns the version of the MySQL database -func (mi *MySQLImpl) Version(ctx context.Context, dsn string) (string, error) { - log.FromContext(ctx).Info("Getting MySQL database version") - db, err := mi.getConnection(ctx, dsn) - if err != nil { - return "", fmt.Errorf("version failed to open MySQL database: %w", err) - } - - var version string - err = db.QueryRowContext(ctx, "SELECT VERSION()").Scan(&version) - if err != nil { - return "", fmt.Errorf("version failed to get MySQL database version: %w", err) - } - - return version, nil -} - -// Load returns the load of the MySQL database measured in MB of data and index size. -// Note it doesn't include CPU or memory usage which could be obtained from other sources. -func (mi *MySQLImpl) Load(ctx context.Context, dsn string) (int, error) { - log.FromContext(ctx).Info("Getting MySQL database load") - db, err := mi.getConnection(ctx, dsn) - if err != nil { - return 0, fmt.Errorf("load failed to open MySQL database: %w", err) - } - - query := ` - SELECT SUM(data_length + index_length) AS total_size - FROM information_schema.TABLES - ` - var totalLoad float64 - row := db.QueryRow(query) - err = row.Scan(&totalLoad) - if err != nil { - return 0, fmt.Errorf("load failed to get MySQL database load: %w", err) - } - totalLoadMB := totalLoad / (1024 * 1024) - return int(totalLoadMB), nil -} - -// Initialize sets up the dbaas_controller database and the users table. -func (mi *MySQLImpl) Initialize(ctx context.Context, dsn string) error { - log.FromContext(ctx).Info("Initializing MySQL database") - - // Connect to MySQL server without specifying a database - db, err := mi.getConnection(ctx, dsn) - if err != nil { - return err - } - - // Create the database if it doesn't exist - _, err = db.ExecContext(ctx, "CREATE DATABASE IF NOT EXISTS dbaas_controller") - if err != nil { - return fmt.Errorf("failed to create database: %w", err) - } - - // Select the database - _, err = db.ExecContext(ctx, "USE dbaas_controller") - if err != nil { - return fmt.Errorf("failed to select database: %w", err) - } - - // Create the users table if it doesn't exist - createTableSQL := ` - CREATE TABLE IF NOT EXISTS users ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - namespace VARCHAR(255) NOT NULL, - username VARCHAR(16) NOT NULL UNIQUE, - password VARCHAR(255) NOT NULL, - dbname VARCHAR(255) NOT NULL UNIQUE, - CONSTRAINT unique_name_namespace UNIQUE (name, namespace) - ) ENGINE=InnoDB;` - - _, err = db.ExecContext(ctx, createTableSQL) - if err != nil { - return fmt.Errorf("failed to create users table: %w", err) - } - - return nil -} - -// generateRandomString generates a random string of specified length -func generateRandomString(length int) string { - const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - result := make([]byte, length) - for i := range result { - result[i] = chars[rand.Intn(len(chars))] - } - return string(result) -} - -func (mi *MySQLImpl) databaseInfo(ctx context.Context, dsn, namespace, name string) (DatabaseInfo, error) { - log.FromContext(ctx).Info("creating a username and database in the dbaas_controller database") - - // MySQL username must use valid characters and be at most 16 characters long - const ( - maxUsernameLength = 16 - maxPasswordLength = 24 - maxDatabaseNameLength = 64 - ) - - info := DatabaseInfo{} - - // Connect to MySQL server and select the dbaas_controller database - db, err := mi.getConnection(ctx, dsn) - if err != nil { - return info, fmt.Errorf("failed to connect to MySQL server: %w", err) - } - - _, err = db.ExecContext(ctx, "USE dbaas_controller") - if err != nil { - return info, fmt.Errorf("failed to select database: %w", err) - } - - // Check if the username and database name already exist - // Use prepared statements to avoid SQL injection vulnerabilities. - err = db.QueryRowContext( - ctx, "SELECT username, password, dbname FROM users WHERE name = ? AND namespace = ?", name, namespace).Scan( - &info.Username, - &info.Password, - &info.Dbname, - ) - if err != nil { - // check if the error is a not found error - if err == sql.ErrNoRows { - info.Username = generateRandomString(maxUsernameLength) - info.Password = generateRandomString(maxPasswordLength) - - dbnamePrefix := namespace - if len(dbnamePrefix) > 50 { - dbnamePrefix = dbnamePrefix[:50] - } - - info.Dbname = fmt.Sprintf( - "%s_%s", dbnamePrefix, generateRandomString(maxDatabaseNameLength-len(dbnamePrefix)-1)) - - // Insert the user into the users table - _, err = db.ExecContext( - ctx, "INSERT INTO users (name, namespace, username, password, dbname) VALUES (?, ?, ?, ?, ?)", - name, namespace, info.Username, info.Password, info.Dbname) - if err != nil { - return info, fmt.Errorf("failed to insert user into users table: %w", err) - } - } else { - return info, fmt.Errorf("failed to query users table: %w", err) - } - } - return info, nil -} - -// CreateDatabase creates a database in the MySQL database if it does not exist. -// It also creates a user and grants the user permissions on the database. -// This function is idempotent and can be called multiple times without side effects. -func (mi *MySQLImpl) CreateDatabase(ctx context.Context, dsn, name, namespace string) (DatabaseInfo, error) { - log.FromContext(ctx).Info("Creating MySQL database", "name", name, "namespace", namespace) - - info, err := mi.databaseInfo(ctx, dsn, namespace, name) - if err != nil { - return info, fmt.Errorf("failed to get database info: %w", err) - } - // Connect to the database server - db, err := mi.getConnection(ctx, dsn) - if err != nil { - return info, fmt.Errorf("create database error connecting to the database server: %w", err) - } - - // Ping the database to verify connection establishment. - if err := db.PingContext(ctx); err != nil { - return info, fmt.Errorf( - "create database error verifying connection to the database server: %w", err) - } - - // Create the database - _, err = db.ExecContext(ctx, fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`", info.Dbname)) - if err != nil { - return info, fmt.Errorf( - "create database error in creating the database `%s`: %w", info.Dbname, err) - } - - // Create the user and grant permissions - // Use prepared statements to avoid SQL injection vulnerabilities. - _, err = db.ExecContext( - ctx, fmt.Sprintf("CREATE USER IF NOT EXISTS '%s'@'%%' IDENTIFIED BY '%s'", info.Username, info.Password)) - if err != nil { - return info, fmt.Errorf("create database error creating user `%s`: %w", info.Username, err) - } - - _, err = db.ExecContext(ctx, fmt.Sprintf( - "GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, REFERENCES, "+ - "INDEX, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES, "+ - "EXECUTE, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EVENT, TRIGGER ON `%s`.* TO '%s'@'%%'", - info.Dbname, info.Username)) - if err != nil { - return info, fmt.Errorf( - "create database error granting privileges to user `%s` on database `%s`: %w", info.Username, info.Dbname, err) - } - - _, err = db.ExecContext(ctx, "FLUSH PRIVILEGES") - if err != nil { - return info, fmt.Errorf("create database error flushing privileges: %w", err) - } - - return info, nil -} - -func (mi *MySQLImpl) DropDatabase(ctx context.Context, dsn, name, namespace string) error { - log.FromContext(ctx).Info("Dropping MySQL database", "name", name, "namespace", namespace) - - info, err := mi.databaseInfo(ctx, dsn, namespace, name) - if err != nil { - return fmt.Errorf("failed to get database info: %w", err) - } - - // Connect to the database server - db, err := mi.getConnection(ctx, dsn) - if err != nil { - return fmt.Errorf("drop database error connecting to the database server: %w", err) - } - - // Ping the database to verify connection establishment. - if err := db.PingContext(ctx); err != nil { - return fmt.Errorf("drop database error verifying connection to the database server: %w", err) - } - - // Drop the database - _, err = db.ExecContext(ctx, fmt.Sprintf("DROP DATABASE IF EXISTS `%s`", info.Dbname)) - if err != nil { - if err == sql.ErrNoRows { - log.FromContext(ctx).Info("Database does not exist", "name", name, "namespace", namespace, "dbname", info.Dbname) - } else { - return fmt.Errorf("drop database error in dropping the database `%s`: %w", info.Dbname, err) - } - } else { - log.FromContext(ctx).Info("Dropped database", "name", name, "namespace", namespace, "dbname", info.Dbname) - } - - // Delete the user - _, err = db.ExecContext(ctx, fmt.Sprintf("DROP USER IF EXISTS '%s'@'%%'", info.Username)) - if err != nil { - if err == sql.ErrNoRows { - log.FromContext(ctx).Info("User does not exist", "name", name, "namespace", namespace, "username", info.Username) - return nil - } else { - return fmt.Errorf("drop database error in dropping user `%s`: %w", info.Username, err) - } - } else { - log.FromContext(ctx).Info("Dropped user", "name", name, "namespace", namespace, "username", info.Username) - } - - return nil -} - -// GetDatabase returns the database name, username, and password for the given name and namespace. -func (mi *MySQLImpl) GetDatabase(ctx context.Context, dsn, name, namespace string) (DatabaseInfo, error) { - log.FromContext(ctx).Info("Getting MySQL database", "name", name, "namespace", namespace) - - info, err := mi.databaseInfo(ctx, dsn, namespace, name) - if err != nil { - return info, fmt.Errorf("failed to get database info: %w", err) - } - - return info, nil -} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 7d80416..e0cb9f0 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -37,8 +37,11 @@ var _ = Describe("controller", Ordered, func() { By("installing the cert-manager") Expect(utils.InstallCertManager()).To(Succeed()) - By("installing MySQL pod") - Expect(utils.InstallMySQL()).To(Succeed()) + By("installing relational databases pods") + Expect(utils.InstallRelationalDatabases()).To(Succeed()) + + By("installing mongodb pods") + Expect(utils.InstallMongoDB()).To(Succeed()) By("creating manager namespace") cmd := exec.Command("kubectl", "create", "ns", namespace) @@ -52,51 +55,59 @@ var _ = Describe("controller", Ordered, func() { By("uninstalling the cert-manager bundle") utils.UninstallCertManager() - By("removing the DatabaseMySQLProvider resource") - // we enforce the deletion by removing the finalizer - cmd := exec.Command( - "kubectl", - "patch", - "databasemysqlprovider", - "databasemysqlprovider-sample", - "-p", - `{"metadata":{"finalizers":[]}}`, - "--type=merge", - ) - _, _ = utils.Run(cmd) - - cmd = exec.Command("kubectl", "delete", "--force", "databasemysqlprovider", "databasemysqlprovider-sample") - _, _ = utils.Run(cmd) - + By("removing the RelationalDatabaseProvider resource") + for _, name := range []string{"mysql", "postgres", "mongodb"} { + cmd := exec.Command( + "kubectl", + "patch", + "relationaldatabaseprovider", + fmt.Sprintf("relationaldatabaseprovider-%s-sample", name), + "-p", + `{"metadata":{"finalizers":[]}}`, + "--type=merge", + ) + _, _ = utils.Run(cmd) + cmd = exec.Command( + "kubectl", "delete", "--force", "relationaldatabaseprovider", fmt.Sprintf( + "relationaldatabaseprovider-%s-sample", name)) + _, _ = utils.Run(cmd) + } By("removing the DatabaseRequest resource") - // we enforce the deletion by removing the finalizer - cmd = exec.Command( - "kubectl", - "patch", - "databaserequest", - "databaserequest-sample", - "-p", - `{"metadata":{"finalizers":[]}}`, - "--type=merge", - ) - _, _ = utils.Run(cmd) - cmd = exec.Command("kubectl", "delete", "--force", "databaserequest", "databaserequest-sample") - _, _ = utils.Run(cmd) - + for _, name := range []string{"mysql", "postgres", "mongodb"} { + cmd := exec.Command( + "kubectl", + "patch", + "databaserequest", + fmt.Sprintf("databaserequest-%s-sample", name), + "-p", + `{"metadata":{"finalizers":[]}}`, + "--type=merge", + ) + _, _ = utils.Run(cmd) + cmd = exec.Command( + "kubectl", "delete", "--force", "databaserequest", fmt.Sprintf( + "databaserequest-%s-sample", name)) + _, _ = utils.Run(cmd) + } By("removing manager namespace") - cmd = exec.Command("kubectl", "delete", "ns", namespace) + cmd := exec.Command("kubectl", "delete", "ns", namespace) _, _ = utils.Run(cmd) - By("uninstalling MySQL pod") - utils.UninstallMySQLPod() + By("uninstalling relational databases pods") + utils.UninstallRelationalDatabases() + + By("uninstalling mongodb pods") + utils.UninstallMongoDB() By("removing service and secret") - cmd = exec.Command( - "kubectl", "delete", "service", "-n", "default", "-l", "app.kubernetes.io/instance=databaserequest-sample") - _, _ = utils.Run(cmd) - cmd = exec.Command( - "kubectl", "delete", "secret", "-n", "default", "-l", "app.kubernetes.io/instance=databaserequest-sample") - _, _ = utils.Run(cmd) + for _, name := range []string{"mysql", "postgres", "mongodb"} { + cmd = exec.Command( + "kubectl", "delete", "service", "-n", "default", "-l", "app.kubernetes.io/instance=databaserequest-"+name+"-sample") + _, _ = utils.Run(cmd) + cmd = exec.Command( + "kubectl", "delete", "secret", "-n", "default", "-l", "app.kubernetes.io/instance=databaserequest-"+name+"-sample") + _, _ = utils.Run(cmd) + } }) Context("Operator", func() { @@ -162,98 +173,115 @@ var _ = Describe("controller", Ordered, func() { } EventuallyWithOffset(1, verifyControllerUp, time.Minute, time.Second).Should(Succeed()) - By("creating a DatabaseMySQLProvider resource") - cmd = exec.Command("kubectl", "apply", "-f", "config/samples/crd_v1alpha1_databasemysqlprovider.yaml") - _, err = utils.Run(cmd) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - - By("validating that the DatabaseMySQLProvider resource is created") - cmd = exec.Command( - "kubectl", - "wait", - "--for=condition=Ready", - "databasemysqlprovider", - "databasemysqlprovider-sample", - "--timeout=60s", - ) - _, err = utils.Run(cmd) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - - By("creating a DatabaseRequest resource") - cmd = exec.Command("kubectl", "apply", "-f", "config/samples/crd_v1alpha1_databaserequest.yaml") - _, err = utils.Run(cmd) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - - By("validating that the DatabaseRequest resource is created") - cmd = exec.Command( - "kubectl", - "wait", - "--for=condition=Ready", - "databaserequest", - "databaserequest-sample", - "--timeout=60s", - ) - _, err = utils.Run(cmd) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - - // verify that the service and secret got created - By("validating that the service is created") - cmd = exec.Command( - "kubectl", - "get", - "service", - "-n", "default", - "-l", "app.kubernetes.io/instance=databaserequest-sample", - ) - serviceOutput, err := utils.Run(cmd) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - serviceNames := utils.GetNonEmptyLines(string(serviceOutput)) - ExpectWithOffset(1, serviceNames).Should(HaveLen(2)) - ExpectWithOffset(1, serviceNames[1]).Should(ContainSubstring("first-mysql-db")) - - By("validating that the secret is created") - cmd = exec.Command( - "kubectl", - "get", - "secret", - "-n", "default", - "-l", "app.kubernetes.io/instance=databaserequest-sample", - ) - secretOutput, err := utils.Run(cmd) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - secretNames := utils.GetNonEmptyLines(string(secretOutput)) - ExpectWithOffset(1, secretNames).Should(HaveLen(2)) - - By("deleting the DatabaseRequest resource the database is getting deprovisioned") - cmd = exec.Command("kubectl", "delete", "databaserequest", "databaserequest-sample") - _, err = utils.Run(cmd) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - - By("validating that the service is deleted") - cmd = exec.Command( - "kubectl", - "get", - "service", - "-n", "default", - "-l", "app.kubernetes.io/instance=databaserequest-sample", - ) - serviceOutput, err = utils.Run(cmd) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - serviceNames = utils.GetNonEmptyLines(string(serviceOutput)) - ExpectWithOffset(1, serviceNames).Should(HaveLen(1)) - - By("validating that the secret is deleted") - cmd = exec.Command( - "kubectl", - "get", - "secret", - "-n", "default", - "-l", "app.kubernetes.io/instance=databaserequest-sample", - ) - secretOutput, err = utils.Run(cmd) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - secretNames = utils.GetNonEmptyLines(string(secretOutput)) - ExpectWithOffset(1, secretNames).Should(HaveLen(1)) + for _, name := range []string{"mysql", "postgres"} { + By("creating a RelationalDatabaseProvider resource") + cmd = exec.Command( + "kubectl", + "apply", + "-f", + fmt.Sprintf("config/samples/crd_v1alpha1_relationaldatabaseprovider_%s.yaml", name), + ) + _, err = utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("validating that the RelationalDatabaseProvider resource is created") + cmd = exec.Command( + "kubectl", + "wait", + "--for=condition=Ready", + "relationaldatabaseprovider", + fmt.Sprintf("relationaldatabaseprovider-%s-sample", name), + "--timeout=60s", + ) + _, err = utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("creating a DatabaseRequest resource") + cmd = exec.Command( + "kubectl", + "apply", + "-f", + fmt.Sprintf("config/samples/crd_v1alpha1_databaserequest_%s.yaml", name), + ) + _, err = utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("validating that the DatabaseRequest resource is created") + cmd = exec.Command( + "kubectl", + "wait", + "--for=condition=Ready", + "databaserequest", + fmt.Sprintf("databaserequest-%s-sample", name), + "--timeout=60s", + ) + _, err = utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + // verify that the service and secret got created + By("validating that the service is created") + cmd = exec.Command( + "kubectl", + "get", + "service", + "-n", "default", + "-l", fmt.Sprintf("app.kubernetes.io/instance=databaserequest-%s-sample", name), + ) + serviceOutput, err := utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + serviceNames := utils.GetNonEmptyLines(string(serviceOutput)) + ExpectWithOffset(1, serviceNames).Should(HaveLen(2)) + ExpectWithOffset(1, serviceNames[1]).Should(ContainSubstring(fmt.Sprintf("first-%s-db", name))) + + By("validating that the secret is created") + cmd = exec.Command( + "kubectl", + "get", + "secret", + "-n", "default", + "-l", fmt.Sprintf("app.kubernetes.io/instance=databaserequest-%s-sample", name), + ) + secretOutput, err := utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + secretNames := utils.GetNonEmptyLines(string(secretOutput)) + ExpectWithOffset(1, secretNames).Should(HaveLen(2)) + + By("deleting the DatabaseRequest resource the database is getting deprovisioned") + cmd = exec.Command( + "kubectl", + "delete", + "databaserequest", + fmt.Sprintf("databaserequest-%s-sample", name), + ) + _, err = utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("validating that the service is deleted") + cmd = exec.Command( + "kubectl", + "get", + "service", + "-n", "default", + "-l", fmt.Sprintf("app.kubernetes.io/instance=databaserequest-%s-sample", name), + ) + serviceOutput, err = utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + serviceNames = utils.GetNonEmptyLines(string(serviceOutput)) + ExpectWithOffset(1, serviceNames).Should(HaveLen(1)) + + By("validating that the secret is deleted") + cmd = exec.Command( + "kubectl", + "get", + "secret", + "-n", "default", + "-l", fmt.Sprintf("app.kubernetes.io/instance=databaserequest-%s-sample", name), + ) + secretOutput, err = utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + secretNames = utils.GetNonEmptyLines(string(secretOutput)) + ExpectWithOffset(1, secretNames).Should(HaveLen(1)) + } // TODO(marco): maybe add a test connecting to the mysql database... diff --git a/test/e2e/testdata/mongodb.yaml b/test/e2e/testdata/mongodb.yaml new file mode 100644 index 0000000..e42e548 --- /dev/null +++ b/test/e2e/testdata/mongodb.yaml @@ -0,0 +1,46 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: mongo +--- +apiVersion: v1 +kind: Pod +metadata: + name: mongo + namespace: mongo + labels: + app: mongo +spec: + containers: + - name: mongo + image: mongo:7 + env: + - name: MONGO_INITDB_ROOT_USERNAME + value: "root" + - name: MONGO_INITDB_ROOT_PASSWORD + value: "e2e-mongo-password" + ports: + - containerPort: 27017 + name: mongo +--- +apiVersion: v1 +kind: Service +metadata: + name: mongo-service + namespace: mongo +spec: + selector: + app: mongo + ports: + - protocol: TCP + port: 27017 + targetPort: 27017 +--- +apiVersion: v1 +kind: Secret +metadata: + name: mongo-secret + namespace: mongo +type: Opaque +data: + password: ZTJlLW1vbmdvLXBhc3N3b3Jk \ No newline at end of file diff --git a/test/e2e/testdata/mysql-client-pod.yaml b/test/e2e/testdata/mysql-client-pod.yaml new file mode 100644 index 0000000..0bb69e6 --- /dev/null +++ b/test/e2e/testdata/mysql-client-pod.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: mysql-init-pod + namespace: mysql +spec: + restartPolicy: Never + containers: + - name: mysql-client + image: mysql:8 # change to mysql:5.7 if you want to test with MySQL 5.7 but remember + # that the MySQL 5.7 image is is not multi-arch and will not work on ARM64 + # out of the box + command: ["sh", "-c"] + args: + - | + mysql -h mysql-service.mysql -uroot -pe2e-mysql-password -e "CREATE DATABASE IF NOT EXISTS seed-database;" \ No newline at end of file diff --git a/test/e2e/testdata/mysql.yaml b/test/e2e/testdata/mysql.yaml index c2af64d..e3e0f39 100644 --- a/test/e2e/testdata/mysql.yaml +++ b/test/e2e/testdata/mysql.yaml @@ -18,7 +18,7 @@ spec: # out of the box env: - name: MYSQL_ROOT_PASSWORD - value: "e2e-test-password" + value: "e2e-mysql-password" ports: - containerPort: 3306 name: mysql @@ -43,4 +43,4 @@ metadata: namespace: mysql type: Opaque data: - password: ZTJlLXRlc3QtcGFzc3dvcmQ= \ No newline at end of file + password: ZTJlLW15c3FsLXBhc3N3b3Jk \ No newline at end of file diff --git a/test/e2e/testdata/postgre-client-pod.yaml b/test/e2e/testdata/postgre-client-pod.yaml new file mode 100644 index 0000000..3b87bd5 --- /dev/null +++ b/test/e2e/testdata/postgre-client-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: postgres-init-pod + namespace: postgres +spec: + restartPolicy: Never + containers: + - name: postgres-client + image: postgres:13 + command: ["sh", "-c"] + args: + - | + PGPASSWORD=e2e-postgres-password psql -h postgres-service.postgres -U postgres -c "CREATE DATABASE IF NOT EXISTS seed-database;" \ No newline at end of file diff --git a/test/e2e/testdata/postgres.yaml b/test/e2e/testdata/postgres.yaml new file mode 100644 index 0000000..375442b --- /dev/null +++ b/test/e2e/testdata/postgres.yaml @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: postgres +--- +apiVersion: v1 +kind: Pod +metadata: + name: postgres + namespace: postgres + labels: + app: postgres +spec: + containers: + - name: postgres + image: postgres:13 + env: + - name: POSTGRES_PASSWORD + value: "e2e-postgres-password" + ports: + - containerPort: 5432 + name: postgres +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres-service + namespace: postgres +spec: + selector: + app: postgres + ports: + - protocol: TCP + port: 5432 + targetPort: 5432 +--- +apiVersion: v1 +kind: Secret +metadata: + name: postgres-secret + namespace: postgres +type: Opaque +data: + password: ZTJlLXBvc3RncmVzLXBhc3N3b3Jk \ No newline at end of file diff --git a/test/e2e/testdata/seed-secret.yaml b/test/e2e/testdata/seed-secret.yaml new file mode 100644 index 0000000..8280836 --- /dev/null +++ b/test/e2e/testdata/seed-secret.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Secret +metadata: + name: mysql-seed-secret + namespace: default +type: Opaque +data: + #database: seed-database + database: c2VlZC1kYXRhYmFzZQ== + #password: seed-password + password: c2VlZC1wYXNzd29yZA== + #username: seed-username + username: c2VlZC11c2VybmFtZQ== + #host mysql-service.mysql + host: bXlzc2VydmljZS5teXNxbC5kYg== + #port 3306 + port: MzMwNg== + diff --git a/test/utils/utils.go b/test/utils/utils.go index c66eac2..d831099 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -33,13 +33,87 @@ const ( certmanagerVersion = "v1.5.3" certmanagerURLTmpl = "https://github.com/jetstack/cert-manager/releases/download/%s/cert-manager.yaml" - mysqlYaml = "test/e2e/testdata/mysql.yaml" + mysqlYaml = "test/e2e/testdata/mysql.yaml" + postgresYaml = "test/e2e/testdata/postgres.yaml" ) func warnError(err error) { fmt.Fprintf(GinkgoWriter, "warning: %v\n", err) } +// InstallRelationalDatabases installs both MySQL and PostgreSQL pods to be used for testing. +func InstallRelationalDatabases() error { + dir, err := GetProjectDir() + if err != nil { + return err + } + errChan := make(chan error, 2) + for _, yaml := range []string{mysqlYaml, postgresYaml} { + cmd := exec.Command("kubectl", "apply", "-f", yaml) + cmd.Dir = dir + fmt.Fprintf(GinkgoWriter, "running: %s in directory: %s\n", strings.Join(cmd.Args, " "), dir) + go func() { + _, err := Run(cmd) + errChan <- err + }() + } + for i := 0; i < 2; i++ { + if err := <-errChan; err != nil { + return err + } + } + return nil +} + +// InstallMongoDB installs a MongoDB pod to be used for testing. +func InstallMongoDB() error { + dir, err := GetProjectDir() + if err != nil { + return err + } + cmd := exec.Command("kubectl", "apply", "-f", "test/e2e/testdata/mongodb.yaml") + cmd.Dir = dir + fmt.Fprintf(GinkgoWriter, "running: %s in directory: %s\n", strings.Join(cmd.Args, " "), dir) + _, err = Run(cmd) + return err +} + +// UninstallRelationalDatabases uninstalls both MySQL and PostgreSQL pods. +func UninstallRelationalDatabases() { + dir, err := GetProjectDir() + if err != nil { + warnError(err) + } + errChan := make(chan error, 2) + for _, yaml := range []string{mysqlYaml, postgresYaml} { + cmd := exec.Command("kubectl", "delete", "-f", yaml) + cmd.Dir = dir + fmt.Fprintf(GinkgoWriter, "running: %s in directory: %s\n", strings.Join(cmd.Args, " "), dir) + go func() { + _, err := Run(cmd) + errChan <- err + }() + } + for i := 0; i < 2; i++ { + if err := <-errChan; err != nil { + warnError(err) + } + } +} + +// UninstallMongoDB uninstalls the MongoDB pod. +func UninstallMongoDB() { + dir, err := GetProjectDir() + if err != nil { + warnError(err) + } + cmd := exec.Command("kubectl", "delete", "-f", "test/e2e/testdata/mongodb.yaml") + cmd.Dir = dir + if _, err := Run(cmd); err != nil { + warnError(err) + } +} + // InstallMySQL installs a MySQL pod to be used for testing. func InstallMySQL() error { dir, err := GetProjectDir()