mirror of
https://github.com/clastix/kamaji.git
synced 2026-02-14 10:00:02 +00:00
feat: kine
This commit is contained in:
committed by
Dario Tranchitella
parent
8f59de6e13
commit
9e3173676e
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
54
controllers/storage.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
31
deploy/mysql/Makefile
Normal 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
43
deploy/mysql/README.md
Normal 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
18
deploy/mysql/ca-csr.json
Normal 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
18
deploy/mysql/config.json
Normal 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
61
deploy/mysql/kine.yaml
Normal 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
77
deploy/mysql/mariadb.yaml
Normal 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
|
||||
5
deploy/mysql/mysql-ssl.cnf
Normal file
5
deploy/mysql/mysql-ssl.cnf
Normal 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
|
||||
19
deploy/mysql/server-csr.json
Normal file
19
deploy/mysql/server-csr.json
Normal 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"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
1
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
131
internal/resources/sql_certificate.go
Normal file
131
internal/resources/sql_certificate.go
Normal 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
|
||||
}
|
||||
250
internal/resources/sql_setup.go
Normal file
250
internal/resources/sql_setup.go
Normal 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
|
||||
}
|
||||
114
internal/resources/sql_storage_config.go
Normal file
114
internal/resources/sql_storage_config.go
Normal 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())
|
||||
}
|
||||
}
|
||||
7
internal/sql/constants.go
Normal file
7
internal/sql/constants.go
Normal 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
173
internal/sql/mysql.go
Normal 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
101
internal/sql/sql.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
4
main.go
4
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"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user