From 31b25f7c784a9b9e5f46a839d673ddf1b8e12927 Mon Sep 17 00:00:00 2001 From: Dario Tranchitella Date: Sat, 20 Aug 2022 11:32:51 +0200 Subject: [PATCH] feat: postgresql kine driver --- controllers/resources.go | 9 +- controllers/storage.go | 90 +++++++---- controllers/tenantcontrolplane_controller.go | 8 +- go.mod | 11 +- go.sum | 23 +++ internal/builders/controlplane/deployment.go | 152 +++++++++++++------ internal/config/config.go | 24 ++- internal/resources/sql_certificate.go | 41 +++-- internal/resources/sql_setup.go | 34 ++--- internal/resources/sql_storage_config.go | 48 +++--- internal/sql/mysql.go | 17 +++ internal/sql/postgresql.go | 126 +++++++++++++++ internal/sql/sql.go | 6 + internal/types/etcd_storage.go | 7 +- main.go | 8 +- 15 files changed, 444 insertions(+), 160 deletions(-) create mode 100644 internal/sql/postgresql.go diff --git a/controllers/resources.go b/controllers/resources.go index 59e9130..52c57ac 100644 --- a/controllers/resources.go +++ b/controllers/resources.go @@ -81,7 +81,7 @@ func getDefaultDeleteableResources(config GroupDeleteableResourceBuilderConfigur Endpoints: getArrayFromString(config.tcpReconcilerConfig.ETCDEndpoints), }, } - case types.KineMySQL: + case types.KineMySQL, types.KinePostgreSQL: return []resources.DeleteableResource{ &resources.SQLSetup{ Client: config.client, @@ -215,13 +215,14 @@ func getKubernetesStorageResources(c client.Client, log logr.Logger, tcpReconcil Endpoints: getArrayFromString(tcpReconcilerConfig.ETCDEndpoints), }, } - case types.KineMySQL: + case types.KineMySQL, types.KinePostgreSQL: return []resources.Resource{ &resources.SQLStorageConfig{ Client: c, Name: "sql-config", Host: dbConnection.GetHost(), Port: dbConnection.GetPort(), + Driver: dbConnection.Driver(), }, &resources.SQLSetup{ Client: c, @@ -232,8 +233,8 @@ func getKubernetesStorageResources(c client.Client, log logr.Logger, tcpReconcil Client: c, Name: "sql-certificate", StorageType: tcpReconcilerConfig.ETCDStorageType, - SQLConfigSecretName: tcpReconcilerConfig.KineMySQLSecretName, - SQLConfigSecretNamespace: tcpReconcilerConfig.KineMySQLSecretNamespace, + SQLConfigSecretName: tcpReconcilerConfig.KineSecretName, + SQLConfigSecretNamespace: tcpReconcilerConfig.KineSecretNamespace, }, } default: diff --git a/controllers/storage.go b/controllers/storage.go index 22c4753..6af5960 100644 --- a/controllers/storage.go +++ b/controllers/storage.go @@ -8,7 +8,9 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "strings" + "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" k8stypes "k8s.io/apimachinery/pkg/types" @@ -17,41 +19,67 @@ import ( ) func (r *TenantControlPlaneReconciler) getStorageConnection(ctx context.Context) (sql.DBConnection, error) { + var driver sql.Driver + var dbName string + // TODO: https://github.com/clastix/kamaji/issues/67 switch r.Config.ETCDStorageType { + case types.ETCD: + return nil, nil 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}, - }, - }, - ) + driver = sql.MySQL + dbName = "mysql" + case types.KinePostgreSQL: + driver = sql.PostgreSQL default: return nil, nil } + + secret := &corev1.Secret{} + namespacedName := k8stypes.NamespacedName{Namespace: r.Config.KineSecretNamespace, Name: r.Config.KineSecretName} + if err := r.Client.Get(ctx, namespacedName, secret); err != nil { + return nil, err + } + + if t := "kamaji.clastix.io/kine"; string(secret.Type) != t { + return nil, fmt.Errorf("expecting a secret of type %s", t) + } + + keys := []string{"ca.crt", "server.crt", "server.key", "username", "password"} + + if secret.Data == nil { + return nil, fmt.Errorf("the Kine secret %s/%s is missing all the required keys (%s)", secret.GetNamespace(), secret.GetName(), strings.Join(keys, ",")) + } + + for _, key := range keys { + if _, ok := secret.Data[key]; !ok { + return nil, fmt.Errorf("missing required key %s for the Kine secret %s/%s", key, secret.GetNamespace(), secret.GetName()) + } + } + + rootCAs := x509.NewCertPool() + if ok := rootCAs.AppendCertsFromPEM(secret.Data["ca.crt"]); !ok { + return nil, fmt.Errorf("error create root CA for the DB connector") + } + + certificate, err := tls.X509KeyPair(secret.Data["server.crt"], secret.Data["server.key"]) + if err != nil { + return nil, errors.Wrap(err, "cannot retrieve x.509 key pair from the Kine Secret") + } + + return sql.GetDBConnection( + sql.ConnectionConfig{ + SQLDriver: driver, + User: string(secret.Data["username"]), + Password: string(secret.Data["password"]), + Host: r.Config.KineHost, + Port: r.Config.KinePort, + DBName: dbName, + TLSConfig: &tls.Config{ + ServerName: r.Config.KineHost, + RootCAs: rootCAs, + Certificates: []tls.Certificate{certificate}, + }, + }, + ) } diff --git a/controllers/tenantcontrolplane_controller.go b/controllers/tenantcontrolplane_controller.go index 30bdcf4..8383dfa 100644 --- a/controllers/tenantcontrolplane_controller.go +++ b/controllers/tenantcontrolplane_controller.go @@ -47,10 +47,10 @@ type TenantControlPlaneReconcilerConfig struct { ETCDCompactionInterval string TmpBaseDirectory string DBConnection sql.DBConnection - KineMySQLSecretName string - KineMySQLSecretNamespace string - KineMySQLHost string - KineMySQLPort int + KineSecretName string + KineSecretNamespace string + KineHost string + KinePort int KineContainerImage string } diff --git a/go.mod b/go.mod index 82fabfe..8e38918 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,10 @@ go 1.18 require ( github.com/go-logr/logr v1.2.0 + github.com/go-pg/pg/v10 v10.10.6 github.com/go-sql-driver/mysql v1.6.0 github.com/google/uuid v1.3.0 + github.com/json-iterator/go v1.1.12 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.17.0 github.com/pkg/errors v0.9.1 @@ -70,6 +72,7 @@ require ( github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect + github.com/go-pg/zerochecker v0.2.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect @@ -84,8 +87,8 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/lithammer/dedent v1.1.0 // indirect github.com/magiconair/properties v1.8.5 // indirect @@ -117,6 +120,11 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/stretchr/testify v1.7.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect + github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect + github.com/vmihailenco/bufpool v0.1.11 // indirect + github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect + github.com/vmihailenco/tagparser v0.1.2 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.1 // indirect go.opencensus.io v0.23.0 // indirect @@ -148,6 +156,7 @@ require ( k8s.io/klog/v2 v2.60.1 // indirect k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect k8s.io/system-validators v1.6.0 // indirect + mellium.im/sasl v0.3.0 // indirect sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect sigs.k8s.io/kustomize/api v0.10.1 // indirect sigs.k8s.io/kustomize/kyaml v0.13.0 // indirect diff --git a/go.sum b/go.sum index 7763ed9..12c061d 100644 --- a/go.sum +++ b/go.sum @@ -413,6 +413,10 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-ozzo/ozzo-validation v3.5.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= +github.com/go-pg/pg/v10 v10.10.6 h1:1vNtPZ4Z9dWUw/TjJwOfFUbF5nEq1IkR6yG8Mq/Iwso= +github.com/go-pg/pg/v10 v10.10.6/go.mod h1:GLmFXufrElQHf5uzM3BQlcfwV3nsgnHue5uzjQ6Nqxg= +github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU= +github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= @@ -591,6 +595,8 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/ishidawataru/sctp v0.0.0-20190723014705-7c296d48a2b5/go.mod h1:DM4VvS+hD/kDi1U1QsX2fnZowwBhqD0Dk3bRPKF/Oc8= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -723,6 +729,7 @@ github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= @@ -909,6 +916,8 @@ github.com/testcontainers/testcontainers-go v0.13.0 h1:OUujSlEGsXVo/ykPVZk3KanBN github.com/testcontainers/testcontainers-go v0.13.0/go.mod h1:z1abufU633Eb/FmSBTzV6ntZAC1eZBYPtaFsn4nPuDk= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= @@ -920,6 +929,14 @@ github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:tw github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94= +github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= +github.com/vmihailenco/msgpack/v5 v5.3.4 h1:qMKAwOV+meBw2Y8k9cVwAy7qErtYCwBzZ2ellBfvnqc= +github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= +github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= @@ -995,6 +1012,7 @@ go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1010,6 +1028,7 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1249,6 +1268,7 @@ golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210923061019-b8560ed6a9b7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1626,6 +1646,9 @@ k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/ k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 h1:HNSDgDCrr/6Ly3WEGKZftiE7IY19Vz2GdbOCyI4qqhc= k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ= +mellium.im/sasl v0.3.0 h1:0qoaTCTo5Py7u/g0cBIQZcMOgG/5LM71nshbXwznBh8= +mellium.im/sasl v0.3.0/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw= modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= diff --git a/internal/builders/controlplane/deployment.go b/internal/builders/controlplane/deployment.go index 48af5a8..a332b72 100644 --- a/internal/builders/controlplane/deployment.go +++ b/internal/builders/controlplane/deployment.go @@ -40,11 +40,13 @@ const ( schedulerKubeconfig controllerManagerKubeconfig kineConfig + kineCerts ) const ( apiServerFlagsAnnotation = "kube-apiserver.kamaji.clastix.io/args" kineVolumeName = "kine-config" + kineVolumeCertName = "kine-certs" ) type Deployment struct { @@ -592,18 +594,37 @@ func (d *Deployment) buildKineVolume(podSpec *corev1.PodSpec, tcp *kamajiv1alpha podSpec.Volumes = volumes } - if d.ETCDStorageType == types.KineMySQL { + if found, index := utilities.HasNamedVolume(podSpec.Volumes, kineVolumeCertName); found { + var volumes []corev1.Volume + + volumes = append(volumes, podSpec.Volumes[:index]...) + volumes = append(volumes, podSpec.Volumes[index+1:]...) + + podSpec.Volumes = volumes + } + + if d.ETCDStorageType == types.KineMySQL || d.ETCDStorageType == types.KinePostgreSQL { if index := int(kineConfig) + 1; len(podSpec.Volumes) < index { podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{}) } - + // Adding the volume to read Kine certificates: + // these must be subsequently fixed with a chmod due to pg issues with private key. podSpec.Volumes[kineConfig].Name = kineVolumeName podSpec.Volumes[kineConfig].VolumeSource = corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ - SecretName: tcp.Status.Storage.KineMySQL.Certificate.SecretName, + SecretName: tcp.Status.Storage.Kine.Certificate.SecretName, DefaultMode: pointer.Int32Ptr(420), }, } + // Adding the Volume for the certificates with fixed permission + if index := int(kineCerts) + 1; len(podSpec.Volumes) < index { + podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{}) + } + + podSpec.Volumes[kineCerts].Name = kineVolumeCertName + podSpec.Volumes[kineCerts].VolumeSource = corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + } } } @@ -619,60 +640,95 @@ func (d *Deployment) buildKine(podSpec *corev1.PodSpec, tcp *kamajiv1alpha1.Tena podSpec.Containers = containers } + // In case of bare ETCD we exit without mangling the PodSpec resource. + if d.ETCDStorageType == types.ETCD { + return + } - if d.ETCDStorageType == types.KineMySQL { - if index := int(kineIndex) + 1; len(podSpec.Containers) < index { - podSpec.Containers = append(podSpec.Containers, corev1.Container{}) - } + if index := int(kineIndex) + 1; len(podSpec.Containers) < index { + podSpec.Containers = append(podSpec.Containers, corev1.Container{}) + } - args := map[string]string{} + args := map[string]string{} - if tcp.Spec.ControlPlane.Deployment.ExtraArgs != nil { - args = utilities.ArgsFromSliceToMap(tcp.Spec.ControlPlane.Deployment.ExtraArgs.Kine) - } + if tcp.Spec.ControlPlane.Deployment.ExtraArgs != nil { + args = utilities.ArgsFromSliceToMap(tcp.Spec.ControlPlane.Deployment.ExtraArgs.Kine) + } - args["--endpoint"] = "mysql://$(MYSQL_USER):$(MYSQL_PASSWORD)@tcp($(MYSQL_HOST):$(MYSQL_PORT))/$(MYSQL_SCHEMA)" - args["--ca-file"] = "/kine/ca.crt" - args["--cert-file"] = "/kine/server.crt" - args["--key-file"] = "/kine/server.key" + switch d.ETCDStorageType { + case types.KineMySQL: + args["--endpoint"] = "mysql://$(DB_USER):$(DB_PASSWORD)@tcp($(DB_HOST):$(DB_PORT))/$(DB_SCHEMA)" + case types.KinePostgreSQL: + args["--endpoint"] = "postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_SCHEMA)" + } - podSpec.Containers[kineIndex].Name = kineContainerName - podSpec.Containers[kineIndex].Image = d.KineContainerImage - podSpec.Containers[kineIndex].Command = []string{"/bin/kine"} - podSpec.Containers[kineIndex].Args = utilities.ArgsFromMapToSlice(args) - podSpec.Containers[kineIndex].VolumeMounts = []corev1.VolumeMount{ - { - Name: kineVolumeName, - MountPath: "/kine", - ReadOnly: true, + args["--ca-file"] = "/certs/ca.crt" + args["--cert-file"] = "/certs/server.crt" + args["--key-file"] = "/certs/server.key" + + podSpec.InitContainers = []corev1.Container{ + { + Name: "chmod", + Image: d.KineContainerImage, + ImagePullPolicy: corev1.PullAlways, + TerminationMessagePath: corev1.TerminationMessagePathDefault, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + Command: []string{"sh"}, + Args: []string{ + "-c", + "cp /kine/*.* /certs && chmod -R 600 /certs/*.*", }, - } - podSpec.Containers[kineIndex].TerminationMessagePath = corev1.TerminationMessagePathDefault - podSpec.Containers[kineIndex].TerminationMessagePolicy = corev1.TerminationMessageReadFile - podSpec.Containers[kineIndex].Env = []corev1.EnvVar{ - { - Name: "GODEBUG", - Value: "x509ignoreCN=0", - }, - } - podSpec.Containers[kineIndex].EnvFrom = []corev1.EnvFromSource{ - { - SecretRef: &corev1.SecretEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: tcp.Status.Storage.KineMySQL.Config.SecretName, - }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: kineVolumeName, + ReadOnly: true, + MountPath: "/kine", + }, + { + Name: kineVolumeCertName, + MountPath: "/certs", + ReadOnly: false, }, }, - } - podSpec.Containers[kineIndex].Ports = []corev1.ContainerPort{ - { - ContainerPort: 2379, - Name: "server", - Protocol: corev1.ProtocolTCP, - }, - } - podSpec.Containers[kineIndex].ImagePullPolicy = corev1.PullAlways + }, } + + podSpec.Containers[kineIndex].Name = kineContainerName + podSpec.Containers[kineIndex].Image = d.KineContainerImage + podSpec.Containers[kineIndex].Command = []string{"/bin/kine"} + podSpec.Containers[kineIndex].Args = utilities.ArgsFromMapToSlice(args) + podSpec.Containers[kineIndex].VolumeMounts = []corev1.VolumeMount{ + { + Name: kineVolumeCertName, + MountPath: "/certs", + ReadOnly: false, + }, + } + podSpec.Containers[kineIndex].TerminationMessagePath = corev1.TerminationMessagePathDefault + podSpec.Containers[kineIndex].TerminationMessagePolicy = corev1.TerminationMessageReadFile + podSpec.Containers[kineIndex].Env = []corev1.EnvVar{ + { + Name: "GODEBUG", + Value: "x509ignoreCN=0", + }, + } + podSpec.Containers[kineIndex].EnvFrom = []corev1.EnvFromSource{ + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: tcp.Status.Storage.Kine.Config.SecretName, + }, + }, + }, + } + podSpec.Containers[kineIndex].Ports = []corev1.ContainerPort{ + { + ContainerPort: 2379, + Name: "server", + Protocol: corev1.ProtocolTCP, + }, + } + podSpec.Containers[kineIndex].ImagePullPolicy = corev1.PullAlways } func (d *Deployment) SetSelector(deploymentSpec *appsv1.DeploymentSpec, tcp *kamajiv1alpha1.TenantControlPlane) { diff --git a/internal/config/config.go b/internal/config/config.go index 2bcaeaf..56df28c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -30,10 +30,8 @@ const ( defaultETCDClientSecretName = "root-client-certs" defaultETCDClientSecretNamespace = "kamaji-system" defaultTmpDirectory = "/tmp/kamaji" - defaultKineMySQLSecretName = "mysql-config" - defaultKineMySQLSecretNamespace = "kamaji-system" - defaultKineMySQLHost = "localhost" - defaultKineMySQLPort = 3306 + defaultKineSecretName = "kine-secret" + defaultKineSecretNamespace = "kamaji-system" defaultKineImage = "rancher/kine:v0.9.2-amd64" ) @@ -46,7 +44,7 @@ func InitConfig() (*viper.Viper, error) { flag.String("health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.Bool("leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") - flag.String("etcd-storage-type", defaultETCDStorageType, "Type of storage for ETCD (i.e etcd, kine-mysql, kine-postgres)") + flag.String("etcd-storage-type", defaultETCDStorageType, "Type of storage for ETCD (i.e etcd, kine-mysql, kine-psql)") flag.String("etcd-ca-secret-name", defaultETCDCASecretName, "Name of the secret which contains CA's certificate and private key.") flag.String("etcd-ca-secret-namespace", defaultETCDCASecretNamespace, "Namespace of the secret which contains CA's certificate and private key.") flag.String("etcd-client-secret-name", defaultETCDClientSecretName, "Name of the secret which contains ETCD client certificates") @@ -54,10 +52,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") + flag.String("kine-secret-name", defaultKineSecretName, "Name of the secret which contains the Kine configuration.") + flag.String("kine-secret-namespace", defaultKineSecretNamespace, "Name of the namespace where the secret which contains the Kine configuration.") + flag.String("kine-host", "", "Host where the DB used by Kine is working.") + flag.Int("kine-port", 0, "Port where the DB used by Kine is listening to.") flag.String("kine-image", defaultKineImage, "Container image along with tag to use for the Kine sidecar container (used only if etcd-storage-type is set to one of kine strategies)") // Setup zap configuration @@ -110,16 +108,16 @@ 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 { + if err := config.BindEnv("kine-secret-name", fmt.Sprintf("%s_KINE_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 { + if err := config.BindEnv("kine-secret-namespace", fmt.Sprintf("%s_KINE_SECRET_NAMESPACE", envPrefix)); err != nil { return nil, err } - if err := config.BindEnv("kine-mysql-host", fmt.Sprintf("%s_KINE_MYSQL_HOST", envPrefix)); err != nil { + if err := config.BindEnv("kine-host", fmt.Sprintf("%s_KINE_HOST", envPrefix)); err != nil { return nil, err } - if err := config.BindEnv("kine-mysql-port", fmt.Sprintf("%s_KINE_MYSQL_PORT", envPrefix)); err != nil { + if err := config.BindEnv("kine-port", fmt.Sprintf("%s_KINE_PORT", envPrefix)); err != nil { return nil, err } if err := config.BindEnv("kine-image", fmt.Sprintf("%s_KINE_IMAGE", envPrefix)); err != nil { diff --git a/internal/resources/sql_certificate.go b/internal/resources/sql_certificate.go index dd22cae..a1f6cb9 100644 --- a/internal/resources/sql_certificate.go +++ b/internal/resources/sql_certificate.go @@ -29,8 +29,8 @@ type SQLCertificate struct { } 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 + return tenantControlPlane.Status.Storage.Kine.Certificate.SecretName != r.resource.GetName() || + tenantControlPlane.Status.Storage.Kine.Certificate.Checksum != r.resource.GetAnnotations()["checksum"] } func (r *SQLCertificate) ShouldCleanup(plane *kamajiv1alpha1.TenantControlPlane) bool { @@ -70,13 +70,13 @@ func (r *SQLCertificate) GetName() string { } func (r *SQLCertificate) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error { - if tenantControlPlane.Status.Storage.KineMySQL == nil { - tenantControlPlane.Status.Storage.KineMySQL = &kamajiv1alpha1.KineMySQLStatus{} + if tenantControlPlane.Status.Storage.Kine == nil { + tenantControlPlane.Status.Storage.Kine = &kamajiv1alpha1.KineStatus{} } - 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() + tenantControlPlane.Status.Storage.Kine.Certificate.SecretName = r.resource.GetName() + tenantControlPlane.Status.Storage.Kine.Certificate.Checksum = r.resource.GetAnnotations()["checksum"] + tenantControlPlane.Status.Storage.Kine.Certificate.LastUpdate = metav1.Now() return nil } @@ -89,10 +89,20 @@ func (r *SQLCertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv return err } - if err := r.buildSecret(ctx, *sqlConfig); err != nil { + checksum, err := r.buildSecret(ctx, *sqlConfig) + if err != nil { return err } + annotations := r.resource.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + + annotations["checksum"] = checksum + + r.resource.SetAnnotations(annotations) + r.resource.SetLabels(utilities.MergeMaps( utilities.KamajiLabels(), r.resource.GetLabels(), @@ -106,26 +116,29 @@ func (r *SQLCertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv } } -func (r *SQLCertificate) buildSecret(ctx context.Context, sqlConfig corev1.Secret) error { +func (r *SQLCertificate) buildSecret(ctx context.Context, sqlConfig corev1.Secret) (checksum string, err error) { switch r.StorageType { - case types.KineMySQL: + case types.KineMySQL, types.KinePostgreSQL: 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) + return "", fmt.Errorf("storage type %s is not implemented", r.StorageType) } } -func (r *SQLCertificate) buildKineSecret(ctx context.Context, keys []string, sqlConfig corev1.Secret) error { +func (r *SQLCertificate) buildKineSecret(ctx context.Context, keys []string, sqlConfig corev1.Secret) (string, error) { + checksumMap := map[string]string{} + for _, key := range keys { value, ok := sqlConfig.Data[key] if !ok { - return fmt.Errorf("%s is not in sql config secret", key) + return "", fmt.Errorf("%s is not in sql config secret", key) } r.resource.Data[key] = value + checksumMap[key] = string(value) } - return nil + return utilities.CalculateConfigMapChecksum(checksumMap), nil } diff --git a/internal/resources/sql_setup.go b/internal/resources/sql_setup.go index b0da574..b92a3c8 100644 --- a/internal/resources/sql_setup.go +++ b/internal/resources/sql_setup.go @@ -17,10 +17,6 @@ import ( "github.com/clastix/kamaji/internal/sql" ) -const ( - secretHashLabelKey = "component.kamaji.clastix.io/secret-hash" -) - type sqlSetupResource struct { schema string user string @@ -35,13 +31,13 @@ type SQLSetup struct { } func (r *SQLSetup) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool { - if tenantControlPlane.Status.Storage.KineMySQL == nil { + if tenantControlPlane.Status.Storage.Kine == 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 + return tenantControlPlane.Status.Storage.Kine.Setup.Schema != r.resource.schema || + tenantControlPlane.Status.Storage.Kine.Setup.User != r.resource.user || + tenantControlPlane.Status.Storage.Kine.Setup.Checksum != tenantControlPlane.Status.Storage.Kine.Config.Checksum } func (r *SQLSetup) ShouldCleanup(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool { @@ -56,16 +52,16 @@ func (r *SQLSetup) Define(ctx context.Context, tenantControlPlane *kamajiv1alpha secret := &corev1.Secret{} namespacedName := types.NamespacedName{ Namespace: tenantControlPlane.GetNamespace(), - Name: tenantControlPlane.Status.Storage.KineMySQL.Config.SecretName, + Name: tenantControlPlane.Status.Storage.Kine.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"]), + schema: string(secret.Data["DB_SCHEMA"]), + user: string(secret.Data["DB_USER"]), + password: string(secret.Data["DB_PASSWORD"]), } return nil @@ -76,8 +72,8 @@ func (r *SQLSetup) GetClient() client.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 tenantControlPlane.Status.Storage.Kine.Setup.Checksum != "" && + tenantControlPlane.Status.Storage.Kine.Setup.Checksum != tenantControlPlane.Status.Storage.Kine.Config.Checksum { if err := r.Delete(ctx, tenantControlPlane); err != nil { return controllerutil.OperationResultNone, err } @@ -135,14 +131,14 @@ func (r *SQLSetup) Delete(ctx context.Context, tenantControlPlane *kamajiv1alpha } func (r *SQLSetup) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error { - if tenantControlPlane.Status.Storage.KineMySQL == nil { + if tenantControlPlane.Status.Storage.Kine == 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 + tenantControlPlane.Status.Storage.Kine.Setup.Schema = r.resource.schema + tenantControlPlane.Status.Storage.Kine.Setup.User = r.resource.user + tenantControlPlane.Status.Storage.Kine.Setup.LastUpdate = metav1.Now() + tenantControlPlane.Status.Storage.Kine.Setup.Checksum = tenantControlPlane.Status.Storage.Kine.Config.Checksum return nil } diff --git a/internal/resources/sql_storage_config.go b/internal/resources/sql_storage_config.go index bf32905..6662dee 100644 --- a/internal/resources/sql_storage_config.go +++ b/internal/resources/sql_storage_config.go @@ -23,15 +23,17 @@ type SQLStorageConfig struct { Name string Host string Port int + Driver string } func (r *SQLStorageConfig) ShouldStatusBeUpdated(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) bool { - if tenantControlPlane.Status.Storage.KineMySQL == nil { + if tenantControlPlane.Status.Storage.Kine == nil { return true } - return tenantControlPlane.Status.Storage.KineMySQL.Config.SecretName != r.resource.GetName() || - tenantControlPlane.Status.Storage.KineMySQL.Config.ResourceVersion != r.resource.ResourceVersion + return tenantControlPlane.Status.Storage.Kine.Config.SecretName != r.resource.GetName() || + tenantControlPlane.Status.Storage.Kine.Config.Checksum != r.resource.GetAnnotations()["checksum"] || + tenantControlPlane.Status.Storage.Kine.Driver != r.Driver } func (r *SQLStorageConfig) ShouldCleanup(plane *kamajiv1alpha1.TenantControlPlane) bool { @@ -70,42 +72,50 @@ func (r *SQLStorageConfig) GetName() string { } func (r *SQLStorageConfig) UpdateTenantControlPlaneStatus(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error { - if tenantControlPlane.Status.Storage.KineMySQL == nil { - tenantControlPlane.Status.Storage.KineMySQL = &kamajiv1alpha1.KineMySQLStatus{} + if tenantControlPlane.Status.Storage.Kine == nil { + tenantControlPlane.Status.Storage.Kine = &kamajiv1alpha1.KineStatus{} } - tenantControlPlane.Status.Storage.KineMySQL.Config.SecretName = r.resource.GetName() - tenantControlPlane.Status.Storage.KineMySQL.Config.ResourceVersion = r.resource.ResourceVersion + tenantControlPlane.Status.Storage.Kine.Driver = r.Driver + tenantControlPlane.Status.Storage.Kine.Config.SecretName = r.resource.GetName() + tenantControlPlane.Status.Storage.Kine.Config.Checksum = r.resource.GetAnnotations()["checksum"] 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 { + + savedHash, ok := r.resource.GetAnnotations()["checksum"] + switch { + case ok && savedHash == utilities.CalculateConfigMapChecksum(r.resource.StringData): + password = r.resource.Data["DB_PASSWORD"] + default: 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, + "DB_HOST": []byte(r.Host), + "DB_PORT": []byte(strconv.Itoa(r.Port)), + "DB_SCHEMA": []byte(tenantControlPlane.GetName()), + "DB_USER": []byte(tenantControlPlane.GetName()), + "DB_PASSWORD": password, } + annotations := r.resource.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + + annotations["checksum"] = utilities.CalculateConfigMapChecksum(r.resource.StringData) + r.resource.SetAnnotations(annotations) + 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), }, )) diff --git a/internal/sql/mysql.go b/internal/sql/mysql.go index 71ba301..ae3b370 100644 --- a/internal/sql/mysql.go +++ b/internal/sql/mysql.go @@ -8,6 +8,7 @@ import ( "database/sql" "fmt" + "github.com/go-pg/pg/v10" "github.com/go-sql-driver/mysql" ) @@ -29,6 +30,22 @@ type MySQLConnection struct { port int } +func (c *MySQLConnection) Driver() string { + return "MySQL" +} + +func getPostgreSQLDB(config ConnectionConfig) (DBConnection, error) { + opt := &pg.Options{ + Addr: fmt.Sprintf("%s:%d", config.Host, config.Port), + Database: config.DBName, + User: config.User, + Password: config.Password, + TLSConfig: config.TLSConfig, + } + + return &PostgreSQLConnection{db: pg.Connect(opt), port: config.Port, host: config.Host}, nil +} + func getMySQLDB(config ConnectionConfig) (DBConnection, error) { tlsKey := "mysql" dataSourceName := config.GetDataSourceName() diff --git a/internal/sql/postgresql.go b/internal/sql/postgresql.go new file mode 100644 index 0000000..e67ce1f --- /dev/null +++ b/internal/sql/postgresql.go @@ -0,0 +1,126 @@ +// Copyright 2022 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "fmt" + "strings" + + "github.com/go-pg/pg/v10" +) + +const ( + postgresqlFetchDBStatement = "SELECT FROM pg_database WHERE datname = ?" + postgresqlCreateDBStatement = "CREATE DATABASE %s" + postgresqlUserExists = "SELECT 1 FROM pg_roles WHERE rolname = ?" + postgresqlCreateUserStatement = "CREATE ROLE %s LOGIN PASSWORD ?" + postgresqlShowGrantsStatement = "SELECT has_database_privilege(rolname, ?, 'create') from pg_roles where rolcanlogin and rolname = ?" + postgresqlGrantPrivilegesStatement = "GRANT ALL PRIVILEGES ON DATABASE %s TO %s" + postgresqlRevokePrivilegesStatement = "REVOKE ALL PRIVILEGES ON DATABASE %s FROM %s" + postgresqlDropRoleStatement = "DROP ROLE %s" + postgresqlDropDBStatement = "DROP DATABASE %s WITH (FORCE)" +) + +type PostgreSQLConnection struct { + db *pg.DB + host string + port int +} + +func (r *PostgreSQLConnection) Driver() string { + return "PostgreSQL" +} + +func (r *PostgreSQLConnection) UserExists(ctx context.Context, user string) (bool, error) { + res, err := r.db.ExecContext(ctx, postgresqlUserExists, user) + if err != nil { + return false, err + } + + return res.RowsReturned() > 0, nil +} + +func (r *PostgreSQLConnection) CreateUser(ctx context.Context, user, password string) error { + _, err := r.db.ExecContext(ctx, fmt.Sprintf(postgresqlCreateUserStatement, user), password) + if err != nil { + return err + } + + return nil +} + +func (r *PostgreSQLConnection) DBExists(ctx context.Context, dbName string) (bool, error) { + rows, err := r.db.ExecContext(ctx, postgresqlFetchDBStatement, dbName) + if err != nil { + return false, err + } + + return rows.RowsReturned() > 0, nil +} + +func (r *PostgreSQLConnection) CreateDB(ctx context.Context, dbName string) error { + _, err := r.db.ExecContext(ctx, fmt.Sprintf(postgresqlCreateDBStatement, dbName)) + if err != nil { + return err + } + + return nil +} + +func (r *PostgreSQLConnection) GrantPrivilegesExists(ctx context.Context, user, dbName string) (bool, error) { + var hasDatabasePrivilege string + + _, err := r.db.QueryContext(ctx, pg.Scan(&hasDatabasePrivilege), postgresqlShowGrantsStatement, dbName, user) + if err != nil { + if strings.Contains(err.Error(), "does not exist") { + return false, nil + } + + return false, err + } + + return hasDatabasePrivilege == "t", nil +} + +func (r *PostgreSQLConnection) GrantPrivileges(ctx context.Context, user, dbName string) error { + res, err := r.db.ExecContext(ctx, fmt.Sprintf(postgresqlGrantPrivilegesStatement, dbName, user)) + _ = res + + return err +} + +func (r *PostgreSQLConnection) DeleteUser(ctx context.Context, user string) error { + _, err := r.db.ExecContext(ctx, fmt.Sprintf(postgresqlDropRoleStatement, user)) + + return err +} + +func (r *PostgreSQLConnection) DeleteDB(ctx context.Context, dbName string) error { + _, err := r.db.ExecContext(ctx, fmt.Sprintf(postgresqlDropDBStatement, dbName)) + + return err +} + +func (r *PostgreSQLConnection) RevokePrivileges(ctx context.Context, user, dbName string) error { + _, err := r.db.ExecContext(ctx, fmt.Sprintf(postgresqlRevokePrivilegesStatement, dbName, user)) + + return err +} + +func (r *PostgreSQLConnection) GetHost() string { + return r.host +} + +func (r *PostgreSQLConnection) GetPort() int { + return r.port +} + +func (r *PostgreSQLConnection) Close() error { + return r.db.Close() +} + +func (r *PostgreSQLConnection) Check() error { + return r.db.Ping(context.Background()) +} diff --git a/internal/sql/sql.go b/internal/sql/sql.go index 28505fb..d8b4cc7 100644 --- a/internal/sql/sql.go +++ b/internal/sql/sql.go @@ -14,12 +14,15 @@ type Driver int const ( MySQL Driver = iota + PostgreSQL ) func (d Driver) ToString() string { switch d { case MySQL: return "mysql" + case PostgreSQL: + return "postgresql" default: return "" } @@ -88,12 +91,15 @@ type DBConnection interface { GetPort() int Close() error Check() error + Driver() string } func GetDBConnection(config ConnectionConfig) (DBConnection, error) { switch config.SQLDriver { case MySQL: return getMySQLDB(config) + case PostgreSQL: + return getPostgreSQLDB(config) default: return nil, fmt.Errorf("%s is not a valid driver", config.SQLDriver.ToString()) } diff --git a/internal/types/etcd_storage.go b/internal/types/etcd_storage.go index 158b856..5c1fb4d 100644 --- a/internal/types/etcd_storage.go +++ b/internal/types/etcd_storage.go @@ -14,12 +14,13 @@ type ETCDStorageType int const ( ETCD ETCDStorageType = iota KineMySQL + KinePostgreSQL ) -var etcdStorageTypeString = map[string]ETCDStorageType{"etcd": ETCD, "kine-mysql": KineMySQL} +var etcdStorageTypeString = map[string]ETCDStorageType{"etcd": ETCD, "kine-mysql": KineMySQL, "kine-postgresql": KinePostgreSQL} func (s ETCDStorageType) String() string { - return [...]string{"etcd", "kine-mysql"}[s] + return [...]string{"etcd", "kine-mysql", "kine-postgresql"}[s] } // ParseETCDStorageType returns the ETCDStorageType given a string representation of the type. @@ -36,7 +37,7 @@ func ParseETCDEndpoint(conf *viper.Viper) string { switch ParseETCDStorageType(conf.GetString("etcd-storage-type")) { case ETCD: return conf.GetString("etcd-endpoints") - case KineMySQL: + case KineMySQL, KinePostgreSQL: return "127.0.0.1:2379" default: panic("unsupported storage type") diff --git a/main.go b/main.go index 21cc560..31e2b16 100644 --- a/main.go +++ b/main.go @@ -72,10 +72,10 @@ func main() { ETCDEndpoints: types.ParseETCDEndpoint(conf), 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"), + KineSecretName: conf.GetString("kine-secret-name"), + KineSecretNamespace: conf.GetString("kine-secret-namespace"), + KineHost: conf.GetString("kine-host"), + KinePort: conf.GetInt("kine-port"), KineContainerImage: conf.GetString("kine-image"), }, }