diff --git a/api/v1alpha1/tenantcontrolplane_types.go b/api/v1alpha1/tenantcontrolplane_types.go index 2f865bf..5e61708 100644 --- a/api/v1alpha1/tenantcontrolplane_types.go +++ b/api/v1alpha1/tenantcontrolplane_types.go @@ -184,9 +184,34 @@ type ETCDStatus struct { User etcd.User `json:"user,omitempty"` } +type SQLCertificateStatus struct { + SecretName string `json:"secretName,omitempty"` + ResourceVersion string `json:"resourceVersion,omitempty"` + LastUpdate metav1.Time `json:"lastUpdate,omitempty"` +} + +type SQLConfigStatus struct { + SecretName string `json:"secretName,omitempty"` + ResourceVersion string `json:"resourceVersion,omitempty"` +} + +type SQLSetup struct { + Schema string `json:"schema,omitempty"` + User string `json:"user,omitempty"` + LastUpdate metav1.Time `json:"lastUpdate,omitempty"` + SQLConfigResourceVersion string `json:"sqlConfigResourceVersion,omitempty"` +} + +type KineMySQLStatus struct { + Config SQLConfigStatus `json:"config,omitempty"` + Setup SQLSetup `json:"setup,omitempty"` + Certificate SQLCertificateStatus `json:"certificate,omitempty"` +} + // StorageStatus defines the observed state of StorageStatus. type StorageStatus struct { - ETCD *ETCDStatus `json:"etcd,omitempty"` + ETCD *ETCDStatus `json:"etcd,omitempty"` + KineMySQL *KineMySQLStatus `json:"kineMySQL,omitempty"` } // TenantControlPlaneKubeconfigsStatus contains information about a the generated kubeconfig. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 0036b4f..fc40903 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -313,6 +313,24 @@ func (in *IngressSpec) DeepCopy() *IngressSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KineMySQLStatus) DeepCopyInto(out *KineMySQLStatus) { + *out = *in + out.Config = in.Config + in.Setup.DeepCopyInto(&out.Setup) + in.Certificate.DeepCopyInto(&out.Certificate) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KineMySQLStatus. +func (in *KineMySQLStatus) DeepCopy() *KineMySQLStatus { + if in == nil { + return nil + } + out := new(KineMySQLStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KonnectivitySpec) DeepCopyInto(out *KonnectivitySpec) { *out = *in @@ -593,6 +611,53 @@ func (in *PublicKeyPrivateKeyPairStatus) DeepCopy() *PublicKeyPrivateKeyPairStat return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SQLCertificateStatus) DeepCopyInto(out *SQLCertificateStatus) { + *out = *in + in.LastUpdate.DeepCopyInto(&out.LastUpdate) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SQLCertificateStatus. +func (in *SQLCertificateStatus) DeepCopy() *SQLCertificateStatus { + if in == nil { + return nil + } + out := new(SQLCertificateStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SQLConfigStatus) DeepCopyInto(out *SQLConfigStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SQLConfigStatus. +func (in *SQLConfigStatus) DeepCopy() *SQLConfigStatus { + if in == nil { + return nil + } + out := new(SQLConfigStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SQLSetup) DeepCopyInto(out *SQLSetup) { + *out = *in + in.LastUpdate.DeepCopyInto(&out.LastUpdate) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SQLSetup. +func (in *SQLSetup) DeepCopy() *SQLSetup { + if in == nil { + return nil + } + out := new(SQLSetup) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceSpec) DeepCopyInto(out *ServiceSpec) { *out = *in @@ -617,6 +682,11 @@ func (in *StorageStatus) DeepCopyInto(out *StorageStatus) { *out = new(ETCDStatus) (*in).DeepCopyInto(*out) } + if in.KineMySQL != nil { + in, out := &in.KineMySQL, &out.KineMySQL + *out = new(KineMySQLStatus) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StorageStatus. diff --git a/config/crd/bases/kamaji.clastix.io_tenantcontrolplanes.yaml b/config/crd/bases/kamaji.clastix.io_tenantcontrolplanes.yaml index 58c745b..846882a 100644 --- a/config/crd/bases/kamaji.clastix.io_tenantcontrolplanes.yaml +++ b/config/crd/bases/kamaji.clastix.io_tenantcontrolplanes.yaml @@ -1186,6 +1186,38 @@ spec: - name type: object type: object + kineMySQL: + properties: + certificate: + properties: + lastUpdate: + format: date-time + type: string + resourceVersion: + type: string + secretName: + type: string + type: object + config: + properties: + resourceVersion: + type: string + secretName: + type: string + type: object + setup: + properties: + lastUpdate: + format: date-time + type: string + schema: + type: string + sqlConfigResourceVersion: + type: string + user: + type: string + type: object + type: object type: object type: object type: object diff --git a/config/install.yaml b/config/install.yaml index df17d52..25dd8fa 100644 --- a/config/install.yaml +++ b/config/install.yaml @@ -978,6 +978,38 @@ spec: - name type: object type: object + kineMySQL: + properties: + certificate: + properties: + lastUpdate: + format: date-time + type: string + resourceVersion: + type: string + secretName: + type: string + type: object + config: + properties: + resourceVersion: + type: string + secretName: + type: string + type: object + setup: + properties: + lastUpdate: + format: date-time + type: string + schema: + type: string + sqlConfigResourceVersion: + type: string + user: + type: string + type: object + type: object type: object type: object type: object diff --git a/controllers/resources.go b/controllers/resources.go index a5830f9..9f19118 100644 --- a/controllers/resources.go +++ b/controllers/resources.go @@ -14,6 +14,7 @@ import ( kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1" "github.com/clastix/kamaji/internal/resources" "github.com/clastix/kamaji/internal/resources/konnectivity" + "github.com/clastix/kamaji/internal/sql" "github.com/clastix/kamaji/internal/types" ) @@ -26,6 +27,7 @@ type GroupResourceBuilderConfiguration struct { log logr.Logger tcpReconcilerConfig TenantControlPlaneReconcilerConfig tenantControlPlane kamajiv1alpha1.TenantControlPlane + DBConnection sql.DBConnection } type GroupDeleteableResourceBuilderConfiguration struct { @@ -33,6 +35,7 @@ type GroupDeleteableResourceBuilderConfiguration struct { log logr.Logger tcpReconcilerConfig TenantControlPlaneReconcilerConfig tenantControlPlane kamajiv1alpha1.TenantControlPlane + DBConnection sql.DBConnection } // GetResources returns a list of resources that will be used to provide tenant control planes @@ -54,7 +57,7 @@ func getDefaultResources(config GroupResourceBuilderConfiguration) []resources.R resources = append(resources, getKubeadmConfigResources(config.client, config.tcpReconcilerConfig, config.tenantControlPlane)...) resources = append(resources, getKubernetesCertificatesResources(config.client, config.log, config.tcpReconcilerConfig, config.tenantControlPlane)...) resources = append(resources, getKubeconfigResources(config.client, config.log, config.tcpReconcilerConfig, config.tenantControlPlane)...) - resources = append(resources, getKubernetesStorageResources(config.client, config.log, config.tcpReconcilerConfig, config.tenantControlPlane)...) + resources = append(resources, getKubernetesStorageResources(config.client, config.log, config.tcpReconcilerConfig, config.DBConnection, config.tenantControlPlane)...) resources = append(resources, getInternalKonnectivityResources(config.client, config.log, config.tcpReconcilerConfig, config.tenantControlPlane)...) resources = append(resources, getKubernetesDeploymentResources(config.client, config.tcpReconcilerConfig, config.tenantControlPlane)...) resources = append(resources, getKubernetesIngressResources(config.client, config.tenantControlPlane)...) @@ -66,15 +69,28 @@ func getDefaultResources(config GroupResourceBuilderConfiguration) []resources.R } func getDefaultDeleteableResources(config GroupDeleteableResourceBuilderConfiguration) []resources.DeleteableResource { - return []resources.DeleteableResource{ - &resources.ETCDSetupResource{ - Name: "etcd-setup", - Client: config.client, - Log: config.log, - ETCDClientCertsSecret: getNamespacedName(config.tcpReconcilerConfig.ETCDClientSecretNamespace, config.tcpReconcilerConfig.ETCDClientSecretName), - ETCDCACertsSecret: getNamespacedName(config.tcpReconcilerConfig.ETCDCASecretNamespace, config.tcpReconcilerConfig.ETCDCASecretName), - Endpoints: getArrayFromString(config.tcpReconcilerConfig.ETCDEndpoints), - }, + switch config.tcpReconcilerConfig.ETCDStorageType { + case types.ETCD: + return []resources.DeleteableResource{ + &resources.ETCDSetupResource{ + Name: "etcd-setup", + Client: config.client, + Log: config.log, + ETCDClientCertsSecret: getNamespacedName(config.tcpReconcilerConfig.ETCDClientSecretNamespace, config.tcpReconcilerConfig.ETCDClientSecretName), + ETCDCACertsSecret: getNamespacedName(config.tcpReconcilerConfig.ETCDCASecretNamespace, config.tcpReconcilerConfig.ETCDCASecretName), + Endpoints: getArrayFromString(config.tcpReconcilerConfig.ETCDEndpoints), + }, + } + case types.KineMySQL: + return []resources.DeleteableResource{ + &resources.SQLSetup{ + Client: config.client, + Name: "sql-setup", + DBConnection: config.DBConnection, + }, + } + default: + return []resources.DeleteableResource{} } } @@ -179,7 +195,7 @@ func getKubeconfigResources(c client.Client, log logr.Logger, tcpReconcilerConfi } } -func getKubernetesStorageResources(c client.Client, log logr.Logger, tcpReconcilerConfig TenantControlPlaneReconcilerConfig, tenantControlPlane kamajiv1alpha1.TenantControlPlane) []resources.Resource { +func getKubernetesStorageResources(c client.Client, log logr.Logger, tcpReconcilerConfig TenantControlPlaneReconcilerConfig, dbConnection sql.DBConnection, tenantControlPlane kamajiv1alpha1.TenantControlPlane) []resources.Resource { switch tcpReconcilerConfig.ETCDStorageType { case types.ETCD: return []resources.Resource{ @@ -204,6 +220,27 @@ func getKubernetesStorageResources(c client.Client, log logr.Logger, tcpReconcil Endpoints: getArrayFromString(tcpReconcilerConfig.ETCDEndpoints), }, } + case types.KineMySQL: + return []resources.Resource{ + &resources.SQLStorageConfig{ + Client: c, + Name: "sql-config", + Host: dbConnection.GetHost(), + Port: dbConnection.GetPort(), + }, + &resources.SQLSetup{ + Client: c, + Name: "sql-setup", + DBConnection: dbConnection, + }, + &resources.SQLCertificate{ + Client: c, + Name: "sql-certificate", + StorageType: tcpReconcilerConfig.ETCDStorageType, + SQLConfigSecretName: tcpReconcilerConfig.KineMySQLSecretName, + SQLConfigSecretNamespace: tcpReconcilerConfig.KineMySQLSecretNamespace, + }, + } default: return []resources.Resource{} } @@ -215,6 +252,7 @@ func getKubernetesDeploymentResources(c client.Client, tcpReconcilerConfig Tenan Client: c, ETCDEndpoints: getArrayFromString(tcpReconcilerConfig.ETCDEndpoints), ETCDCompactionInterval: tcpReconcilerConfig.ETCDCompactionInterval, + ETCDStorageType: tcpReconcilerConfig.ETCDStorageType, }, } } diff --git a/controllers/storage.go b/controllers/storage.go new file mode 100644 index 0000000..9e56fff --- /dev/null +++ b/controllers/storage.go @@ -0,0 +1,54 @@ +package controllers + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + + corev1 "k8s.io/api/core/v1" + k8stypes "k8s.io/apimachinery/pkg/types" + + "github.com/clastix/kamaji/internal/sql" + "github.com/clastix/kamaji/internal/types" +) + +func (r *TenantControlPlaneReconciler) getStorageConnection(ctx context.Context) (sql.DBConnection, error) { + // TODO: https://github.com/clastix/kamaji/issues/67 + switch r.Config.ETCDStorageType { + case types.KineMySQL: + secret := &corev1.Secret{} + namespacedName := k8stypes.NamespacedName{Namespace: r.Config.KineMySQLSecretNamespace, Name: r.Config.KineMySQLSecretName} + if err := r.Client.Get(ctx, namespacedName, secret); err != nil { + return nil, err + } + + rootCAs := x509.NewCertPool() + if ok := rootCAs.AppendCertsFromPEM(secret.Data["ca.crt"]); !ok { + return nil, fmt.Errorf("error creating root ca for mysql db connector") + } + + certificate, err := tls.X509KeyPair(secret.Data["server.crt"], secret.Data["server.key"]) + if err != nil { + return nil, err + } + + return sql.GetDBConnection( + sql.ConnectionConfig{ + SQLDriver: sql.MySQL, + User: "root", + Password: string(secret.Data["MYSQL_ROOT_PASSWORD"]), + Host: r.Config.KineMySQLHost, + Port: r.Config.KineMySQLPort, + DBName: "mysql", + TLSConfig: &tls.Config{ + ServerName: r.Config.KineMySQLHost, + RootCAs: rootCAs, + Certificates: []tls.Certificate{certificate}, + }, + }, + ) + default: + return nil, nil + } +} diff --git a/controllers/tenantcontrolplane_controller.go b/controllers/tenantcontrolplane_controller.go index 2389d60..9c9a458 100644 --- a/controllers/tenantcontrolplane_controller.go +++ b/controllers/tenantcontrolplane_controller.go @@ -21,6 +21,7 @@ import ( kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1" kamajierrors "github.com/clastix/kamaji/internal/errors" "github.com/clastix/kamaji/internal/resources" + "github.com/clastix/kamaji/internal/sql" "github.com/clastix/kamaji/internal/types" ) @@ -45,6 +46,11 @@ type TenantControlPlaneReconcilerConfig struct { ETCDEndpoints string ETCDCompactionInterval string TmpBaseDirectory string + DBConnection sql.DBConnection + KineMySQLSecretName string + KineMySQLSecretNamespace string + KineMySQLHost string + KineMySQLPort int } //+kubebuilder:rbac:groups=kamaji.clastix.io,resources=tenantcontrolplanes,verbs=get;list;watch;create;update;patch;delete @@ -75,12 +81,25 @@ func (r *TenantControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R return ctrl.Result{}, nil } + dbConnection, err := r.getStorageConnection(ctx) + if err != nil { + return ctrl.Result{}, err + } + defer func() { + // TODO: Currently, etcd is not accessed using this dbConnection. For that reason we need this check + // Check: https://github.com/clastix/kamaji/issues/67 + if dbConnection != nil { + dbConnection.Close() + } + }() + if markedToBeDeleted { groupDeleteableResourceBuilderConfiguration := GroupDeleteableResourceBuilderConfiguration{ client: r.Client, log: log, tcpReconcilerConfig: r.Config, tenantControlPlane: *tenantControlPlane, + DBConnection: dbConnection, } registeredDeleteableResources := GetDeleteableResources(groupDeleteableResourceBuilderConfiguration) @@ -108,6 +127,7 @@ func (r *TenantControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R log: log, tcpReconcilerConfig: r.Config, tenantControlPlane: *tenantControlPlane, + DBConnection: dbConnection, } registeredResources := GetResources(groupResourceBuilderConfiguration) diff --git a/deploy/README.md b/deploy/README.md index b53f0eb..bc81e23 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -14,3 +14,8 @@ make -C kind make -C etcd ``` +## Multi-tenant MySQL-MariaDB cluster + +> This assumes you already have a running Kubernetes cluster and kubeconfig. + +Read [this](./mysql/README.md) in order to know more about. diff --git a/deploy/mysql/Makefile b/deploy/mysql/Makefile new file mode 100644 index 0000000..b7fccc5 --- /dev/null +++ b/deploy/mysql/Makefile @@ -0,0 +1,31 @@ +mariadb_path := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST))))) + +.PHONY: mariadb mariadb-certificates mariadb-secrets + +mariadb: mariadb-certificates mariadb-secrets mariadb-deployment + +mariadb-certificates: + rm -rf $(mariadb_path)/certs && mkdir $(mariadb_path)/certs + cfssl gencert -initca $(mariadb_path)/ca-csr.json | cfssljson -bare $(mariadb_path)/certs/ca + @mv $(mariadb_path)/certs/ca.pem $(mariadb_path)/certs/ca.crt + @mv $(mariadb_path)/certs/ca-key.pem $(mariadb_path)/certs/ca.key + cfssl gencert -ca=$(mariadb_path)/certs/ca.crt -ca-key=$(mariadb_path)/certs/ca.key \ + -config=$(mariadb_path)/config.json -profile=server \ + $(mariadb_path)/server-csr.json | cfssljson -bare $(mariadb_path)/certs/server + @mv $(mariadb_path)/certs/server.pem $(mariadb_path)/certs/server.crt + @mv $(mariadb_path)/certs/server-key.pem $(mariadb_path)/certs/server.key + chmod 644 $(mariadb_path)/certs/* + +mariadb-secrets: + @kubectl -n kamaji-system create secret generic mysql-config \ + --from-file=$(mariadb_path)/certs/ca.crt --from-file=$(mariadb_path)/certs/ca.key \ + --from-file=$(mariadb_path)/certs/server.key --from-file=$(mariadb_path)/certs/server.crt \ + --from-file=$(mariadb_path)/mysql-ssl.cnf \ + --from-literal=MYSQL_ROOT_PASSWORD=root + +mariadb-deployment: + @kubectl -n kamaji-system apply -f $(mariadb_path)/mariadb.yaml + +destroy: + @kubectl delete -n kamaji-system -f $(mariadb_path)/mariadb.yaml + @kubectl delete -n kamaji-system secret mysql-config diff --git a/deploy/mysql/README.md b/deploy/mysql/README.md new file mode 100644 index 0000000..36e68ec --- /dev/null +++ b/deploy/mysql/README.md @@ -0,0 +1,43 @@ +# MySQL as Kubernetes Storage + +Kamaji offers the possibility of having a different storage system than `ETCD` thanks to [kine](https://github.com/k3s-io/kine). One of the implementations is [MySQL](https://www.mysql.com/). + +Kamaji project is developed using [kind](https://kind.sigs.k8s.io), therefore, MySQL (or [MariaDB](https://mariadb.org/) in this case) will be deployed into the local kubernetes cluster in order to be used as storage for the tenants. + +There is a Makefile to help with the process: + +* **Full Installation** + +```bash +$ make mariadb +``` + +This action will perform all the necessary stuffs to have MariaDB as kubernetes storage backend using kine. + +* **Certificate creation** + +```bash +$ make mariadb-certificates +``` + +Communication between kine and the backend is encrypted, therefore, some certificates must be created. + +* **Secret Deployment** + +```bash +$ make mariadb-secrets +``` + +Previous certificates and MySQL configuration have to be available in order to be used. They will be under the secret `kamaji-system:mysql-config`. + +* **Deployment** + +```bash +$ make mariadb-deployment +``` + +* **Uninstall Everything** + +```bash +$ make destroy +``` \ No newline at end of file diff --git a/deploy/mysql/ca-csr.json b/deploy/mysql/ca-csr.json new file mode 100644 index 0000000..f6902ff --- /dev/null +++ b/deploy/mysql/ca-csr.json @@ -0,0 +1,18 @@ +{ + "CN": "Clastix CA", + "key": { + "algo": "rsa", + "size": 2048 + }, + "names": [ + { + "C": "IT", + "ST": "Italy", + "L": "Milan" + } + ], + "hosts": [ + "127.0.0.1", + "localhost" + ] +} diff --git a/deploy/mysql/config.json b/deploy/mysql/config.json new file mode 100644 index 0000000..d45a072 --- /dev/null +++ b/deploy/mysql/config.json @@ -0,0 +1,18 @@ +{ + "signing": { + "default": { + "expiry": "8760h" + }, + "profiles": { + "server": { + "expiry": "8760h", + "usages": [ + "signing", + "key encipherment", + "server auth", + "client auth" + ] + } + } + } +} \ No newline at end of file diff --git a/deploy/mysql/kine.yaml b/deploy/mysql/kine.yaml new file mode 100644 index 0000000..b1c2fb3 --- /dev/null +++ b/deploy/mysql/kine.yaml @@ -0,0 +1,61 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kine-tenant + namespace: +--- +apiVersion: v1 +kind: Service +metadata: + name: kine-tenant + namespace: +spec: + type: ClusterIP + ports: + - name: server + port: 2379 + protocol: TCP + targetPort: 2379 + selector: + app: kine-tenant +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kine-tenant + labels: + app: kine-tenant + namespace: +spec: + selector: + matchLabels: + app: kine-tenant + replicas: 1 + template: + metadata: + name: kine-tenant + labels: + app: kine-tenant + spec: + serviceAccountName: kine-tenant + volumes: + - name: certs + secret: + secretName: mysql-certs + containers: + - name: kine-tenant + image: rancher/kine:v0.9.2-amd64 + ports: + - containerPort: 2379 + name: server + volumeMounts: + - name: certs + mountPath: /kine + env: + - name: GODEBUG + value: "x509ignoreCN=0" + args: + - --endpoint=mysql://tenant1:tenant1@tcp(mysql:3306)/tenant1 + - --ca-file=/kine/ca.crt + - --cert-file=/kine/server.crt + - --key-file=/kine/server.key diff --git a/deploy/mysql/mariadb.yaml b/deploy/mysql/mariadb.yaml new file mode 100644 index 0000000..50c89ed --- /dev/null +++ b/deploy/mysql/mariadb.yaml @@ -0,0 +1,77 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: mariadb + namespace: +--- +apiVersion: v1 +kind: Service +metadata: + name: mariadb + namespace: +spec: + type: ClusterIP + ports: + - name: server + port: 3306 + protocol: TCP + targetPort: 3306 + selector: + app: mariadb +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mariadb + labels: + app: mariadb + namespace: +spec: + selector: + matchLabels: + app: mariadb + replicas: 1 + template: + metadata: + name: mariadb + labels: + app: mariadb + spec: + serviceAccountName: mariadb + volumes: + - name: certs + secret: + secretName: mysql-config + - name: data + persistentVolumeClaim: + claimName: pvc-mariadb + containers: + - name: mariadb + image: mariadb:10.7.4 + ports: + - containerPort: 3306 + name: server + volumeMounts: + - name: data + mountPath: /var/lib/mariadb + - name: certs + mountPath: /etc/mysql/conf.d/ + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-config + key: MYSQL_ROOT_PASSWORD +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: pvc-mariadb + namespace: +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi + storageClassName: standard diff --git a/deploy/mysql/mysql-ssl.cnf b/deploy/mysql/mysql-ssl.cnf new file mode 100644 index 0000000..37599ef --- /dev/null +++ b/deploy/mysql/mysql-ssl.cnf @@ -0,0 +1,5 @@ +[mysqld] +ssl-ca=/etc/mysql/conf.d/ca.crt +ssl-cert=/etc/mysql/conf.d/server.crt +ssl-key=/etc/mysql/conf.d/server.key +require_secure_transport=ON diff --git a/deploy/mysql/server-csr.json b/deploy/mysql/server-csr.json new file mode 100644 index 0000000..bc6a4ee --- /dev/null +++ b/deploy/mysql/server-csr.json @@ -0,0 +1,19 @@ +{ + "CN": "mariadb.kamaji-system.svc.cluster.local", + "key": { + "algo": "rsa", + "size": 2048 + }, + "hosts": [ + "127.0.0.1", + "localhost", + "mariadb", + "mariadb.kamaji-system", + "mariadb.kamaji-system.svc", + "mariadb.kamaji-system.svc.cluster.local", + "mysql", + "mysql.kamaji-system", + "mysql.kamaji-system.svc", + "mysql.kamaji-system.svc.cluster.local" + ] +} diff --git a/docs/getting-started-with-kamaji.md b/docs/getting-started-with-kamaji.md index 16c0338..e82f933 100644 --- a/docs/getting-started-with-kamaji.md +++ b/docs/getting-started-with-kamaji.md @@ -38,6 +38,51 @@ At this moment you will have your KinD up and running and ETCD cluster in multit Now you're ready to install Kamaji operator. +#### Kine MySQL + +Kamaji offers the possibility of using a different storage system than `ETCD` for the tenants, like MySQL compatible databases. + +Once a compatible-mysql database is running, we need to provide information about it to kamaji: + +``` +--etcd-storage-type=kine-mysql +--kine-mysql-host= +--kine-mysql-port= +--kine-mysql-secret-name= +--kine-mysql-secret-name= +``` + +The secret with the configuration and certificates for mysql should look like: +```yaml +apiVersion: v1 +data: + MYSQL_ROOT_PASSWORD: ... + ca.crt: ... + ca.key: ... + mysql-ssl.cnf: ... + server.crt: ... + server.key: ... +kind: Secret +metadata: + creationTimestamp: "2022-06-30T08:03:15Z" + name: mysql-config + namespace: kamaji-system + resourceVersion: "32228" + uid: 51b155a1-426c-42d2-8147-be680bf458a6 +type: Opaque +``` + +and `mysql-ssl.cnf`: +``` +[mysqld] +ssl-ca=/etc/mysql/conf.d/ca.crt +ssl-cert=/etc/mysql/conf.d/server.crt +ssl-key=/etc/mysql/conf.d/server.key +require_secure_transport=ON +``` + +You can read more about it [here](../deploy/mysql/README.md) + ### Install Kamaji ```bash diff --git a/docs/reference.md b/docs/reference.md index 358b107..f91c5b0 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -22,7 +22,12 @@ Available flags are the following: --etcd-client-secret-namespace Name of the namespace where the secret which contains ETCD client certificates is. (default: "kamaji") --etcd-compaction-interval ETCD Compaction interval (i.e. "5m0s"). (default: "0" (disabled)) --etcd-endpoints Comma-separated list with ETCD endpoints (i.e. https://etcd-0.etcd.kamaji.svc.cluster.local,https://etcd-1.etcd.kamaji.svc.cluster.local,https://etcd-2.etcd.kamaji.svc.cluster.local) +--etcd-storage-type ETCD Storage type (i.e. "etcd", "kine-mysql"). (default: "etcd") --health-probe-bind-address string The address the probe endpoint binds to. (default ":8081") +--kine-mysql-host Host where MySQL is running (default: "localhost") +--kine-mysql-port int Port where MySQL is running (default: 3306) +--kine-mysql-secret-name Name of the secret where the necessary configuration and certificates are. (default: "mysql-config") +--kine-mysql-secret-name Name of the namespace of the secret where the necessary configuration and certificates are. (default: "kamaji-system") --kubeconfig string Paths to a kubeconfig. Only required if out-of-cluster. --leader-elect Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager. --metrics-bind-address string The address the metric endpoint binds to. (default ":8080") @@ -43,9 +48,14 @@ Available environment variables are: | `KAMAJI_ETCD_CLIENT_SECRET_NAMESPACE` | Name of the namespace where the secret which contains ETCD client certificates is. (default: "kamaji") | | `KAMAJI_ETCD_COMPACTION_INTERVAL` | ETCD Compaction interval (i.e. "5m0s"). (default: "0" (disabled)) | | `KAMAJI_ETCD_ENDPOINTS` | Comma-separated list with ETCD endpoints (i.e. etcd-server-1:2379,etcd-server-2:2379). (default: "etcd-server:2379") | +| `KAMAJI_ETCD_STORAGE_TYPE` | ETCD Storage type (i.e. "etcd", "kine-mysql"). (default: "etcd") | | `KAMAJI_ETCD_SERVERS` | Comma-separated list with ETCD servers (i.e. etcd-0.etcd.kamaji.svc.cluster.local,etcd-1.etcd.kamaji.svc.cluster.local,etcd-2.etcd.kamaji.svc.cluster.local) | | `KAMAJI_METRICS_BIND_ADDRESS` | The address the metric endpoint binds to. (default ":8080") | | `KAMAJI_HEALTH_PROBE_BIND_ADDRESS` | The address the probe endpoint binds to. (default ":8081") | +| `KAMAJI_KINE_MYSQL_HOST` | Host where MySQL is running(default "localhost") | +| `KAMAJI_KINE_MYSQL_PORT` | Port where MySQL is running (default: 3306) | +| `KAMAJI_KINE_MYSQL_SECRET_NAME` | Name of the secret where the necessary configuration and certificates are. (default: "mysql-config") | +| `KAMAJI_KINE_MYSQL_SECRET_NAMESPACE` | Name of the namespace of the secret where the necessary configuration and certificates are. (default: "kamaji-system") | | `KAMAJI_LEADER_ELECTION` | Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager. | | `KAMAJI_TMP_DIRECTORY` | Directory which will be used to work with temporary files. (default "/tmp/kamaji") | diff --git a/go.mod b/go.mod index e809a10..82fabfe 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.18 require ( github.com/go-logr/logr v1.2.0 + github.com/go-sql-driver/mysql v1.6.0 github.com/google/uuid v1.3.0 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.17.0 diff --git a/helm/kamaji/crds/tenantcontrolplane.yaml b/helm/kamaji/crds/tenantcontrolplane.yaml index 58c745b..846882a 100644 --- a/helm/kamaji/crds/tenantcontrolplane.yaml +++ b/helm/kamaji/crds/tenantcontrolplane.yaml @@ -1186,6 +1186,38 @@ spec: - name type: object type: object + kineMySQL: + properties: + certificate: + properties: + lastUpdate: + format: date-time + type: string + resourceVersion: + type: string + secretName: + type: string + type: object + config: + properties: + resourceVersion: + type: string + secretName: + type: string + type: object + setup: + properties: + lastUpdate: + format: date-time + type: string + schema: + type: string + sqlConfigResourceVersion: + type: string + user: + type: string + type: object + type: object type: object type: object type: object diff --git a/internal/config/config.go b/internal/config/config.go index efab01d..63f49ec 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -30,6 +30,10 @@ const ( defaultETCDClientSecretName = "root-client-certs" defaultETCDClientSecretNamespace = "kamaji-system" defaultTmpDirectory = "/tmp/kamaji" + defaultKineMySQLSecretName = "mysql-config" + defaultKineMySQLSecretNamespace = "kamaji-system" + defaultKineMySQLHost = "localhost" + defaultKineMySQLPort = 3306 ) func InitConfig() (*viper.Viper, error) { @@ -49,6 +53,10 @@ func InitConfig() (*viper.Viper, error) { flag.String("etcd-endpoints", defaultETCDEndpoints, "Comma-separated list with ETCD endpoints (i.e. https://etcd-0.etcd.kamaji-system.svc.cluster.local,https://etcd-1.etcd.kamaji-system.svc.cluster.local,https://etcd-2.etcd.kamaji-system.svc.cluster.local)") flag.String("etcd-compaction-interval", defaultETCDCompactionInterval, "ETCD Compaction interval (i.e. \"5m0s\"). (default: \"0\" (disabled))") flag.String("tmp-directory", defaultTmpDirectory, "Directory which will be used to work with temporary files.") + flag.String("kine-mysql-secret-name", defaultKineMySQLSecretName, "Name of the secret which contains MySQL (Kine) configuration.") + flag.String("kine-mysql-secret-namespace", defaultKineMySQLSecretNamespace, "Name of the namespace where the secret which contains MySQL (Kine) configuration.") + flag.String("kine-mysql-host", defaultKineMySQLHost, "Host where MySQL (Kine) is working") + flag.Int("kine-mysql-port", defaultKineMySQLPort, "Port where MySQL (Kine) is working") // Setup zap configuration opts := zap.Options{ @@ -100,6 +108,18 @@ func InitConfig() (*viper.Viper, error) { if err := config.BindEnv("tmp-directory", fmt.Sprintf("%s_TMP_DIRECTORY", envPrefix)); err != nil { return nil, err } + if err := config.BindEnv("kine-mysql-secret-name", fmt.Sprintf("%s_KINE_MYSQL_SECRET_NAME", envPrefix)); err != nil { + return nil, err + } + if err := config.BindEnv("kine-mysql-secret-namespace", fmt.Sprintf("%s_KINE_MYSQL_SECRET_NAMESPACE", envPrefix)); err != nil { + return nil, err + } + if err := config.BindEnv("kine-mysql-host", fmt.Sprintf("%s_KINE_MYSQL_HOST", envPrefix)); err != nil { + return nil, err + } + if err := config.BindEnv("kine-mysql-port", fmt.Sprintf("%s_KINE_MYSQL_PORT", envPrefix)); err != nil { + return nil, err + } // Setup config file if cfgFile != "" { diff --git a/internal/resources/k8s_deployment_resource.go b/internal/resources/k8s_deployment_resource.go index 608bf94..706d4ba 100644 --- a/internal/resources/k8s_deployment_resource.go +++ b/internal/resources/k8s_deployment_resource.go @@ -5,10 +5,8 @@ package resources import ( "context" - "crypto/md5" "fmt" "path" - "sort" "strings" "github.com/pkg/errors" @@ -16,7 +14,6 @@ import ( corev1 "k8s.io/api/core/v1" quantity "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - apimachinerytypes "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3" "k8s.io/kubernetes/cmd/kubeadm/app/constants" @@ -76,32 +73,6 @@ func (r *KubernetesDeploymentResource) Define(ctx context.Context, tenantControl return nil } -// secretHashValue function returns the md5 value for the given secret: -// this will trigger a new rollout in case of value change. -func (r *KubernetesDeploymentResource) secretHashValue(ctx context.Context, namespace, name string) (string, error) { - secret := &corev1.Secret{} - - if err := r.Client.Get(ctx, apimachinerytypes.NamespacedName{Namespace: namespace, Name: name}, secret); err != nil { - return "", errors.Wrap(err, "cannot retrieve *corev1.Secret for resource version retrieval") - } - // Go access map values in random way, it means we have to sort them - keys := make([]string, 0, len(secret.Data)) - - for k := range secret.Data { - keys = append(keys, k) - } - - sort.Strings(keys) - // Generating MD5 of Secret values, sorted by key - h := md5.New() - - for _, key := range keys { - h.Write(secret.Data[key]) - } - - return fmt.Sprintf("%x", h.Sum(nil)), nil -} - func (r *KubernetesDeploymentResource) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) { maxSurge := intstr.FromString("100%") @@ -129,42 +100,42 @@ func (r *KubernetesDeploymentResource) CreateOrUpdate(ctx context.Context, tenan Labels: map[string]string{ "kamaji.clastix.io/soot": tenantControlPlane.GetName(), "component.kamaji.clastix.io/api-server-certificate": func() (hash string) { - hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.APIServer.SecretName) + hash, _ = utilities.SecretHashValue(ctx, r.Client, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.APIServer.SecretName) return }(), "component.kamaji.clastix.io/api-server-kubelet-client-certificate": func() (hash string) { - hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.APIServerKubeletClient.SecretName) + hash, _ = utilities.SecretHashValue(ctx, r.Client, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.APIServerKubeletClient.SecretName) return }(), "component.kamaji.clastix.io/ca": func() (hash string) { - hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.CA.SecretName) + hash, _ = utilities.SecretHashValue(ctx, r.Client, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.CA.SecretName) return }(), "component.kamaji.clastix.io/controller-manager-kubeconfig": func() (hash string) { - hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.KubeConfig.ControllerManager.SecretName) + hash, _ = utilities.SecretHashValue(ctx, r.Client, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.KubeConfig.ControllerManager.SecretName) return }(), "component.kamaji.clastix.io/front-proxy-ca-certificate": func() (hash string) { - hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.FrontProxyCA.SecretName) + hash, _ = utilities.SecretHashValue(ctx, r.Client, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.FrontProxyCA.SecretName) return }(), "component.kamaji.clastix.io/front-proxy-client-certificate": func() (hash string) { - hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.FrontProxyClient.SecretName) + hash, _ = utilities.SecretHashValue(ctx, r.Client, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.FrontProxyClient.SecretName) return }(), "component.kamaji.clastix.io/service-account": func() (hash string) { - hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.SA.SecretName) + hash, _ = utilities.SecretHashValue(ctx, r.Client, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.SA.SecretName) return }(), "component.kamaji.clastix.io/scheduler-kubeconfig": func() (hash string) { - hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.KubeConfig.Scheduler.SecretName) + hash, _ = utilities.SecretHashValue(ctx, r.Client, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.KubeConfig.Scheduler.SecretName) return }(), @@ -267,6 +238,7 @@ func (r *KubernetesDeploymentResource) CreateOrUpdate(ctx context.Context, tenan fmt.Sprintf("--client-ca-file=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.CACertName)), fmt.Sprintf("--enable-admission-plugins=%s", strings.Join(tenantControlPlane.Spec.Kubernetes.AdmissionControllers.ToSlice(), ",")), "--enable-bootstrap-token-auth=true", + fmt.Sprintf("--etcd-servers=%s", strings.Join(r.ETCDEndpoints, ",")), fmt.Sprintf("--service-cluster-ip-range=%s", tenantControlPlane.Spec.NetworkProfile.ServiceCIDR), fmt.Sprintf("--kubelet-client-certificate=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.APIServerKubeletClientCertName)), fmt.Sprintf("--kubelet-client-key=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.APIServerKubeletClientKeyName)), @@ -748,6 +720,8 @@ func (r *KubernetesDeploymentResource) customizeStorage(ctx context.Context, pod switch r.ETCDStorageType { case types.ETCD: r.customizeETCDStorage(ctx, podTemplate, tenantControlPlane) + case types.KineMySQL: + r.customizeKineMySQLStorage(ctx, podTemplate, tenantControlPlane) default: return } @@ -756,12 +730,12 @@ func (r *KubernetesDeploymentResource) customizeStorage(ctx context.Context, pod func (r *KubernetesDeploymentResource) customizeETCDStorage(ctx context.Context, podTemplate *corev1.PodTemplateSpec, tenantControlPlane kamajiv1alpha1.TenantControlPlane) { labels := map[string]string{ "component.kamaji.clastix.io/etcd-ca-certificates": func() (hash string) { - hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.ETCD.CA.SecretName) + hash, _ = utilities.SecretHashValue(ctx, r.Client, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.ETCD.CA.SecretName) return }(), "component.kamaji.clastix.io/etcd-certificates": func() (hash string) { - hash, _ = r.secretHashValue(ctx, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.ETCD.APIServer.SecretName) + hash, _ = utilities.SecretHashValue(ctx, r.Client, tenantControlPlane.GetNamespace(), tenantControlPlane.Status.Certificates.ETCD.APIServer.SecretName) return }(), @@ -771,11 +745,11 @@ func (r *KubernetesDeploymentResource) customizeETCDStorage(ctx context.Context, utilities.MergeMaps(labels, podTemplate.Labels), ) - commands := []string{fmt.Sprintf("--etcd-compaction-interval=%s", r.ETCDCompactionInterval), + commands := []string{ + fmt.Sprintf("--etcd-compaction-interval=%s", r.ETCDCompactionInterval), fmt.Sprintf("--etcd-cafile=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.EtcdCACertName)), fmt.Sprintf("--etcd-certfile=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.APIServerEtcdClientCertName)), fmt.Sprintf("--etcd-keyfile=%s", path.Join(v1beta3.DefaultCertificatesDir, constants.APIServerEtcdClientKeyName)), - fmt.Sprintf("--etcd-servers=%s", strings.Join(r.ETCDEndpoints, ",")), fmt.Sprintf("--etcd-prefix=/%s", tenantControlPlane.GetName()), } @@ -802,3 +776,60 @@ func (r *KubernetesDeploymentResource) customizeETCDStorage(ctx context.Context, podTemplate.Spec.Volumes[0].VolumeSource.Projected.Sources = append(podTemplate.Spec.Volumes[0].VolumeSource.Projected.Sources, volumeProjections...) } + +func (r *KubernetesDeploymentResource) customizeKineMySQLStorage(ctx context.Context, podTemplate *corev1.PodTemplateSpec, tenantControlPlane kamajiv1alpha1.TenantControlPlane) { + volume := corev1.Volume{ + Name: "mysql-config", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: tenantControlPlane.Status.Storage.KineMySQL.Certificate.SecretName, + DefaultMode: pointer.Int32Ptr(420), + }, + }, + } + + podTemplate.Spec.Volumes = append(podTemplate.Spec.Volumes, volume) + + container := corev1.Container{ + Name: "kine", + // TODO: parameter. + Image: fmt.Sprintf("%s:%s", "rancher/kine", "v0.9.2-amd64"), + Args: []string{ + "--endpoint=mysql://$(MYSQL_USER):$(MYSQL_PASSWORD)@tcp($(MYSQL_HOST):$(MYSQL_PORT))/$(MYSQL_SCHEMA)", + "--ca-file=/kine/ca.crt", + "--cert-file=/kine/server.crt", + "--key-file=/kine/server.key", + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: volume.Name, + MountPath: "/kine", + ReadOnly: true, + }, + }, + Env: []corev1.EnvVar{ + {Name: "GODEBUG", Value: "x509ignoreCN=0"}, + }, + EnvFrom: []corev1.EnvFromSource{ + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: tenantControlPlane.Status.Storage.KineMySQL.Config.SecretName, + }, + }, + }, + }, + Ports: []corev1.ContainerPort{ + { + ContainerPort: 2379, + Name: "server", + Protocol: corev1.ProtocolTCP, + }, + }, + TerminationMessagePath: "/dev/termination-log", + TerminationMessagePolicy: "File", + ImagePullPolicy: corev1.PullIfNotPresent, + } + + podTemplate.Spec.Containers = append(podTemplate.Spec.Containers, container) +} diff --git a/internal/resources/resource.go b/internal/resources/resource.go index a70e307..0a442f3 100644 --- a/internal/resources/resource.go +++ b/internal/resources/resource.go @@ -73,7 +73,7 @@ func Handle(ctx context.Context, resource Resource, tenantControlPlane *kamajiv1 return controllerutil.OperationResultNone, err } -// HandleDeletion handles the deletion of the given resource +// HandleDeletion handles the deletion of the given resource. func HandleDeletion(ctx context.Context, resource DeleteableResource, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error { if err := resource.Define(ctx, tenantControlPlane); err != nil { return err diff --git a/internal/resources/sql_certificate.go b/internal/resources/sql_certificate.go new file mode 100644 index 0000000..2d9a98e --- /dev/null +++ b/internal/resources/sql_certificate.go @@ -0,0 +1,131 @@ +// Copyright 2022 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package resources + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8stypes "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1" + "github.com/clastix/kamaji/internal/types" + "github.com/clastix/kamaji/internal/utilities" +) + +type SQLCertificate struct { + resource *corev1.Secret + Client client.Client + Name string + StorageType types.ETCDStorageType + SQLConfigSecretName string + SQLConfigSecretNamespace string +} + +func (r *SQLCertificate) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool { + return tenantControlPlane.Status.Storage.KineMySQL.Certificate.SecretName != r.resource.GetName() || + tenantControlPlane.Status.Storage.KineMySQL.Certificate.ResourceVersion != r.resource.ResourceVersion +} + +func (r *SQLCertificate) ShouldCleanup(plane *kamajiv1alpha1.TenantControlPlane) bool { + return false +} + +func (r *SQLCertificate) CleanUp(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (bool, error) { + return false, nil +} + +func (r *SQLCertificate) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error { + r.resource = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: r.getPrefixedName(tenantControlPlane), + Namespace: tenantControlPlane.GetNamespace(), + }, + Data: map[string][]byte{}, + } + + return nil +} + +func (r *SQLCertificate) getPrefixedName(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) string { + return utilities.AddTenantPrefix(r.Name, tenantControlPlane) +} + +func (r *SQLCertificate) GetClient() client.Client { + return r.Client +} + +func (r *SQLCertificate) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) { + return controllerutil.CreateOrUpdate(ctx, r.Client, r.resource, r.mutate(ctx, tenantControlPlane)) +} + +func (r *SQLCertificate) GetName() string { + return r.Name +} + +func (r *SQLCertificate) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error { + if tenantControlPlane.Status.Storage.KineMySQL == nil { + tenantControlPlane.Status.Storage.KineMySQL = &kamajiv1alpha1.KineMySQLStatus{} + } + + tenantControlPlane.Status.Storage.KineMySQL.Certificate.SecretName = r.resource.GetName() + tenantControlPlane.Status.Storage.KineMySQL.Certificate.ResourceVersion = r.resource.ResourceVersion + tenantControlPlane.Status.Storage.KineMySQL.Certificate.LastUpdate = metav1.Now() + + return nil +} + +func (r *SQLCertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn { + return func() error { + sqlConfig := &corev1.Secret{} + namespacedName := k8stypes.NamespacedName{Namespace: r.SQLConfigSecretNamespace, Name: r.SQLConfigSecretName} + if err := r.Client.Get(ctx, namespacedName, sqlConfig); err != nil { + return err + } + + if err := r.buildSecret(ctx, *sqlConfig); err != nil { + return err + } + + r.resource.SetLabels(utilities.MergeMaps( + utilities.KamajiLabels(), + r.resource.GetLabels(), + map[string]string{ + "kamaji.clastix.io/name": tenantControlPlane.GetName(), + "kamaji.clastix.io/component": r.GetName(), + }, + )) + + return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme()) + } +} + +func (r *SQLCertificate) buildSecret(ctx context.Context, sqlConfig corev1.Secret) error { + switch r.StorageType { + case types.KineMySQL: + keys := []string{"ca.crt", "server.crt", "server.key"} + + return r.buildKineSecret(ctx, keys, sqlConfig) + default: + return fmt.Errorf("storage type %s is not implemented", r.StorageType) + } +} + +func (r *SQLCertificate) buildKineSecret(ctx context.Context, keys []string, sqlConfig corev1.Secret) error { + for _, key := range keys { + value, ok := sqlConfig.Data[key] + if !ok { + return fmt.Errorf("%s is not in sql config secret", key) + } + + r.resource.Data[key] = value + } + + return nil +} diff --git a/internal/resources/sql_setup.go b/internal/resources/sql_setup.go new file mode 100644 index 0000000..b0da574 --- /dev/null +++ b/internal/resources/sql_setup.go @@ -0,0 +1,250 @@ +// Copyright 2022 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package resources + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1" + "github.com/clastix/kamaji/internal/sql" +) + +const ( + secretHashLabelKey = "component.kamaji.clastix.io/secret-hash" +) + +type sqlSetupResource struct { + schema string + user string + password string +} + +type SQLSetup struct { + resource sqlSetupResource + Client client.Client + DBConnection sql.DBConnection + Name string +} + +func (r *SQLSetup) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool { + if tenantControlPlane.Status.Storage.KineMySQL == nil { + return true + } + + return tenantControlPlane.Status.Storage.KineMySQL.Setup.Schema != r.resource.schema || + tenantControlPlane.Status.Storage.KineMySQL.Setup.User != r.resource.user || + tenantControlPlane.Status.Storage.KineMySQL.Setup.SQLConfigResourceVersion != tenantControlPlane.Status.Storage.KineMySQL.Config.ResourceVersion +} + +func (r *SQLSetup) ShouldCleanup(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool { + return false +} + +func (r *SQLSetup) CleanUp(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (bool, error) { + return false, nil +} + +func (r *SQLSetup) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error { + secret := &corev1.Secret{} + namespacedName := types.NamespacedName{ + Namespace: tenantControlPlane.GetNamespace(), + Name: tenantControlPlane.Status.Storage.KineMySQL.Config.SecretName, + } + if err := r.Client.Get(ctx, namespacedName, secret); err != nil { + return err + } + + r.resource = sqlSetupResource{ + schema: string(secret.Data["MYSQL_SCHEMA"]), + user: string(secret.Data["MYSQL_USER"]), + password: string(secret.Data["MYSQL_PASSWORD"]), + } + + return nil +} + +func (r *SQLSetup) GetClient() client.Client { + return r.Client +} + +func (r *SQLSetup) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) { + if tenantControlPlane.Status.Storage.KineMySQL.Setup.SQLConfigResourceVersion != "" && + tenantControlPlane.Status.Storage.KineMySQL.Setup.SQLConfigResourceVersion != tenantControlPlane.Status.Storage.KineMySQL.Config.ResourceVersion { + if err := r.Delete(ctx, tenantControlPlane); err != nil { + return controllerutil.OperationResultNone, err + } + + return controllerutil.OperationResultUpdated, nil + } + + reconcilationResult := controllerutil.OperationResultNone + var operationResult controllerutil.OperationResult + var err error + + operationResult, err = r.createDB(ctx, tenantControlPlane) + if err != nil { + return reconcilationResult, err + } + reconcilationResult = updateOperationResult(reconcilationResult, operationResult) + + operationResult, err = r.createUser(ctx, tenantControlPlane) + if err != nil { + return reconcilationResult, err + } + reconcilationResult = updateOperationResult(reconcilationResult, operationResult) + + operationResult, err = r.createGrantPrivileges(ctx, tenantControlPlane) + if err != nil { + return reconcilationResult, err + } + reconcilationResult = updateOperationResult(reconcilationResult, operationResult) + + return reconcilationResult, nil +} + +func (r *SQLSetup) GetName() string { + return r.Name +} + +func (r *SQLSetup) Delete(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error { + if err := r.Define(ctx, tenantControlPlane); err != nil { + return err + } + + if err := r.revokeGrantPrivileges(ctx, tenantControlPlane); err != nil { + return err + } + + if err := r.deleteDB(ctx, tenantControlPlane); err != nil { + return err + } + + if err := r.deleteUser(ctx, tenantControlPlane); err != nil { + return err + } + + return nil +} + +func (r *SQLSetup) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error { + if tenantControlPlane.Status.Storage.KineMySQL == nil { + return fmt.Errorf("sql configuration is not ready") + } + + tenantControlPlane.Status.Storage.KineMySQL.Setup.Schema = r.resource.schema + tenantControlPlane.Status.Storage.KineMySQL.Setup.User = r.resource.user + tenantControlPlane.Status.Storage.KineMySQL.Setup.LastUpdate = metav1.Now() + tenantControlPlane.Status.Storage.KineMySQL.Setup.SQLConfigResourceVersion = tenantControlPlane.Status.Storage.KineMySQL.Config.ResourceVersion + + return nil +} + +func (r *SQLSetup) createDB(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) { + exists, err := r.DBConnection.DBExists(ctx, r.resource.schema) + if err != nil { + return controllerutil.OperationResultNone, err + } + + if exists { + return controllerutil.OperationResultNone, nil + } + + if err := r.DBConnection.CreateDB(ctx, r.resource.schema); err != nil { + return controllerutil.OperationResultNone, err + } + + return controllerutil.OperationResultCreated, nil +} + +func (r *SQLSetup) deleteDB(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error { + exists, err := r.DBConnection.DBExists(ctx, r.resource.schema) + if err != nil { + return err + } + + if !exists { + return nil + } + + if err := r.DBConnection.DeleteDB(ctx, r.resource.schema); err != nil { + return err + } + + return nil +} + +func (r *SQLSetup) createUser(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) { + exists, err := r.DBConnection.UserExists(ctx, r.resource.user) + if err != nil { + return controllerutil.OperationResultNone, err + } + + if exists { + return controllerutil.OperationResultNone, nil + } + + if err := r.DBConnection.CreateUser(ctx, r.resource.user, r.resource.password); err != nil { + return controllerutil.OperationResultNone, err + } + + return controllerutil.OperationResultCreated, nil +} + +func (r *SQLSetup) deleteUser(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error { + exists, err := r.DBConnection.UserExists(ctx, r.resource.user) + if err != nil { + return err + } + + if !exists { + return nil + } + + if err := r.DBConnection.DeleteUser(ctx, r.resource.user); err != nil { + return err + } + + return nil +} + +func (r *SQLSetup) createGrantPrivileges(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) { + exists, err := r.DBConnection.GrantPrivilegesExists(ctx, r.resource.user, r.resource.schema) + if err != nil { + return controllerutil.OperationResultNone, err + } + + if exists { + return controllerutil.OperationResultNone, nil + } + + if err := r.DBConnection.GrantPrivileges(ctx, r.resource.user, r.resource.schema); err != nil { + return controllerutil.OperationResultNone, err + } + + return controllerutil.OperationResultCreated, nil +} + +func (r *SQLSetup) revokeGrantPrivileges(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error { + exists, err := r.DBConnection.GrantPrivilegesExists(ctx, r.resource.user, r.resource.schema) + if err != nil { + return err + } + + if !exists { + return nil + } + + if err := r.DBConnection.RevokePrivileges(ctx, r.resource.user, r.resource.schema); err != nil { + return err + } + + return nil +} diff --git a/internal/resources/sql_storage_config.go b/internal/resources/sql_storage_config.go new file mode 100644 index 0000000..f604f23 --- /dev/null +++ b/internal/resources/sql_storage_config.go @@ -0,0 +1,114 @@ +// Copyright 2022 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package resources + +import ( + "context" + "strconv" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1" + "github.com/clastix/kamaji/internal/utilities" +) + +type SQLStorageConfig struct { + resource *corev1.Secret + Client client.Client + Name string + Host string + Port int +} + +func (r *SQLStorageConfig) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool { + if tenantControlPlane.Status.Storage.KineMySQL == nil { + return true + } + + return tenantControlPlane.Status.Storage.KineMySQL.Config.SecretName != r.resource.GetName() || + tenantControlPlane.Status.Storage.KineMySQL.Config.ResourceVersion != r.resource.ResourceVersion +} + +func (r *SQLStorageConfig) ShouldCleanup(plane *kamajiv1alpha1.TenantControlPlane) bool { + return false +} + +func (r *SQLStorageConfig) CleanUp(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (bool, error) { + return false, nil +} + +func (r *SQLStorageConfig) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error { + r.resource = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: r.getPrefixedName(tenantControlPlane), + Namespace: tenantControlPlane.GetNamespace(), + }, + } + + return nil +} + +func (r *SQLStorageConfig) getPrefixedName(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) string { + return utilities.AddTenantPrefix(r.Name, tenantControlPlane) +} + +func (r *SQLStorageConfig) GetClient() client.Client { + return r.Client +} + +func (r *SQLStorageConfig) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) { + return controllerutil.CreateOrUpdate(ctx, r.Client, r.resource, r.mutate(ctx, tenantControlPlane)) +} + +func (r *SQLStorageConfig) GetName() string { + return r.Name +} + +func (r *SQLStorageConfig) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error { + if tenantControlPlane.Status.Storage.KineMySQL == nil { + tenantControlPlane.Status.Storage.KineMySQL = &kamajiv1alpha1.KineMySQLStatus{} + } + + tenantControlPlane.Status.Storage.KineMySQL.Config.SecretName = r.resource.GetName() + tenantControlPlane.Status.Storage.KineMySQL.Config.ResourceVersion = r.resource.ResourceVersion + + return nil +} + +func (r *SQLStorageConfig) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn { + return func() error { + calculatedHash := utilities.HashValue(*r.resource) + savedHash, ok := r.resource.GetLabels()[secretHashLabelKey] + + var password []byte + if ok && calculatedHash == savedHash { + password = r.resource.Data["MYSQL_PASSWORD"] + } else { + password = []byte(utilities.GenerateUUIDString()) + } + + r.resource.Data = map[string][]byte{ + "MYSQL_HOST": []byte(r.Host), + "MYSQL_PORT": []byte(strconv.Itoa(r.Port)), + "MYSQL_SCHEMA": []byte(tenantControlPlane.GetName()), + "MYSQL_USER": []byte(tenantControlPlane.GetName()), + "MYSQL_PASSWORD": password, + } + + r.resource.SetLabels(utilities.MergeMaps( + utilities.KamajiLabels(), + map[string]string{ + "kamaji.clastix.io/name": tenantControlPlane.GetName(), + "kamaji.clastix.io/component": r.GetName(), + secretHashLabelKey: utilities.HashValue(*r.resource), + }, + )) + + return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme()) + } +} diff --git a/internal/sql/constants.go b/internal/sql/constants.go new file mode 100644 index 0000000..efc9987 --- /dev/null +++ b/internal/sql/constants.go @@ -0,0 +1,7 @@ +package sql + +const ( + defaultProtocol = "tcp" + firstPort = 1024 + sqlErrorNoRows = "sql: no rows in result set" +) diff --git a/internal/sql/mysql.go b/internal/sql/mysql.go new file mode 100644 index 0000000..c904a65 --- /dev/null +++ b/internal/sql/mysql.go @@ -0,0 +1,173 @@ +package sql + +import ( + "context" + "database/sql" + "fmt" + + "github.com/go-sql-driver/mysql" +) + +const ( + mysqlFetchUserStatement = "SELECT User FROM mysql.user WHERE User= ? LIMIT 1" + mysqlFetchDBStatement = "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME= ? LIMIT 1" + mysqlShowGrantsStatement = "SHOW GRANTS FOR `%s`@`%%`" + mysqlCreateDBStatement = "CREATE DATABASE IF NOT EXISTS %s" + mysqlCreateUserStatement = "CREATE USER `%s`@`%%` IDENTIFIED BY '%s'" + mysqlGrantPrivilegesStatement = "GRANT ALL PRIVILEGES ON `%s`.* TO `%s`@`%%`" + mysqlDropDBStatement = "DROP DATABASE IF EXISTS `%s`" + mysqlDropUserStatement = "DROP USER IF EXISTS `%s`" + mysqlRevokePrivilegesStatement = "REVOKE ALL PRIVILEGES ON `%s`.* FROM `%s`" +) + +type MySQLConnection struct { + db *sql.DB + host string + port int +} + +func getMySQLDB(config ConnectionConfig) (DBConnection, error) { + tlsKey := "mysql" + dataSourceName := config.GetDataSourceName() + mysqlConfig, err := mysql.ParseDSN(dataSourceName) + if err != nil { + return nil, err + } + + if err := mysql.RegisterTLSConfig(tlsKey, config.TLSConfig); err != nil { + return nil, err + } + + mysqlConfig.DBName = config.DBName + mysqlConfig.TLSConfig = tlsKey + parsedDSN := mysqlConfig.FormatDSN() + + db, err := sql.Open("mysql", parsedDSN) + if err != nil { + return nil, err + } + + return &MySQLConnection{ + db: db, + host: config.Host, + port: config.Port, + }, nil +} + +func (c *MySQLConnection) GetHost() string { + return c.host +} + +func (c *MySQLConnection) GetPort() int { + return c.port +} + +func (c *MySQLConnection) Close() error { + return c.db.Close() +} + +func (c *MySQLConnection) Check() error { + return c.db.Ping() +} + +func (c *MySQLConnection) CreateUser(ctx context.Context, user, password string) error { + return c.mutate(ctx, mysqlCreateUserStatement, user, password) +} + +func (c *MySQLConnection) CreateDB(ctx context.Context, dbName string) error { + return c.mutate(ctx, mysqlCreateDBStatement, dbName) +} + +func (c *MySQLConnection) GrantPrivileges(ctx context.Context, user, dbName string) error { + return c.mutate(ctx, mysqlGrantPrivilegesStatement, user, dbName) +} + +func (c *MySQLConnection) UserExists(ctx context.Context, user string) (bool, error) { + checker := func(row *sql.Row) (bool, error) { + var name string + if err := row.Scan(&name); err != nil { + if checkEmptyQueryResult(err) { + return false, nil + } + + return false, err + } + + return name == user, nil + } + + return c.check(ctx, mysqlFetchUserStatement, checker, user) +} + +func (c *MySQLConnection) DBExists(ctx context.Context, dbName string) (bool, error) { + checker := func(row *sql.Row) (bool, error) { + var name string + if err := row.Scan(&name); err != nil { + if checkEmptyQueryResult(err) { + return false, nil + } + + return false, err + } + + return name == dbName, nil + } + + return c.check(ctx, mysqlFetchDBStatement, checker, dbName) +} + +func (c *MySQLConnection) GrantPrivilegesExists(ctx context.Context, user, dbName string) (bool, error) { + statementShowGrantsStatement := fmt.Sprintf(mysqlShowGrantsStatement, user) + rows, err := c.db.Query(statementShowGrantsStatement) + if err != nil { + return false, err + } + + expected := fmt.Sprintf(mysqlGrantPrivilegesStatement, user, dbName) + var grant string + + for rows.Next() { + if err = rows.Scan(&grant); err != nil { + return false, err + } + + if grant == expected { + return true, nil + } + } + + return false, nil +} + +func (c *MySQLConnection) DeleteUser(ctx context.Context, user string) error { + return c.mutate(ctx, mysqlDropUserStatement, user) +} + +func (c *MySQLConnection) DeleteDB(ctx context.Context, dbName string) error { + return c.mutate(ctx, mysqlDropDBStatement, dbName) +} + +func (c *MySQLConnection) RevokePrivileges(ctx context.Context, user, dbName string) error { + return c.mutate(ctx, mysqlRevokePrivilegesStatement, user, dbName) +} + +func (c *MySQLConnection) check(ctx context.Context, nonFilledStatement string, checker func(*sql.Row) (bool, error), args ...any) (bool, error) { + statement, err := c.db.Prepare(nonFilledStatement) + if err != nil { + return false, err + } + defer statement.Close() + + row := statement.QueryRowContext(ctx, args...) + + return checker(row) +} + +func (c *MySQLConnection) mutate(ctx context.Context, nonFilledStatement string, args ...any) error { + statement := fmt.Sprintf(nonFilledStatement, args...) + if _, err := c.db.ExecContext(ctx, statement); err != nil { + return err + } + + return nil +} diff --git a/internal/sql/sql.go b/internal/sql/sql.go new file mode 100644 index 0000000..a3bce24 --- /dev/null +++ b/internal/sql/sql.go @@ -0,0 +1,101 @@ +package sql + +import ( + "context" + "crypto/tls" + "fmt" + "net/url" +) + +type Driver int + +const ( + MySQL Driver = iota +) + +func (d Driver) ToString() string { + switch d { + case MySQL: + return "mysql" + default: + return "" + } +} + +type ConnectionConfig struct { + SQLDriver Driver + User string + Password string + Host string + Port int + DBName string + TLSConfig *tls.Config + Parameters map[string][]string +} + +func (config ConnectionConfig) GetDataSourceName() string { + userPassword := config.getDataSourceNameUserPassword() + db := config.getDataSourceNameDB() + dataSourceName := fmt.Sprintf("%s%s/%s?%s", userPassword, db, config.DBName, config.formatParameters()) + + return dataSourceName +} + +func (config ConnectionConfig) getDataSourceNameUserPassword() string { + if config.User == "" { + return "" + } + + if config.Password == "" { + return fmt.Sprintf("%s@", config.User) + } + + return fmt.Sprintf("%s:%s@", config.User, config.Password) +} + +func (config ConnectionConfig) getDataSourceNameDB() string { + if config.Host == "" || config.Port < firstPort { + return "" + } + + return fmt.Sprintf("%s(%s:%d)", defaultProtocol, config.Host, config.Port) +} + +func (config ConnectionConfig) formatParameters() string { + if len(config.Parameters) == 0 { + return "" + } + + values := url.Values(config.Parameters) + + return values.Encode() +} + +type DBConnection interface { + CreateUser(ctx context.Context, user, password string) error + CreateDB(ctx context.Context, dbName string) error + GrantPrivileges(ctx context.Context, user, dbName string) error + UserExists(ctx context.Context, user string) (bool, error) + DBExists(ctx context.Context, dbName string) (bool, error) + GrantPrivilegesExists(ctx context.Context, user, dbName string) (bool, error) + DeleteUser(ctx context.Context, user string) error + DeleteDB(ctx context.Context, dbName string) error + RevokePrivileges(ctx context.Context, user, dbName string) error + GetHost() string + GetPort() int + Close() error + Check() error +} + +func GetDBConnection(config ConnectionConfig) (DBConnection, error) { + switch config.SQLDriver { + case MySQL: + return getMySQLDB(config) + default: + return nil, fmt.Errorf("%s is not a valid driver", config.SQLDriver.ToString()) + } +} + +func checkEmptyQueryResult(err error) bool { + return err.Error() == sqlErrorNoRows +} diff --git a/internal/types/etcd_storage.go b/internal/types/etcd_storage.go index 301dd56..92ac651 100644 --- a/internal/types/etcd_storage.go +++ b/internal/types/etcd_storage.go @@ -4,19 +4,20 @@ type ETCDStorageType int const ( ETCD ETCDStorageType = iota + KineMySQL ) const ( defaultETCDStorageType = ETCD ) -var ( - etcdStorageTypeString = map[string]ETCDStorageType{ - "etcd": ETCD, - } -) +var etcdStorageTypeString = map[string]ETCDStorageType{"etcd": ETCD, "kine-mysql": KineMySQL} -// ParseETCDStorageType returns the ETCDStorageType given a string representation of the type +func (s ETCDStorageType) String() string { + return [...]string{"etcd", "kine-mysql"}[s] +} + +// ParseETCDStorageType returns the ETCDStorageType given a string representation of the type. func ParseETCDStorageType(s string) ETCDStorageType { if storageType, ok := etcdStorageTypeString[s]; ok { return storageType diff --git a/internal/utilities/utilities.go b/internal/utilities/utilities.go index 09a25c2..50eefdc 100644 --- a/internal/utilities/utilities.go +++ b/internal/utilities/utilities.go @@ -5,12 +5,20 @@ package utilities import ( "bytes" + "context" + "crypto/md5" "fmt" "net" "regexp" + "sort" + "github.com/google/uuid" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer/json" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1" "github.com/clastix/kamaji/internal/constants" @@ -79,3 +87,42 @@ func validateRegex(pattern string, value string) bool { return isFound } + +func GenerateUUID() uuid.UUID { + return uuid.New() +} + +func GenerateUUIDString() string { + return GenerateUUID().String() +} + +// SecretHashValue function returns the md5 value for the secret of the given name and namespace. +func SecretHashValue(ctx context.Context, client client.Client, namespace, name string) (string, error) { + secret := &corev1.Secret{} + + if err := client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, secret); err != nil { + return "", errors.Wrap(err, "cannot retrieve *corev1.Secret for resource version retrieval") + } + + return HashValue(*secret), nil +} + +// HashValue function returns the md5 value for the given secret. +func HashValue(secret corev1.Secret) string { + // Go access map values in random way, it means we have to sort them. + keys := make([]string, 0, len(secret.Data)) + + for k := range secret.Data { + keys = append(keys, k) + } + + sort.Strings(keys) + // Generating MD5 of Secret values, sorted by key + h := md5.New() + + for _, key := range keys { + h.Write(secret.Data[key]) + } + + return fmt.Sprintf("%x", h.Sum(nil)) +} diff --git a/kamaji.yaml b/kamaji.yaml index 77b3927..de348ef 100755 --- a/kamaji.yaml +++ b/kamaji.yaml @@ -1,3 +1,4 @@ +etcd-storage-type: etcd etcd-ca-secret-name: "etcd-certs" etcd-ca-secret-namespace: kamaji-system etcd-endpoints: https://etcd-0.etcd.kamaji-system.svc.cluster.local:2379,https://etcd-1.etcd.kamaji-system.svc.cluster.local:2379,https://etcd-2.etcd.kamaji-system.svc.cluster.local:2379 @@ -8,3 +9,7 @@ metrics-bind-address: :8080 health-probe-bind-address: :8081 leader-elect: false tmp-directory: /tmp/kamaji +kine-mysql-secret-name: "mysql-config" +kine-mysql-secret-namespace: kamaji-system +kine-mysql-host: localhost +kine-mysql-port: 3306 \ No newline at end of file diff --git a/main.go b/main.go index 8ed2379..107cb9b 100644 --- a/main.go +++ b/main.go @@ -85,6 +85,10 @@ func main() { ETCDEndpoints: conf.GetString("etcd-endpoints"), ETCDCompactionInterval: conf.GetString("etcd-compaction-interval"), TmpBaseDirectory: conf.GetString("tmp-directory"), + KineMySQLSecretName: conf.GetString("kine-mysql-secret-name"), + KineMySQLSecretNamespace: conf.GetString("kine-mysql-secret-namespace"), + KineMySQLHost: conf.GetString("kine-mysql-host"), + KineMySQLPort: conf.GetInt("kine-mysql-port"), }, }