Skip to content

Commit 2f0e74b

Browse files
authored
feat: add support for sql databases (#4)
1 parent ab55025 commit 2f0e74b

20 files changed

+713
-79
lines changed

.github/workflows/test.yml

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Go Test
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
tags:
8+
- '*'
9+
pull_request:
10+
branches:
11+
- main
12+
13+
jobs:
14+
build:
15+
name: Test on Go ${{ matrix.go-version }} and ${{ matrix.os }}
16+
runs-on: ${{ matrix.os }}
17+
strategy:
18+
matrix:
19+
go-version: [1.18.x]
20+
os: [ubuntu-latest]
21+
steps:
22+
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
23+
uses: actions/setup-go@v1
24+
with:
25+
go-version: ${{ matrix.go-version }}
26+
id: go
27+
28+
- name: Check out code into the Go module directory
29+
uses: actions/checkout@v2
30+
31+
- name: Start containers
32+
run: docker-compose up -d
33+
34+
- name: Test adapters on ${{ matrix.os }}
35+
env:
36+
GO111MODULE: on
37+
run: |
38+
go test -v -race ./adapters/...
39+
40+
- name: Stop containers
41+
if: always()
42+
run: docker-compose down

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ spec:
2626

2727
- mongo :white_check_mark:
2828
- couchdb :white_check_mark:
29-
- mysql :hammer:
30-
- postgres :clock1:
29+
- mysql :white_check_mark:
30+
- postgres :white_check_mark:
31+
- mssql :white_check_mark:
3132

3233
## Deployment
3334

adapters/couchdb_adapter.go

-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ func (adapter couchdbAdapter) HasDatabaseUserWithAccess(ctx context.Context, dat
4444
}
4545

4646
func (adapter couchdbAdapter) CreateDatabaseUser(ctx context.Context, database string, username string, password string) error {
47-
4847
exists, err := adapter.db.DBExists(ctx, "_users")
4948
if err != nil {
5049
return err

adapters/couchdb_adapter_test.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,20 @@ func TestCouchDB(t *testing.T) {
1414
databasePort := "5984"
1515

1616
ctx := context.Background()
17-
couchdbUrl := fmt.Sprintf("http://admin:1234@%s:%s", databaseHost, databasePort)
18-
adapter, err := adapters.GetCouchdbConnection(ctx, couchdbUrl)
17+
url := fmt.Sprintf("http://%s:%s@%s:%s", "admin", "pA_sw0rd", databaseHost, databasePort)
18+
adapter, err := adapters.GetCouchdbConnection(ctx, url)
1919
if err != nil {
2020
t.Fatalf("Error opening database connection: %s", err)
2121
}
2222

23-
clientConnectTest := func(databaseName string, databaseUsername string, databasePassword string) error {
24-
client, err := kivik.New("couch", fmt.Sprintf("http://%s:%s@%s:%s", databaseUsername, databasePassword, databaseHost, databasePort))
23+
clientConnectTest := func(ctx context.Context, databaseName string, databaseUsername string, databasePassword string) error {
24+
url := fmt.Sprintf("http://%s:%s@%s:%s", databaseUsername, databasePassword, databaseHost, databasePort)
25+
client, err := kivik.New("couch", url)
2526
if err != nil {
2627
return err
2728
}
29+
defer client.Close(ctx)
30+
2831
_, _, err = client.DB(databaseName).CreateDoc(ctx, map[string]interface{}{"test": "test"})
2932
return err
3033
}

adapters/mongo_adapter_test.go

+6-4
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,21 @@ func TestMongoDB(t *testing.T) {
1515
databasePort := "27017"
1616

1717
ctx := context.Background()
18-
mongodbUrl := fmt.Sprintf("mongodb://admin:1234@%s:%s/?authSource=admin", databaseHost, databasePort)
19-
adapter, err := adapters.GetMongoConnection(ctx, mongodbUrl)
18+
url := fmt.Sprintf("mongodb://%s:%s@%s:%s/?authSource=admin", "admin", "pA_sw0rd", databaseHost, databasePort)
19+
adapter, err := adapters.GetMongoConnection(ctx, url)
2020
if err != nil {
2121
t.Fatalf("Error opening database connection: %s", err)
2222
}
2323

24-
clientConnectTest := func(databaseName string, databaseUsername string, databasePassword string) error {
25-
clientOpts := options.Client().ApplyURI(fmt.Sprintf("mongodb://%s:%s@%s:%s/%s", databaseUsername, databasePassword, databaseHost, databasePort, databaseName))
24+
clientConnectTest := func(ctx context.Context, databaseName string, databaseUsername string, databasePassword string) error {
25+
url := fmt.Sprintf("mongodb://%s:%s@%s:%s/%s", databaseUsername, databasePassword, databaseHost, databasePort, databaseName)
26+
clientOpts := options.Client().ApplyURI(url)
2627
client, err := mongo.Connect(ctx, clientOpts)
2728
if err != nil {
2829
return err
2930
}
3031
defer client.Disconnect(ctx)
32+
3133
_, err = client.Database(databaseName).Collection("test").InsertOne(ctx, map[string]interface{}{"test": "test"})
3234
return err
3335
}

adapters/mssql_adapter.go

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package adapters
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
8+
_ "github.com/denisenkom/go-mssqldb"
9+
)
10+
11+
type mssqlAdapter struct {
12+
db *sql.DB
13+
}
14+
15+
func (adapter mssqlAdapter) HasDatabase(ctx context.Context, database string) (bool, error) {
16+
var count int
17+
query := fmt.Sprintf("SELECT COUNT(*) FROM master.sys.databases WHERE name='%s';", database)
18+
err := adapter.db.QueryRowContext(ctx, query).Scan(&count)
19+
if err != nil {
20+
return false, err
21+
}
22+
return count == 1, nil
23+
}
24+
25+
func (adapter mssqlAdapter) CreateDatabase(ctx context.Context, database string) error {
26+
query := fmt.Sprintf("EXEC ('sp_configure ''contained database authentication'', 1; reconfigure;');")
27+
_, err := adapter.db.ExecContext(ctx, query)
28+
if err != nil {
29+
return err
30+
}
31+
32+
query = fmt.Sprintf("CREATE DATABASE [%s] CONTAINMENT=PARTIAL;", database)
33+
_, err = adapter.db.ExecContext(ctx, query)
34+
return err
35+
}
36+
37+
func (adapter mssqlAdapter) DeleteDatabase(ctx context.Context, database string) error {
38+
query := fmt.Sprintf("DROP DATABASE [%s];", database)
39+
_, err := adapter.db.ExecContext(ctx, query)
40+
return err
41+
}
42+
43+
func (adapter mssqlAdapter) HasDatabaseUserWithAccess(ctx context.Context, database string, username string) (bool, error) {
44+
var count int
45+
query := fmt.Sprintf("USE [%s]; SELECT COUNT(*) FROM sys.database_principals WHERE authentication_type=2 AND name='%s';", database, username)
46+
err := adapter.db.QueryRowContext(ctx, query).Scan(&count)
47+
if err != nil {
48+
return false, err
49+
}
50+
return count == 1, nil
51+
}
52+
53+
func (adapter mssqlAdapter) CreateDatabaseUser(ctx context.Context, database string, username string, password string) error {
54+
// make password sql safe
55+
quotedPassword := QuoteLiteral(password)
56+
query := fmt.Sprintf("USE [%s]; CREATE USER [%s] WITH PASSWORD=%s", database, username, quotedPassword)
57+
_, err := adapter.db.ExecContext(ctx, query)
58+
if err != nil {
59+
return err
60+
}
61+
62+
query = fmt.Sprintf("USE [%s]; ALTER ROLE db_owner ADD MEMBER [%s];", database, username)
63+
_, err = adapter.db.ExecContext(ctx, query)
64+
return err
65+
}
66+
67+
func (adapter mssqlAdapter) DeleteDatabaseUser(ctx context.Context, database string, username string) error {
68+
query := fmt.Sprintf("USE [%s]; DROP USER %s;", database, username)
69+
_, err := adapter.db.ExecContext(ctx, query)
70+
return err
71+
}
72+
73+
func (adapter mssqlAdapter) Close(ctx context.Context) error {
74+
return adapter.db.Close()
75+
}
76+
77+
func GetMssqlConnection(ctx context.Context, url string) (*mssqlAdapter, error) {
78+
db, err := sql.Open("sqlserver", url)
79+
if err != nil {
80+
return nil, err
81+
}
82+
83+
adapter := mssqlAdapter{
84+
db: db,
85+
}
86+
87+
if err := adapter.db.PingContext(ctx); err != nil {
88+
return nil, err
89+
}
90+
91+
return &adapter, nil
92+
}

adapters/mssql_adapter_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package adapters_test
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
"testing"
8+
9+
"github.com/anbraten/k8s-external-database-operator/adapters"
10+
)
11+
12+
func TestMsSqlDB(t *testing.T) {
13+
databaseHost := "localhost"
14+
databasePort := "1433"
15+
16+
ctx := context.Background()
17+
url := fmt.Sprintf("sqlserver://%s:%s@%s:%s", "sa", "pA_sw0rd", databaseHost, databasePort)
18+
adapter, err := adapters.GetMssqlConnection(ctx, url)
19+
if err != nil {
20+
t.Fatalf("Error opening database connection: %s", err)
21+
}
22+
23+
clientConnectTest := func(ctx context.Context, databaseName string, databaseUsername string, databasePassword string) error {
24+
url := fmt.Sprintf("sqlserver://%s:%s@%s:%s?database=%s", databaseUsername, databasePassword, databaseHost, databasePort, databaseName)
25+
client, err := sql.Open("sqlserver", url)
26+
if err != nil {
27+
return err
28+
}
29+
defer client.Close()
30+
31+
_, err = client.ExecContext(ctx, "CREATE TABLE test (id int);")
32+
return err
33+
}
34+
35+
testHelper(t, ctx, adapter, clientConnectTest)
36+
}

adapters/mysql_adapter.go

+39-16
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ package adapters
33
import (
44
"context"
55
"database/sql"
6+
"fmt"
67

7-
// SQL driver for mysql
8+
// load SQL driver for mysql
89
_ "github.com/go-sql-driver/mysql"
910
)
1011

@@ -13,40 +14,62 @@ type mysqlAdapter struct {
1314
}
1415

1516
func (adapter mysqlAdapter) HasDatabase(ctx context.Context, database string) (bool, error) {
16-
return false, nil
17+
var count int
18+
query := "SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME=?"
19+
err := adapter.db.QueryRowContext(ctx, query, database).Scan(&count)
20+
return count == 1, err
1721
}
1822

19-
func (adapter mysqlAdapter) CreateDatabase(ctx context.Context, name string) error {
20-
_, err := adapter.db.Exec("CREATE DATABASE IF NOT EXISTS $1;", name)
23+
func (adapter mysqlAdapter) CreateDatabase(ctx context.Context, database string) error {
24+
query := fmt.Sprintf("CREATE DATABASE %s;", database)
25+
_, err := adapter.db.ExecContext(ctx, query)
2126
return err
2227
}
2328

24-
func (adapter mysqlAdapter) DeleteDatabase(ctx context.Context, name string) error {
25-
_, err := adapter.db.Exec("DROP DATABASE IF EXISTS $1;", name)
29+
func (adapter mysqlAdapter) DeleteDatabase(ctx context.Context, database string) error {
30+
query := fmt.Sprintf("DROP DATABASE %s;", database)
31+
_, err := adapter.db.ExecContext(ctx, query)
2632
return err
2733
}
2834

29-
func (adapter mysqlAdapter) HasDatabaseUserWithAccess(ctx context.Context, username string, database string) (bool, error) {
30-
// TODO implement
31-
return false, nil
35+
func (adapter mysqlAdapter) HasDatabaseUserWithAccess(ctx context.Context, database string, username string) (bool, error) {
36+
var count int
37+
query := "SELECT COUNT(*) FROM mysql.db WHERE Db=? AND USER=?"
38+
err := adapter.db.QueryRowContext(ctx, query, database, username).Scan(&count)
39+
return count == 1, err
3240
}
3341

34-
func (adapter mysqlAdapter) CreateDatabaseUser(ctx context.Context, username string, password string, database string) error {
35-
// TODO implement
36-
return nil
42+
func (adapter mysqlAdapter) CreateDatabaseUser(ctx context.Context, database string, username string, password string) error {
43+
// make password sql safe
44+
quotedPassword := QuoteLiteral(password)
45+
query := fmt.Sprintf("CREATE USER '%s'@'%%' IDENTIFIED BY %s;", username, quotedPassword)
46+
_, err := adapter.db.ExecContext(ctx, query)
47+
if err != nil {
48+
return err
49+
}
50+
51+
query = fmt.Sprintf("GRANT ALL PRIVILEGES ON %s.* TO '%s'@'%%';", database, username)
52+
_, err = adapter.db.ExecContext(ctx, query)
53+
if err != nil {
54+
return err
55+
}
56+
57+
_, err = adapter.db.ExecContext(ctx, "FLUSH PRIVILEGES;")
58+
return err
3759
}
3860

3961
func (adapter mysqlAdapter) DeleteDatabaseUser(ctx context.Context, database string, username string) error {
40-
// TODO implement
41-
return nil
62+
query := fmt.Sprintf("DROP USER %s;", username)
63+
_, err := adapter.db.ExecContext(ctx, query)
64+
return err
4265
}
4366

4467
func (adapter mysqlAdapter) Close(ctx context.Context) error {
4568
return adapter.db.Close()
4669
}
4770

48-
func GetMysqlConnection(ctx context.Context, host string, adminUsername string, adminPassword string) (*mysqlAdapter, error) {
49-
db, err := sql.Open("mysql", adminUsername+":"+adminPassword+"@"+host)
71+
func GetMysqlConnection(ctx context.Context, dsn string) (*mysqlAdapter, error) {
72+
db, err := sql.Open("mysql", dsn)
5073
if err != nil {
5174
return nil, err
5275
}

adapters/mysql_adapter_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package adapters_test
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
"testing"
8+
9+
"github.com/anbraten/k8s-external-database-operator/adapters"
10+
)
11+
12+
func TestMySqlDB(t *testing.T) {
13+
databaseHost := "localhost"
14+
databasePort := "3306"
15+
16+
ctx := context.Background()
17+
url := fmt.Sprintf("%s:%s@tcp(%s:%s)/", "root", "pA_sw0rd", databaseHost, databasePort)
18+
adapter, err := adapters.GetMysqlConnection(ctx, url)
19+
if err != nil {
20+
t.Fatalf("Error opening database connection: %s", err)
21+
}
22+
23+
clientConnectTest := func(ctx context.Context, databaseName string, databaseUsername string, databasePassword string) error {
24+
url := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", databaseUsername, databasePassword, databaseHost, databasePort, databaseName)
25+
client, err := sql.Open("mysql", url)
26+
if err != nil {
27+
return err
28+
}
29+
defer client.Close()
30+
31+
_, err = client.ExecContext(ctx, "CREATE TABLE test (id int);")
32+
return err
33+
}
34+
35+
testHelper(t, ctx, adapter, clientConnectTest)
36+
}

0 commit comments

Comments
 (0)