Skip to content

Commit 5d02dad

Browse files
committed
feat(providers): single database mode
Creates a new database provider mode named `single`. The aim of the mode is to create a single database instance and share it between the apps by separating them by Posgres schemas. Each app gets a schema created by the name of the requested database. Schemas are created under environment's database, with the same name as the ClowdEnv. RHINENG-14526
1 parent db21c03 commit 5d02dad

File tree

5 files changed

+318
-8
lines changed

5 files changed

+318
-8
lines changed

apis/cloud.redhat.com/v1alpha1/clowdenvironment_types.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -284,16 +284,18 @@ type KafkaConfig struct {
284284
}
285285

286286
// DatabaseMode details the mode of operation of the Clowder Database Provider
287-
// +kubebuilder:validation:Enum=shared;app-interface;local;none
287+
// +kubebuilder:validation:Enum=single;shared;app-interface;local;none
288288
type DatabaseMode string
289289

290290
// DatabaseConfig configures the Clowder provider controlling the creation of
291291
// Database instances.
292292
type DatabaseConfig struct {
293293
// The mode of operation of the Clowder Database Provider. Valid options are:
294294
// (*_app-interface_*) where the provider will pass through database credentials
295-
// found in the secret defined by the database name in the ClowdApp, and (*_local_*)
296-
// where the provider will spin up a local instance of the database.
295+
// found in the secret defined by the database name in the ClowdApp, (*_local_*)
296+
// where the provider will spin up a local instance of the database, and (*_single_*)
297+
// that is similar to local but keeps only one database deployment and isolates apps by
298+
// PG schemas.
297299
Mode DatabaseMode `json:"mode"`
298300

299301
// Indicates where Clowder will fetch the database CA certificate bundle from. Currently only used in

config/crd/bases/cloud.redhat.com_clowdenvironments.yaml

+8-5
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,15 @@ spec:
8181
type: string
8282
mode:
8383
description: 'The mode of operation of the Clowder Database
84-
Provider. Valid options are: (*_app-interface_*) where the
85-
provider will pass through database credentials found in
86-
the secret defined by the database name in the ClowdApp,
87-
and (*_local_*) where the provider will spin up a local
88-
instance of the database.'
84+
Provider. Valid options are: (*_app-interface_*) where
85+
the provider will pass through database credentials found
86+
in the secret defined by the database name in the
87+
ClowdApp, (*_local_*) where the provider will spin up a
88+
local instance of the database, and (*_single_*) that is
89+
similar to local but keeps only one database deployment
90+
and isolates apps by PG schemas.'
8991
enum:
92+
- single
9093
- shared
9194
- app-interface
9295
- local

controllers/cloud.redhat.com/providers/database/provider.go

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ var imageList map[int32]string
1818
func GetDatabase(c *p.Provider) (p.ClowderProvider, error) {
1919
dbMode := c.Env.Spec.Providers.Database.Mode
2020
switch dbMode {
21+
case "single":
22+
return NewSingleDBProvider(c)
2123
case "shared":
2224
return NewSharedDBProvider(c)
2325
case "local":
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
package database
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
"time"
8+
9+
"github.com/lib/pq"
10+
11+
crd "github.com/RedHatInsights/clowder/apis/cloud.redhat.com/v1alpha1"
12+
"github.com/RedHatInsights/clowder/controllers/cloud.redhat.com/config"
13+
"github.com/RedHatInsights/clowder/controllers/cloud.redhat.com/errors"
14+
obj "github.com/RedHatInsights/clowder/controllers/cloud.redhat.com/object"
15+
"github.com/RedHatInsights/clowder/controllers/cloud.redhat.com/providers"
16+
"github.com/RedHatInsights/clowder/controllers/cloud.redhat.com/providers/sizing"
17+
"github.com/RedHatInsights/clowder/controllers/cloud.redhat.com/providers/sizing/sizingconfig"
18+
provutils "github.com/RedHatInsights/clowder/controllers/cloud.redhat.com/providers/utils"
19+
20+
apps "k8s.io/api/apps/v1"
21+
core "k8s.io/api/core/v1"
22+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23+
"k8s.io/apimachinery/pkg/types"
24+
25+
rc "github.com/RedHatInsights/rhc-osdk-utils/resourceCache"
26+
"github.com/RedHatInsights/rhc-osdk-utils/utils"
27+
)
28+
29+
// SingleDBDeployment is the ident referring to the single local DB deployment object
30+
var SingleDBDeployment = rc.NewSingleResourceIdent(ProvName, "single_db_deployment", &apps.Deployment{})
31+
32+
// SingleDBScret is the ident referring to the single local DB secret object
33+
var SingleDBSecret = rc.NewMultiResourceIdent(ProvName, "single_db_secret", &core.Secret{})
34+
35+
// SingleDBPVC is the ident referring to the single local DB PersitentVolumeClaim
36+
var SingleDBPVC = rc.NewSingleResourceIdent(ProvName, "single_db_pvc", &core.PersistentVolumeClaim{})
37+
38+
// SingleDBService is the ident referring to the single local DB service object.
39+
var SingleDBService = rc.NewMultiResourceIdent(ProvName, "single_db_service", &core.Service{})
40+
41+
func NewSingleDBProvider(p *providers.Provider) (providers.ClowderProvider, error) {
42+
p.Cache.AddPossibleGVKFromIdent(
43+
SingleDBDeployment,
44+
SingleDBSecret,
45+
SingleDBPVC,
46+
SingleDBService,
47+
)
48+
return &singleDbProvider{Provider: *p}, nil
49+
}
50+
51+
type singleDbProvider struct {
52+
providers.Provider
53+
}
54+
55+
func (db *singleDbProvider) DBNamespacedName() types.NamespacedName {
56+
return types.NamespacedName{
57+
Name: fmt.Sprintf("%s-db-single", db.Env.Name),
58+
Namespace: db.Env.Status.TargetNamespace,
59+
}
60+
}
61+
62+
func (db *singleDbProvider) EnvProvide() error {
63+
appList, err := db.Env.GetAppsInEnv(db.Ctx, db.Client)
64+
if err != nil {
65+
return err
66+
}
67+
68+
needsDb := false
69+
for _, app := range appList.Items {
70+
if app.Spec.Database.Name != "" {
71+
needsDb = true
72+
break
73+
}
74+
}
75+
76+
if needsDb {
77+
if _, err := db.createDatabaseDeployment(); err != nil {
78+
return err
79+
}
80+
if db.Env.Spec.Providers.Database.PVC {
81+
if err := db.createDatabasePVC(); err != nil {
82+
return err
83+
}
84+
}
85+
}
86+
return nil
87+
}
88+
89+
// Creates a single database deployment locked to one version with a main secret
90+
func (db *singleDbProvider) createDatabaseDeployment() (*config.DatabaseConfig, error) {
91+
nn := db.DBNamespacedName()
92+
93+
dd := &apps.Deployment{}
94+
ownerrefs := []metav1.OwnerReference{db.Env.MakeOwnerReference()}
95+
dd.ObjectMeta.OwnerReferences = ownerrefs
96+
err := db.Cache.Create(SingleDBDeployment, nn, dd)
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
dbName := db.Env.Name
102+
dbCfg, err := db.createOrReadDbConfig(db.Env, nn, dbName, true)
103+
if err != nil {
104+
return nil, err
105+
}
106+
107+
labels := &map[string]string{
108+
"sub": "single_db",
109+
}
110+
usePVC := db.Env.Spec.Providers.Database.PVC
111+
provutils.MakeLocalDB(dd, nn, db.Env, labels, dbCfg, provutils.DefaultImageDatabasePG, usePVC, dbName, nil)
112+
if err := db.Cache.Update(SingleDBDeployment, dd); err != nil {
113+
return dbCfg, err
114+
}
115+
116+
svc := &core.Service{}
117+
svc.ObjectMeta.OwnerReferences = ownerrefs
118+
if err := db.Cache.Create(SingleDBService, nn, svc); err != nil {
119+
return dbCfg, err
120+
}
121+
122+
provutils.MakeLocalDBService(svc, nn, db.Env, labels)
123+
124+
if err = db.Cache.Update(SingleDBService, svc); err != nil {
125+
return dbCfg, err
126+
}
127+
128+
return dbCfg, nil
129+
}
130+
131+
func (db *singleDbProvider) createDatabasePVC() error {
132+
nn := db.DBNamespacedName()
133+
134+
pvc := &core.PersistentVolumeClaim{}
135+
if err := db.Cache.Create(SingleDBPVC, nn, pvc); err != nil {
136+
return err
137+
}
138+
139+
// TODO handle volume capacity
140+
capacity := sizing.GetVolCapacityForSize(sizingconfig.SizeLarge)
141+
provutils.MakeLocalDBPVC(pvc, nn, db.Env, capacity)
142+
if err := db.Cache.Update(SingleDBPVC, pvc); err != nil {
143+
return err
144+
}
145+
return nil
146+
}
147+
148+
func (db *singleDbProvider) Provide(app *crd.ClowdApp) error {
149+
if app.Spec.Database.SharedDBAppName != "" {
150+
return db.processSharedDB(app)
151+
} else if app.Spec.Database.Name != "" {
152+
return db.provideAppDB(app)
153+
}
154+
return nil
155+
}
156+
157+
func (db *singleDbProvider) provideAppDB(app *crd.ClowdApp) error {
158+
dbnn := db.DBNamespacedName()
159+
dbCfg := &config.DatabaseConfig{}
160+
161+
if err := provutils.ReadDbConfigFromSecret(db.Provider, SingleDBSecret, dbCfg, dbnn); err != nil {
162+
return err
163+
}
164+
165+
// Create database config and secret for the app,
166+
// without the admin credentials.
167+
appnn := types.NamespacedName{
168+
Name: fmt.Sprintf("%v-db", app.Name),
169+
Namespace: app.Namespace,
170+
}
171+
appDbCfg, err := db.createOrReadDbConfig(app, appnn, app.Name, false)
172+
if err != nil {
173+
return err
174+
}
175+
db.Config.Database = appDbCfg
176+
177+
// reconcile access
178+
if err := db.reconcileDBAppAccess(dbCfg, appDbCfg); err != nil {
179+
return err
180+
}
181+
182+
return nil
183+
}
184+
185+
func (db *singleDbProvider) processSharedDB(app *crd.ClowdApp) error {
186+
refApp, err := crd.GetAppForDBInSameEnv(db.Ctx, db.Client, app)
187+
if err != nil {
188+
return err
189+
}
190+
191+
nn := types.NamespacedName{
192+
Name: fmt.Sprintf("%v-db", refApp.Name),
193+
Namespace: refApp.Namespace,
194+
}
195+
dbCfg := &config.DatabaseConfig{}
196+
if err := provutils.ReadDbConfigFromSecret(db.Provider, SingleDBSecret, dbCfg, nn); err != nil {
197+
return errors.Wrap(fmt.Sprintf("sharedDBApp %s", refApp.Name), err)
198+
}
199+
200+
db.Config.Database = dbCfg
201+
return nil
202+
}
203+
204+
func (db *singleDbProvider) Hostname() string {
205+
nn := db.DBNamespacedName()
206+
return fmt.Sprintf("%v.%v.svc", nn.Name, nn.Namespace)
207+
}
208+
209+
func (db *singleDbProvider) createOrReadDbConfig(obj obj.ClowdObject, nn types.NamespacedName, username string, setAdmin bool) (cfg *config.DatabaseConfig, err error) {
210+
cfg = &config.DatabaseConfig{}
211+
password, err := utils.RandPassword(16, provutils.RCharSet)
212+
if err != nil {
213+
return cfg, errors.Wrap("password generate failed", err)
214+
}
215+
216+
var pgPassword string
217+
if setAdmin {
218+
pgPassword, err = utils.RandPassword(16, provutils.RCharSet)
219+
if err != nil {
220+
return cfg, errors.Wrap("pgPassword generate failed", err)
221+
}
222+
}
223+
224+
dataInit := func() map[string]string {
225+
return map[string]string{
226+
"hostname": db.Hostname(),
227+
"port": provutils.DefaultPGPort,
228+
"name": db.Env.Name,
229+
"username": username,
230+
"password": password,
231+
"pgPass": pgPassword,
232+
}
233+
}
234+
235+
var secMap *map[string]string
236+
secMap, err = providers.MakeOrGetSecret(obj, db.Cache, SingleDBSecret, nn, dataInit)
237+
if err != nil {
238+
return cfg, errors.Wrap("couldn't set/get secret", err)
239+
}
240+
241+
err = cfg.Populate(secMap)
242+
if err != nil {
243+
return cfg, errors.Wrap("couldn't populate db config from secret", err)
244+
}
245+
if setAdmin {
246+
cfg.AdminUsername = provutils.DefaultPGAdminUsername
247+
}
248+
cfg.SslMode = "disable"
249+
return
250+
}
251+
252+
func (db *singleDbProvider) reconcileDBAppAccess(envCfg *config.DatabaseConfig, appCfg *config.DatabaseConfig) error {
253+
appSQLConnectionString := provutils.PGAdminConnectionStr(envCfg, envCfg.Name)
254+
255+
ctx, cancel := context.WithTimeout(db.Ctx, 5*time.Second)
256+
defer cancel()
257+
258+
dbClient, err := sql.Open("postgres", appSQLConnectionString)
259+
if err != nil {
260+
return errors.Wrap("unable ,to connect to db", err)
261+
}
262+
defer dbClient.Close()
263+
264+
if err := dbClient.PingContext(ctx); err != nil {
265+
return err
266+
}
267+
268+
username := appCfg.Username
269+
password := appCfg.Password
270+
rows, err := dbClient.QueryContext(ctx, "SELECT TRUE FROM pg_roles WHERE rolname = $1;", username)
271+
if err != nil {
272+
return errors.Wrap("unable to query for roles", err)
273+
}
274+
275+
var roleExists = rows.Next()
276+
rows.Close()
277+
278+
if roleExists {
279+
_, err = dbClient.ExecContext(ctx,
280+
fmt.Sprintf("ALTER ROLE %s WITH LOGIN ENCRYPTED PASSWORD %s;",
281+
pq.QuoteIdentifier(username), pq.QuoteLiteral(password),
282+
))
283+
} else {
284+
_, err = dbClient.ExecContext(ctx,
285+
fmt.Sprintf("CREATE ROLE %s WITH LOGIN ENCRYPTED PASSWORD %s;",
286+
pq.QuoteIdentifier(username), pq.QuoteLiteral(password),
287+
))
288+
}
289+
if err != nil {
290+
return errors.Wrap("unable to create/alter role", err)
291+
}
292+
293+
_, err = dbClient.ExecContext(ctx,
294+
fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s AUTHORIZATION %s;",
295+
pq.QuoteIdentifier(username), pq.QuoteIdentifier(username),
296+
))
297+
if err != nil {
298+
return errors.Wrap("unable to create db schema for the app", err)
299+
}
300+
301+
return nil
302+
}

controllers/cloud.redhat.com/providers/utils/utils.go

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ var DefaultImageDatabasePG13 = "quay.io/cloudservices/postgresql-rds:13-2318dee"
3636
var DefaultImageDatabasePG14 = "quay.io/cloudservices/postgresql-rds:14-2318dee"
3737
var DefaultImageDatabasePG15 = "quay.io/cloudservices/postgresql-rds:15-2318dee"
3838
var DefaultImageDatabasePG16 = "quay.io/cloudservices/postgresql-rds:16-759c25d"
39+
var DefaultImageDatabasePG = DefaultImageDatabasePG16
3940
var DefaultImageInMemoryDB = "registry.redhat.io/rhel9/redis-6:1-199.1726663404"
4041
var DefaultPGPort = "5432"
4142
var DefaultPGAdminUsername = "postgres"

0 commit comments

Comments
 (0)