feat: kine

This commit is contained in:
mendrugory
2022-06-23 12:00:54 +02:00
committed by Dario Tranchitella
parent 8f59de6e13
commit 9e3173676e
33 changed files with 1580 additions and 60 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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,
},
}
}

54
controllers/storage.go Normal file
View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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.

31
deploy/mysql/Makefile Normal file
View File

@@ -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

43
deploy/mysql/README.md Normal file
View File

@@ -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
```

18
deploy/mysql/ca-csr.json Normal file
View File

@@ -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"
]
}

18
deploy/mysql/config.json Normal file
View File

@@ -0,0 +1,18 @@
{
"signing": {
"default": {
"expiry": "8760h"
},
"profiles": {
"server": {
"expiry": "8760h",
"usages": [
"signing",
"key encipherment",
"server auth",
"client auth"
]
}
}
}
}

61
deploy/mysql/kine.yaml Normal file
View File

@@ -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

77
deploy/mysql/mariadb.yaml Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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"
]
}

View File

@@ -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=<database host>
--kine-mysql-port=<database port>
--kine-mysql-secret-name=<secret name>
--kine-mysql-secret-name=<secret namespace>
```
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

View File

@@ -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") |

1
go.mod
View File

@@ -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

View File

@@ -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

View File

@@ -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 != "" {

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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())
}
}

View File

@@ -0,0 +1,7 @@
package sql
const (
defaultProtocol = "tcp"
firstPort = 1024
sqlErrorNoRows = "sql: no rows in result set"
)

173
internal/sql/mysql.go Normal file
View File

@@ -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
}

101
internal/sql/sql.go Normal file
View File

@@ -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
}

View File

@@ -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

View File

@@ -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))
}

View File

@@ -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

View File

@@ -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"),
},
}