diff --git a/.travis.yml b/.travis.yml index 73eacc8e..2ffbbe93 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,9 +16,9 @@ cache: - $GOPATH/pkg/mod jobs: - # allow_failures: - # - env: - # - ALLOW_FAIL=true + allow_failures: + - env: + - ALLOW_FAIL=true include: - stage: test name: "Unit Tests" @@ -37,6 +37,8 @@ jobs: - find . -type f -name "*\.go" | grep -v "zz_generated" | while read line; do golint -set_exit_status $line || exit 1; done + env: + - ALLOW_FAIL=true - stage: test name: "Go Vet" script: diff --git a/Makefile b/Makefile index 1aec93fe..03a5ce4c 100644 --- a/Makefile +++ b/Makefile @@ -60,6 +60,9 @@ test: @docker stop postgres @docker stop mysql +lint: + @golint ./... + vet: @go vet ./... diff --git a/go.mod b/go.mod index 15a772fd..25bf8594 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,9 @@ require ( github.com/sirupsen/logrus v1.4.2 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.4.0 + golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 + golang.org/x/tools v0.0.0-20200526224456-8b020aee10d2 // indirect google.golang.org/api v0.9.0 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 gopkg.in/yaml.v2 v2.2.4 diff --git a/go.sum b/go.sum index b9d6b903..8644e3ee 100644 --- a/go.sum +++ b/go.sum @@ -598,6 +598,7 @@ github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -633,6 +634,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -644,8 +647,12 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180112015858-5ccada7d0a7b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -676,6 +683,8 @@ golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQ golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -753,8 +762,15 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20191018212557-ed542cd5b28a h1:UuQ+70Pi/ZdWHuP4v457pkXeOynTdgd/4enxeIO/98k= golang.org/x/tools v0.0.0-20191018212557-ed542cd5b28a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200526224456-8b020aee10d2 h1:21BqcH/onxtGHn1A2GDOJjZnbt4Nlez629S3eaR+eYs= +golang.org/x/tools v0.0.0-20200526224456-8b020aee10d2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.0.1 h1:xyiBuvkD2g5n7cYzx6u2sxQvsAy4QJsZFCzGVdzOXZ0= gomodules.xyz/jsonpatch/v2 v2.0.1/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= diff --git a/helm/db-instances-0.4.0.tgz b/helm/db-instances-0.4.0.tgz new file mode 100644 index 00000000..6524575c Binary files /dev/null and b/helm/db-instances-0.4.0.tgz differ diff --git a/helm/db-instances/Chart.yaml b/helm/db-instances/Chart.yaml index bf93caa1..ae92f808 100644 --- a/helm/db-instances/Chart.yaml +++ b/helm/db-instances/Chart.yaml @@ -2,4 +2,4 @@ apiVersion: v1 appVersion: "1.0" description: Database Instances for db operator name: db-instances -version: 0.3.1 +version: 0.4.0 diff --git a/helm/db-instances/templates/dbinstance.yaml b/helm/db-instances/templates/dbinstance.yaml index c7b19162..d5ee8783 100644 --- a/helm/db-instances/templates/dbinstance.yaml +++ b/helm/db-instances/templates/dbinstance.yaml @@ -52,6 +52,21 @@ spec: backupHost: {{ $value.generic.backupHost }} {{- end }} {{- end }} + {{- if $value.percona }} + percona: + servers: + {{- range $server := $value.percona.servers }} + - host: {{ $server.host }} + port: {{ $server.port }} + maxConn: {{ $server.maxConn }} + {{- if $server.readonly }} + readonly: {{ $server.readonly }} + {{- end }} + {{- end }} + monitorUserSecretRef: + Name: {{ $name }}-monitoruser-secret + Namespace: {{ $operatorNs }} + {{- end }} {{- if not $value.existingAdminSecret }} --- apiVersion: v1 @@ -83,5 +98,20 @@ data: config: | {{ $value.google.configMap.data | indent 4 }} {{- end }} +{{- if $value.percona }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $name }}-monitoruser-secret + namespace: {{ $operatorNs }} + labels: + chart: {{ $chart }} + release: {{ $release.Name }} + heritage: {{ $heritage }} +type: Opaque +data: + user: {{ $value.percona.monitoruser.name | b64enc }} + password: {{ $value.percona.monitoruser.password | b64enc }} +{{- end }} {{- end }} {{- end }} \ No newline at end of file diff --git a/helm/db-operator-0.3.0.tgz b/helm/db-operator-0.3.0.tgz new file mode 100644 index 00000000..0ab88ff8 Binary files /dev/null and b/helm/db-operator-0.3.0.tgz differ diff --git a/helm/db-operator/Chart.yaml b/helm/db-operator/Chart.yaml index cb11ee27..ba9d2606 100644 --- a/helm/db-operator/Chart.yaml +++ b/helm/db-operator/Chart.yaml @@ -2,4 +2,4 @@ apiVersion: v1 appVersion: "1.0" description: A Database Operator name: db-operator -version: 0.2.0 +version: 0.3.0 diff --git a/helm/db-operator/templates/config.yaml b/helm/db-operator/templates/config.yaml index 16a39437..186f1cb6 100644 --- a/helm/db-operator/templates/config.yaml +++ b/helm/db-operator/templates/config.yaml @@ -17,6 +17,9 @@ data: {{ toYaml .Values.config.instance.google.proxy.nodeSelector | indent 10 }} image: {{ .Values.config.instance.google.proxy.image }} generic: {} + percona: + proxy: + image: {{ .Values.config.instance.percona.proxy.image }} backup: nodeSelector: {{ toYaml .Values.config.backup.nodeSelector | indent 8 }} diff --git a/helm/db-operator/templates/crd.yaml b/helm/db-operator/templates/crd.yaml index 18e9f7ba..d69036aa 100644 --- a/helm/db-operator/templates/crd.yaml +++ b/helm/db-operator/templates/crd.yaml @@ -68,60 +68,100 @@ spec: subresources: status: {} preserveUnknownFields: true - # validation: - # openAPIV3Schema: - # description: DbInstance is the Schema for the dbinstances API - # properties: - # apiVersion: - # description: 'APIVersion defines the versioned schema of this representation - # of an object. Servers should convert recognized schemas to the latest - # internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources' - # type: string - # kind: - # description: 'Kind is a string value representing the REST resource this - # object represents. Servers may infer this from the endpoint the client - # submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds' - # type: string - # metadata: - # type: object - # spec: - # description: DbInstanceSpec defines the desired state of DbInstance - # type: object - # properties: - # engine: - # type: string - # adminSecretRef: - # type: map - # properties: - # Namespace: - # type: string - # Name: - # type: string - # backup: - # type: object - # generic: - # type: object - # properties: - # host: - # type: string - # port: - # type: integer - # google: - # type: object - # properties: - # instance: - # type: string - # configmapRef: - # type: map - # properties: - # Namespace: - # type: string - # Name: - # type: string - # status: - # description: DbInstanceStatus defines the observed state of DbInstance - # type: object - # type: object + validation: + openAPIV3Schema: + description: DbInstance is the Schema for the dbinstances API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: DbInstanceSpec defines the desired state of DbInstance + type: object + required: + - engine + - adminSecretRef + oneOf: + - required: + - percona + - required: + - google + - required: + - generic + properties: + engine: + type: string + adminSecretRef: + type: object + properties: + Namespace: + type: string + Name: + type: string + backup: + type: object + generic: + type: object + required: + - host + - port + properties: + host: + type: string + port: + type: integer + google: + type: object + required: + - instance + properties: + instance: + type: string + configmapRef: + type: object + properties: + Namespace: + type: string + Name: + type: string + percona: + type: object + required: + - servers + - monitorUserSecretRef + properties: + servers: + type: array + items: + required: + - host + - port + - maxConn + properties: + host: + type: string + port: + type: integer + maxConn: + type: integer + minimum: 1 + readonly: + type: boolean + monitorUserSecretRef: + type: object + status: + description: DbInstanceStatus defines the observed state of DbInstance + type: object + type: object version: v1alpha1 versions: - name: v1alpha1 diff --git a/helm/db-operator/values.yaml b/helm/db-operator/values.yaml index 338e05bb..fb63c96a 100644 --- a/helm/db-operator/values.yaml +++ b/helm/db-operator/values.yaml @@ -33,6 +33,9 @@ config: nodeSelector: {} image: gcr.io/cloudsql-docker/gce-proxy:1.16 generic: {} + percona: + proxy: + image: severalnines/proxysql:2.0 backup: nodeSelector: {} postgres: diff --git a/helm/index.yaml b/helm/index.yaml index b85dd0be..a148e07f 100644 --- a/helm/index.yaml +++ b/helm/index.yaml @@ -3,7 +3,16 @@ entries: db-instances: - apiVersion: v1 appVersion: "1.0" - created: "2020-05-04T13:32:38.190474+02:00" + created: "2020-06-04T11:04:09.992748+02:00" + description: Database Instances for db operator + digest: b039af38554da0fc8037b16397d49757d2c0b1a9e652867309df81b45950f375 + name: db-instances + urls: + - https://kloeckner-i.github.io/db-operator/helm/db-instances-0.4.0.tgz + version: 0.4.0 + - apiVersion: v1 + appVersion: "1.0" + created: "2020-06-04T11:04:09.992512+02:00" description: Database Instances for db operator digest: 3593c0f28606ed4ee028d779ce70b23e257f85d8c96f93449ed4deb25bcea208 name: db-instances @@ -12,7 +21,7 @@ entries: version: 0.3.1 - apiVersion: v1 appVersion: "1.0" - created: "2020-05-04T13:32:38.189679+02:00" + created: "2020-06-04T11:04:09.992029+02:00" description: Database Instances for db operator digest: 6abeb95a030681041d41bb416624846819a7fb284a1f9b8cd8ef00d4517343e4 name: db-instances @@ -21,7 +30,7 @@ entries: version: 0.3.0 - apiVersion: v1 appVersion: "1.0" - created: "2020-05-04T13:32:38.189208+02:00" + created: "2020-06-04T11:04:09.991531+02:00" description: Database Instances for db operator digest: ba545b09123c5cf0276da24cf16fcaeae1d51a92e253731768b35478fb0294f4 name: db-instances @@ -30,7 +39,7 @@ entries: version: 0.2.0 - apiVersion: v1 appVersion: "1.0" - created: "2020-05-04T13:32:38.188659+02:00" + created: "2020-06-04T11:04:09.99106+02:00" description: Database Instances for db operator digest: 0a9f5c620c9ee8d8e33ea96f797be989efab296fbeeca24e26c84fdb2136d704 name: db-instances @@ -40,7 +49,16 @@ entries: db-operator: - apiVersion: v1 appVersion: "1.0" - created: "2020-05-04T13:32:38.193207+02:00" + created: "2020-06-04T11:04:09.995848+02:00" + description: A Database Operator + digest: 7022f5c9cf4e1704bc06dafc4f3dfc662421b2ecb918eed0b0a601acca860bc9 + name: db-operator + urls: + - https://kloeckner-i.github.io/db-operator/helm/db-operator-0.3.0.tgz + version: 0.3.0 + - apiVersion: v1 + appVersion: "1.0" + created: "2020-06-04T11:04:09.995516+02:00" description: A Database Operator digest: a00294defa1914f326b363c1cb0d36b22297fefd71e72a83510c70a1a8020d31 name: db-operator @@ -49,7 +67,7 @@ entries: version: 0.2.0 - apiVersion: v1 appVersion: "1.0" - created: "2020-05-04T13:32:38.192471+02:00" + created: "2020-06-04T11:04:09.994767+02:00" description: A Database Operator digest: d2d2a7f6b7913585283d72cea95770067e6c9625479e349c2e18b7dc3fbd3a52 name: db-operator @@ -58,11 +76,11 @@ entries: version: 0.1.1 - apiVersion: v1 appVersion: "1.0" - created: "2020-05-04T13:32:38.191438+02:00" + created: "2020-06-04T11:04:09.993578+02:00" description: A Database Operator digest: 6f41f18fe10b74edaec021defdcca99e2c65b5ed60b3c53b3d7ee7b124ccfeb0 name: db-operator urls: - https://kloeckner-i.github.io/db-operator/helm/db-operator-0.1.0.tgz version: 0.1.0 -generated: "2020-05-04T13:32:38.187967+02:00" +generated: "2020-06-04T11:04:09.990427+02:00" diff --git a/integration/mysql/Chart.yaml b/integration/mysql-generic/Chart.yaml similarity index 72% rename from integration/mysql/Chart.yaml rename to integration/mysql-generic/Chart.yaml index c637a6a1..0a3ba27c 100644 --- a/integration/mysql/Chart.yaml +++ b/integration/mysql-generic/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v1 appVersion: "1.0" description: Integration test for db operator mysql -name: test-mysql-db-operator +name: test-mysql-generic-db-operator version: 0.1.0 diff --git a/integration/mysql/scripts/test_read.sh b/integration/mysql-generic/scripts/test_read.sh similarity index 100% rename from integration/mysql/scripts/test_read.sh rename to integration/mysql-generic/scripts/test_read.sh diff --git a/integration/mysql/scripts/test_write.sh b/integration/mysql-generic/scripts/test_write.sh similarity index 100% rename from integration/mysql/scripts/test_write.sh rename to integration/mysql-generic/scripts/test_write.sh diff --git a/integration/mysql-generic/templates/db.yaml b/integration/mysql-generic/templates/db.yaml new file mode 100644 index 00000000..efcb0765 --- /dev/null +++ b/integration/mysql-generic/templates/db.yaml @@ -0,0 +1,11 @@ +apiVersion: "kci.rocks/v1alpha1" +kind: "Database" +metadata: + name: {{ .Values.db.name }} + labels: + env: test +spec: + secretName: {{ .Values.db.name }}-credentials # where to save db name user, password for application + instance: {{ .Values.instance.name }} + backup: + enable: false diff --git a/integration/mysql/templates/instance.yaml b/integration/mysql-generic/templates/instance.yaml similarity index 77% rename from integration/mysql/templates/instance.yaml rename to integration/mysql-generic/templates/instance.yaml index 7b2ff9ee..61913191 100644 --- a/integration/mysql/templates/instance.yaml +++ b/integration/mysql-generic/templates/instance.yaml @@ -1,11 +1,11 @@ apiVersion: kci.rocks/v1alpha1 kind: DbInstance metadata: - name: my-local-test + name: {{ .Values.instance.name }} spec: adminSecretRef: Namespace: {{ .Release.Namespace }} - Name: my-local-db-password + Name: {{ .Values.instance.name }}-admin-password engine: mysql generic: host: {{ .Values.mysql.serviceName }}.{{ .Release.Namespace }} @@ -16,7 +16,7 @@ spec: apiVersion: v1 kind: Secret metadata: - name: my-local-db-password + name: {{ .Values.instance.name }}-admin-password type: Opaque data: password: {{ .Values.mysql.adminPassword | b64enc }} diff --git a/integration/mysql/templates/mysql-server.yaml b/integration/mysql-generic/templates/mysql-server.yaml similarity index 100% rename from integration/mysql/templates/mysql-server.yaml rename to integration/mysql-generic/templates/mysql-server.yaml diff --git a/integration/mysql/templates/test/test.yaml b/integration/mysql-generic/templates/test/test.yaml similarity index 79% rename from integration/mysql/templates/test/test.yaml rename to integration/mysql-generic/templates/test/test.yaml index f2cb30d1..0a0c1a63 100644 --- a/integration/mysql/templates/test/test.yaml +++ b/integration/mysql-generic/templates/test/test.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Pod metadata: - name: mysql-tester-app-{{ randAlphaNum 5 | lower }} + name: {{ .Values.db.name }}-tester-app-{{ randAlphaNum 5 | lower }} annotations: "helm.sh/hook": test-success labels: @@ -21,17 +21,17 @@ spec: - name: MYSQL_USERNAME valueFrom: secretKeyRef: - name: my-db-test-credentials + name: {{ .Values.db.name }}-credentials key: USER - name: MYSQL_DB valueFrom: secretKeyRef: - name: my-db-test-credentials + name: {{ .Values.db.name }}-credentials key: DB - name: MYSQL_HOST valueFrom: configMapKeyRef: - name: my-db-test-credentials + name: {{ .Values.db.name }}-credentials key: DB_CONN volumeMounts: - name: db-secret @@ -58,17 +58,17 @@ spec: - name: MYSQL_USERNAME valueFrom: secretKeyRef: - name: my-db-test-credentials + name: {{ .Values.db.name }}-credentials key: USER - name: MYSQL_DB valueFrom: secretKeyRef: - name: my-db-test-credentials + name: {{ .Values.db.name }}-credentials key: DB - name: MYSQL_HOST valueFrom: configMapKeyRef: - name: my-db-test-credentials + name: {{ .Values.db.name }}-credentials key: DB_CONN volumeMounts: - name: db-secret @@ -87,9 +87,9 @@ spec: volumes: - name: db-secret secret: - secretName: my-db-test-credentials + secretName: {{ .Values.db.name }}-credentials - name: script configMap: - name: mysql-test-script + name: {{ .Values.db.name }}-test-script - name: shared-data emptyDir: {} \ No newline at end of file diff --git a/integration/mysql/templates/testscript.yaml b/integration/mysql-generic/templates/testscript.yaml similarity index 80% rename from integration/mysql/templates/testscript.yaml rename to integration/mysql-generic/templates/testscript.yaml index 26f56cf6..c58e6996 100644 --- a/integration/mysql/templates/testscript.yaml +++ b/integration/mysql-generic/templates/testscript.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: ConfigMap metadata: - name: mysql-test-script + name: {{ .Values.db.name }}-test-script data: write.sh: | {{ .Files.Get "scripts/test_write.sh" | indent 4}} diff --git a/integration/mysql-generic/values.yaml b/integration/mysql-generic/values.yaml new file mode 100644 index 00000000..739e8614 --- /dev/null +++ b/integration/mysql-generic/values.yaml @@ -0,0 +1,11 @@ +mysql: + serviceName: my-generic-server + adminUser: root + adminPassword: test1234 + image: mysql:5.7 + +instance: + name: my-generic-instance + +db: + name: my-generic-db \ No newline at end of file diff --git a/integration/mysql-percona/.gitignore b/integration/mysql-percona/.gitignore new file mode 100644 index 00000000..eb3b48f0 --- /dev/null +++ b/integration/mysql-percona/.gitignore @@ -0,0 +1,2 @@ +charts/ +requirements.lock \ No newline at end of file diff --git a/integration/mysql-percona/Chart.yaml b/integration/mysql-percona/Chart.yaml new file mode 100644 index 00000000..9aaddfab --- /dev/null +++ b/integration/mysql-percona/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: Integration test for db operator mysql percona instance type +name: test-mysql-percona-db-operator +version: 0.1.0 \ No newline at end of file diff --git a/integration/mysql-percona/percona-xtradb-cluster/.helmignore b/integration/mysql-percona/percona-xtradb-cluster/.helmignore new file mode 100755 index 00000000..f0c13194 --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/integration/mysql-percona/percona-xtradb-cluster/Chart.yaml b/integration/mysql-percona/percona-xtradb-cluster/Chart.yaml new file mode 100755 index 00000000..9923ab64 --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/Chart.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +appVersion: 5.7.19 +description: free, fully compatible, enhanced, open source drop-in replacement for + MySQL with Galera Replication (xtradb) +engine: gotpl +home: https://www.percona.com/ +icon: https://www.percona.com/sites/all/themes/percona2015/logo.png +keywords: +- mysql +- percona +- database +- sql +- xtradb +- galera +- wsrep +maintainers: +- email: username.taken@gmail.com + name: paulczar +name: percona-xtradb-cluster +sources: +- https://github.com/kubernetes/charts +- https://github.com/percona-lab/percona-docker/ +version: 1.0.3 diff --git a/integration/mysql-percona/percona-xtradb-cluster/OWNERS b/integration/mysql-percona/percona-xtradb-cluster/OWNERS new file mode 100755 index 00000000..4abdb65b --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/OWNERS @@ -0,0 +1,8 @@ +approvers: +- paulczar +- maver1ck +reviewers: +- paulczar +- maver1ck + + diff --git a/integration/mysql-percona/percona-xtradb-cluster/README.md b/integration/mysql-percona/percona-xtradb-cluster/README.md new file mode 100755 index 00000000..c70f64e0 --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/README.md @@ -0,0 +1,198 @@ +# Percona XtraDB Cluster + +[Percona Server](https://MySQL.org) for MySQL® is a free, fully compatible, enhanced, open source drop-in replacement for MySQL that provides superior performance, scalability and instrumentation. With over 3,000,000 downloads, Percona Server for MySQL's self-tuning algorithms and support for extremely high-performance hardware delivers excellent performance and reliability. + +Notable users include Netflix, Amazon Web Services, Alcatel-Lucent, and Smug Mug. + +## Introduction + +This chart, based off of the Percona chart (which in turn is based off the MySQL chart), bootstraps a multi-node Percona XtraDB Cluster deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. + +The chart exploits the deterministic nature of StatefulSet and KubeDNS to ensure the cluster bootstrap is performed in the correct order. + +## Prerequisites + +- Kubernetes 1.8+ with Beta APIs enabled +- PV provisioner support in the underlying infrastructure + +## Installing the Chart + +To install the chart with the release name `my-release`: + +```bash +$ helm install --name my-release stable/percona-xtradb-cluster +``` + +The command deploys a Percona XtraDB Cluster on the Kubernetes cluster in the default configuration. The [configuration](#configuration) section lists the parameters that can be configured during installation. + +The root password can only be used inside each `pod`. You should set a default `mysqlDatabase`, `mysqlUser` and `mysqlPassword` in the values.yaml file. + +By default an **insecure** password will be generated for the root and replication users. If you'd like to set your own password change the `mysqlRootPassword` or `xtraBackupPassword` respectively +in the values.yaml. + +You can retrieve your root password (usable only via localhost in each pod) by running the following command. Make sure to replace [YOUR_RELEASE_NAME]: + + printf $(printf '\%o' `kubectl get secret [YOUR_RELEASE_NAME]-percona -o jsonpath="{.data.mysql-root-password[*]}"`) + +> **Tip**: List all releases using `helm list` + +## Uninstalling the Chart + +To uninstall/delete the `my-release` deployment: + +```bash +$ helm delete my-release +``` + +The command removes all the Kubernetes components associated with the chart and deletes the release. + +## Configuration + +The following table lists the configurable parameters of the Percona chart and their default values. + +| Parameter | Description | Default | +| ----------------------- | ---------------------------------- | ---------------------------------------------------------- | +| `image.repository` | `percona-xtradb-cluster` image Repo. | 5.7.19 release | +| `image.tag` | `percona-xtradb-cluster` image tag. | `percona/percona-xtradb-cluster` | +| `image.pullPolicy` | Image pull policy | `IfNotPresent` | +| `replicas` | Number of pods to join the Percona XtraDB Cluster | 3 | +| `allowRootFrom` | Remote hosts to allow root access, set to `127.0.0.1` to disable remote root | `%` | +| `mysqlRootPassword` | Password for the `root` user. | `not-a-secure-password` | +| `xtraBackupPassword` | Password for the `xtrabackup` user. | `replicate-my-data` | +| `pxc_strict_mode` | Setting for `pxc_strict_mode`. | ENFORCING | +| `mysqlUser` | Username of new user to create. | `nil` | +| `mysqlPassword` | Password for the new user. | `nil` | +| `mysqlDatabase` | Name for new database to create. | `nil` | +| `persistence.enabled` | Create a volume to store data | false | +| `persistence.size` | Size of persistent volume claim | 8Gi RW | +| `persistence.storageClass` | Type of persistent volume claim | nil (uses alpha storage class annotation) | +| `persistence.accessMode` | ReadWriteOnce or ReadOnly | ReadWriteOnce | +| `tolerations` | Node labels for pod assignment | `[]` | +| `nodeSelector` | Node labels for pod assignment | `{}` | +| `podAnnotations` | Pod annotations | `{}` | +| `resources` | CPU/Memory resource requests/limits | Memory: `256Mi`, CPU: `100m` | +| `configFiles` | files to write to /etc/mysql/conf.d | see values.yaml | +| `ssl.enabled` | Setup and use SSL for MySQL connections | `false` | +| `ssl.secret` | Name of the secret containing the SSL certificates | mysql-ssl-certs | +| `ssl.certificates[0].name` | Name of the secret containing the SSL certificates | `nil` | +| `ssl.certificates[0].ca` | CA certificate | `nil` | +| `ssl.certificates[0].cert` | Server certificate (public key) | `nil` | +| `ssl.certificates[0].key` | Server key (private key) | `nil` | +| `logTail` | if set to true runs a container to tail /var/log/mysqld.log in the pod | true | +| `metricsExporter.enabled` | if set to true runs a [mysql metrics exporter](https://github.com/prometheus/mysqld_exporter) container in the pod | false | +| `metricsExporter.commandOverrides` | Overrides default docker command for metrics exporter | `[]` | +| `metricsExporter.argsOverrides` | Overrides default docker args for metrics exporter | `[]` | +| `metricsExporter.tag` | Specify a docker image tag for `prom/mysqld-exporter` metrics exporter docker image | `nil` | +| `prometheus.operator.enabled` | Setting to true will create Prometheus-Operator specific resources | `false` | +| `prometheus.operator.prometheusRule.enabled` | Create default alerting rules | `true` | +| `prometheus.operator.prometheusRule.labels` | Labels to add to alerts | `{}` | +| `prometheus.operator.prometheusRule.namespace` | Namespace which Prometheus is installed in | `nil` | +| `prometheus.operator.prometheusRule.selector` | Label Selector for Prometheus to find ServiceMonitors | `nil` | +| `prometheus.operator.serviceMonitor.interval` | Interval at which Prometheus will scrape metrics exporter | `10s` | +| `prometheus.operator.serviceMonitor.namespace` | Namespace which Prometheus is installed in | `nil` | +| `prometheus.operator.serviceMonitor.selector` | Label Selector for Prometheus to find ServiceMonitors | `nil` | +| `podDisruptionBudget` | Pod disruption budget | `{enabled: false, maxUnavailable: 1}` | +| `service.percona.headless` | if set to true makes the percona service [headless](https://kubernetes.io/docs/concepts/services-networking/service/#headless-services) | false | + + +Some of the parameters above map to the env variables defined in the [Percona XtraDB Cluster DockerHub image](https://hub.docker.com/r/percona/percona-xtradb-cluster/). + +Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, + +```bash +$ helm install --name my-release \ + --set mysqlRootPassword=secretpassword,mysqlUser=my-user,mysqlPassword=my-password,mysqlDatabase=my-database \ + stable/percona-xtradb-cluster +``` + +The above command sets the MySQL `root` account password to `secretpassword`. Additionally it creates a standard database user named `my-user`, with the password `my-password`, who has access to a database named `my-database`. + +Alternatively, a YAML file that specifies the values for the parameters can be provided while installing the chart. For example, + +```bash +$ helm install --name my-release -f values.yaml stable/percona-xtradb-cluster +``` + +> **Tip**: You can use the default [values.yaml](values.yaml) + +## Persistence + +The [Percona XtraDB Cluster DockerHub image](https://hub.docker.com/r/percona/percona-xtradb-cluster/) stores the MySQL data and configurations at the `/var/lib/mysql` path of the container. + +By default, an emptyDir volume is mounted at that location. + +> *"An emptyDir volume is first created when a Pod is assigned to a Node, and exists as long as that Pod is running on that node. When a Pod is removed from a node for any reason, the data in the emptyDir is deleted forever."* + +You can change the values.yaml to enable persistence and use a PersistentVolumeClaim instead. + +## SSL + +This chart supports configuring MySQL to use [encrypted connections](https://dev.mysql.com/doc/refman/5.7/en/encrypted-connections.html) with TLS/SSL certificates provided by the user. This is accomplished by storing the required Certificate Authority file, the server public key certificate, and the server private key as a Kubernetes secret. The SSL options for this chart support the following use cases: + +* Manage certificate secrets with helm +* Manage certificate secrets outside of helm + +## Manage certificate secrets with helm + +Include your certificate data in the `ssl.certificates` section. For example: + +``` +ssl: + enabled: false + secret: mysql-ssl-certs + certificates: + - name: mysql-ssl-certs + ca: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- + cert: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- + key: |- + -----BEGIN RSA PRIVATE KEY----- + ... + -----END RSA PRIVATE KEY----- +``` + +> **Note**: Make sure your certificate data has the correct formatting in the values file. + +## Manage certificate secrets outside of helm + +1. Ensure the certificate secret exist before installation of this chart. +2. Set the name of the certificate secret in `ssl.secret`. +3. Make sure there are no entries underneath `ssl.certificates`. + +To manually create the certificate secret from local files you can execute: +``` +kubectl create secret generic mysql-ssl-certs \ + --from-file=ca.pem=./ssl/certificate-authority.pem \ + --from-file=server-cert.pem=./ssl/server-public-key.pem \ + --from-file=server-key.pem=./ssl/server-private-key.pem +``` +> **Note**: `ca.pem`, `server-cert.pem`, and `server-key.pem` **must** be used as the key names in this generic secret. + +If you are using a certificate your configurationFiles must include the three ssl lines under [mysqld] + +``` +[mysqld] + ssl-ca=/ssl/ca.pem + ssl-cert=/ssl/server-cert.pem + ssl-key=/ssl/server-key.pem +``` + +## PXC Strict Mode + +PXC Strict Mode is designed to avoid the use of experimental and unsupported features in Percona XtraDB Cluster. It performs a number of validations at startup and during runtime. + +Depending on the actual mode you select, upon encountering a failed validation, the server will either throw an error (halting startup or denying the operation), or log a warning and continue running as normal. The following modes are available: + +* DISABLED: Do not perform strict mode validations and run as normal. +* PERMISSIVE: If a vaidation fails, log a warning and continue running as normal. +* ENFORCING: If a validation fails during startup, halt the server and throw an error. If a validation fails during runtime, deny the operation and throw an error. +* MASTER: The same as ENFORCING except that the validation of explicit table locking is not performed. This mode can be used with clusters in which write operations are isolated to a single node. + +By default, PXC Strict Mode is set to ENFORCING, except if the node is acting as a standalone server or the node is bootstrapping, then PXC Strict Mode defaults to DISABLED. + +Source: https://www.percona.com/doc/percona-xtradb-cluster/LATEST/features/pxc-strict-mode.html diff --git a/integration/mysql-percona/percona-xtradb-cluster/files/entrypoint.sh b/integration/mysql-percona/percona-xtradb-cluster/files/entrypoint.sh new file mode 100755 index 00000000..91b0fca5 --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/files/entrypoint.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -e + +if [[ -n "${DEBUG}" ]]; then + set -x +fi + +. /startup-scripts/functions.sh + +ipaddr=$(hostname -i | awk ' { print $1 } ') +hostname=$(hostname) +echo "I AM $hostname - $ipaddr" + +# if command starts with an option, prepend mysqld +if [ "${1:0:1}" = '-' ]; then + CMDARG="$@" +fi + +cluster_join=$(resolveip -s "${K8S_SERVICE_NAME}" || echo "") +if [[ -z "${cluster_join}" ]]; then + echo "I am the Primary Node" + init_mysql + write_password_file + exec mysqld --user=mysql --wsrep_cluster_name=$SHORT_CLUSTER_NAME --wsrep_node_name=$hostname \ + --wsrep_cluster_address=gcomm:// --wsrep_sst_method=xtrabackup-v2 \ + --wsrep_sst_auth="xtrabackup:$XTRABACKUP_PASSWORD" \ + --wsrep_node_address="$ipaddr" --pxc_strict_mode="$PXC_STRICT_MODE" $CMDARG +else + echo "I am not the Primary Node" + chown -R mysql:mysql /var/lib/mysql || true # default is root:root 777 + touch /var/log/mysqld.log + chown mysql:mysql /var/log/mysqld.log + write_password_file + exec mysqld --user=mysql --wsrep_cluster_name=$SHORT_CLUSTER_NAME --wsrep_node_name=$hostname \ + --wsrep_cluster_address="gcomm://$cluster_join" --wsrep_sst_method=xtrabackup-v2 \ + --wsrep_sst_auth="xtrabackup:$XTRABACKUP_PASSWORD" \ + --wsrep_node_address="$ipaddr" --pxc_strict_mode="$PXC_STRICT_MODE" $CMDARG +fi diff --git a/integration/mysql-percona/percona-xtradb-cluster/files/functions.sh b/integration/mysql-percona/percona-xtradb-cluster/files/functions.sh new file mode 100755 index 00000000..83f71ada --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/files/functions.sh @@ -0,0 +1,117 @@ +#!/bin/bash + +write_password_file() { + if [[ -n "${MYSQL_ROOT_PASSWORD}" ]]; then + cat < /root/.my.cnf + [client] + user=root + password=${MYSQL_ROOT_PASSWORD} +EOF + fi +} + +init_mysql() { + DATADIR=/var/lib/mysql + # if we have CLUSTER_JOIN - then we do not need to perform datadir initialize + # the data will be copied from another node + if [ ! -e "$DATADIR/mysql" ]; then + if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" -a -z "$MYSQL_RANDOM_ROOT_PASSWORD" -a -z "$MYSQL_ROOT_PASSWORD_FILE" ]; then + echo >&2 'error: database is uninitialized and password option is not specified ' + echo >&2 ' You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ROOT_PASSWORD_FILE, MYSQL_ALLOW_EMPTY_PASSWORD or MYSQL_RANDOM_ROOT_PASSWORD' + exit 1 + fi + + if [ ! -z "$MYSQL_ROOT_PASSWORD_FILE" -a -z "$MYSQL_ROOT_PASSWORD" ]; then + MYSQL_ROOT_PASSWORD=$(cat $MYSQL_ROOT_PASSWORD_FILE) + fi + mkdir -p "$DATADIR" + + echo "Running --initialize-insecure on $DATADIR" + ls -lah $DATADIR + if [ "$PERCONA_MAJOR" = "5.6" ]; then + mysql_install_db --user=mysql --datadir="$DATADIR" + else + mysqld --user=mysql --datadir="$DATADIR" --initialize-insecure + fi + chown -R mysql:mysql "$DATADIR" || true # default is root:root 777 + if [ -f /var/log/mysqld.log ]; then + chown mysql:mysql /var/log/mysqld.log + fi + echo 'Finished --initialize-insecure' + + mysqld --user=mysql --datadir="$DATADIR" --skip-networking & + pid="$!" + + mysql=( mysql --protocol=socket -uroot ) + + for i in {30..0}; do + if echo 'SELECT 1' | "${mysql[@]}" &> /dev/null; then + break + fi + echo 'MySQL init process in progress...' + sleep 1 + done + + if [ "$i" = 0 ]; then + echo >&2 'MySQL init process failed.' + exit 1 + fi + + # sed is for https://bugs.mysql.com/bug.php?id=20545 + mysql_tzinfo_to_sql /usr/share/zoneinfo | sed 's/Local time zone must be set--see zic manual page/FCTY/' | "${mysql[@]}" mysql + "${mysql[@]}" <<-EOSQL + -- What's done in this file shouldn't be replicated + -- or products like mysql-fabric won't work + SET @@SESSION.SQL_LOG_BIN=0; + CREATE USER 'root'@'${ALLOW_ROOT_FROM}' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ; + GRANT ALL ON *.* TO 'root'@'${ALLOW_ROOT_FROM}' WITH GRANT OPTION ; + GRANT ALL ON *.* TO 'root'@'localhost' WITH GRANT OPTION ; + CREATE USER 'xtrabackup'@'localhost' IDENTIFIED BY '$XTRABACKUP_PASSWORD'; + GRANT RELOAD,PROCESS,LOCK TABLES,REPLICATION CLIENT ON *.* TO 'xtrabackup'@'localhost'; + GRANT REPLICATION CLIENT ON *.* TO monitor@'%' IDENTIFIED BY 'monitor'; + GRANT PROCESS ON *.* TO monitor@localhost IDENTIFIED BY 'monitor'; + CREATE USER 'mysql'@'localhost' IDENTIFIED BY '' ; + DROP DATABASE IF EXISTS test ; + FLUSH PRIVILEGES ; +EOSQL + + if [ "$PERCONA_MAJOR" = "5.6" ]; then + echo "SET PASSWORD FOR 'root'@'localhost' = PASSWORD('${MYSQL_ROOT_PASSWORD}'); FLUSH PRIVILEGES;" | "${mysql[@]}" + else + echo "ALTER USER 'root'@'localhost' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}'; FLUSH PRIVILEGES;" | "${mysql[@]}" + fi + + if [ ! -z "$MYSQL_ROOT_PASSWORD" ]; then + mysql+=( -p"${MYSQL_ROOT_PASSWORD}" ) + fi + + if [ "$MYSQL_DATABASE" ]; then + echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" | "${mysql[@]}" + mysql+=( "$MYSQL_DATABASE" ) + fi + + if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then + echo "CREATE USER '"$MYSQL_USER"'@'%' IDENTIFIED BY '"$MYSQL_PASSWORD"' ;" | "${mysql[@]}" + + if [ "$MYSQL_DATABASE" ]; then + echo "GRANT ALL ON \`"$MYSQL_DATABASE"\`.* TO '"$MYSQL_USER"'@'%' ;" | "${mysql[@]}" + fi + + echo 'FLUSH PRIVILEGES ;' | "${mysql[@]}" + fi + + if [ ! -z "$MYSQL_ONETIME_PASSWORD" ]; then + "${mysql[@]}" <<-EOSQL + ALTER USER 'root'@'%' PASSWORD EXPIRE; +EOSQL + fi + if ! kill -s TERM "$pid" || ! wait "$pid"; then + echo >&2 'MySQL init process failed.' + exit 1 + fi + + echo + echo 'MySQL init process done. Ready for start up.' + echo + fi +} diff --git a/integration/mysql-percona/percona-xtradb-cluster/files/node.cnf b/integration/mysql-percona/percona-xtradb-cluster/files/node.cnf new file mode 100755 index 00000000..952ebacd --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/files/node.cnf @@ -0,0 +1,13 @@ +[mysqld] +datadir=/var/lib/mysql +default_storage_engine=InnoDB +binlog_format=ROW +innodb_flush_log_at_trx_commit = 0 +innodb_flush_method = O_DIRECT +innodb_file_per_table = 1 +innodb_autoinc_lock_mode=2 +bind_address = 0.0.0.0 +wsrep_slave_threads=2 +wsrep_cluster_address=gcomm:// +wsrep_provider=/usr/lib/galera3/libgalera_smm.so +wsrep_sst_method=xtrabackup-v2 \ No newline at end of file diff --git a/integration/mysql-percona/percona-xtradb-cluster/templates/NOTES.txt b/integration/mysql-percona/percona-xtradb-cluster/templates/NOTES.txt new file mode 100755 index 00000000..7867dba6 --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/templates/NOTES.txt @@ -0,0 +1,35 @@ +Percona can be accessed via port 3306 on the following DNS name from within your cluster: +{{ template "percona-xtradb-cluster.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local + +To get your root password run (this password can only be used from inside the container): + + $ kubectl get secret --namespace {{ .Release.Namespace }} {{ template "percona-xtradb-cluster.fullname" . }} -o jsonpath="{.data.mysql-root-password}" | base64 --decode; echo + +To get your xtradb backup password run: + + $ kubectl get secret --namespace {{ .Release.Namespace }} {{ template "percona-xtradb-cluster.fullname" . }} -o jsonpath="{.data.xtrabackup-password}" | base64 --decode; echo + +To check the size of the xtradb cluster: + + $ kubectl exec --namespace {{ .Release.Namespace }} -ti {{ template "percona-xtradb-cluster.fullname" . }}-0 -c database -- mysql -e "SHOW GLOBAL STATUS LIKE 'wsrep_cluster_size'" + +To connect to your database: + +1. Run a command in the first pod in the StatefulSet: + + $ kubectl exec --namespace {{ .Release.Namespace }} -ti {{ template "percona-xtradb-cluster.fullname" . }}-0 -c database -- mysql + +{{- if .Values.mysqlUser }}{{ if .Values.mysqlPassword }}{{ if .Values.mysqlDatabase }} + +2. Run a percona pod that you can use as a client: + + $ kubectl run -i --tty --rm percona-client --image=percona:{{ .Values.image.tag }} --restart=Never -- mysql -h {{ template "percona-xtradb-cluster.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local -u{{ .Values.mysqlUser }} \ + -p$(kubectl get secret --namespace {{ .Release.Namespace }} {{ template "percona-xtradb-cluster.fullname" . }} -o jsonpath="{.data.mysql-password}" | base64 --decode; echo) \ + {{ .Values.mysqlDatabase }} +{{ end }}{{ end }}{{ end }} + +{{- if .Values.logTail }} +To view your Percona XtraDB Cluster logs run: + + $ kubectl logs -f {{ template "percona-xtradb-cluster.fullname" . }}-0 logs +{{ end }} diff --git a/integration/mysql-percona/percona-xtradb-cluster/templates/_helpers.tpl b/integration/mysql-percona/percona-xtradb-cluster/templates/_helpers.tpl new file mode 100755 index 00000000..ac633908 --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/templates/_helpers.tpl @@ -0,0 +1,25 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +*/}} +{{- define "percona-xtradb-cluster.name" -}} +{{- default "pxc" .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +*/}} +{{- define "percona-xtradb-cluster.fullname" -}} +{{- $name := default "pxc" .Values.nameOverride -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{/* +Create a short cluster name in order to fulfill Percona's `wsrep_cluster_name` variable max size. +https://www.percona.com/doc/percona-xtradb-cluster/LATEST/wsrep-system-index.html#wsrep_cluster_name +*/}} +{{- define "percona-xtradb-cluster.shortname" -}} +{{- $name := default "pxc" .Values.nameOverride -}} +{{- printf "%s-%s" .Release.Name $name | trunc 32 | trimSuffix "-" -}} +{{- end -}} diff --git a/integration/mysql-percona/percona-xtradb-cluster/templates/config-map_mysql-config.yaml b/integration/mysql-percona/percona-xtradb-cluster/templates/config-map_mysql-config.yaml new file mode 100755 index 00000000..89f48dc8 --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/templates/config-map_mysql-config.yaml @@ -0,0 +1,20 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: {{ template "percona-xtradb-cluster.fullname" . }}-config-files + labels: + app: {{ template "percona-xtradb-cluster.fullname" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +data: +{{- if .Values.configFiles }} +{{ toYaml .Values.configFiles | indent 2 }} +{{ if not (index .Values.configFiles "node.cnf") }} + node.cnf: | +{{ .Files.Get "files/node.cnf" | indent 4 }} +{{ end }} +{{ else }} + node.cnf: | +{{ .Files.Get "files/node.cnf" | indent 4 }} +{{ end }} diff --git a/integration/mysql-percona/percona-xtradb-cluster/templates/config-map_startup-scripts.yaml b/integration/mysql-percona/percona-xtradb-cluster/templates/config-map_startup-scripts.yaml new file mode 100755 index 00000000..111f16be --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/templates/config-map_startup-scripts.yaml @@ -0,0 +1,14 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: {{ template "percona-xtradb-cluster.fullname" . }}-startup-scripts + labels: + app: {{ template "percona-xtradb-cluster.fullname" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +data: + entrypoint.sh: | +{{ .Files.Get "files/entrypoint.sh" | indent 4 }} + functions.sh: | +{{ .Files.Get "files/functions.sh" | indent 4 }} diff --git a/integration/mysql-percona/percona-xtradb-cluster/templates/pdb.yaml b/integration/mysql-percona/percona-xtradb-cluster/templates/pdb.yaml new file mode 100755 index 00000000..01bd17ec --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/templates/pdb.yaml @@ -0,0 +1,22 @@ +{{- if .Values.podDisruptionBudget.enabled }} +apiVersion: policy/v1beta1 +kind: PodDisruptionBudget +metadata: + labels: + app: {{ template "percona-xtradb-cluster.fullname" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + name: {{ template "percona-xtradb-cluster.fullname" . }}-pdb +spec: +{{- if .Values.podDisruptionBudget.minAvailable }} + minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} +{{- end }} +{{- if .Values.podDisruptionBudget.maxUnavailable }} + maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} +{{- end }} + selector: + matchLabels: + app: {{ template "percona-xtradb-cluster.fullname" . }} + release: {{ .Release.Name }} +{{- end }} diff --git a/integration/mysql-percona/percona-xtradb-cluster/templates/prometheusrule.yaml b/integration/mysql-percona/percona-xtradb-cluster/templates/prometheusrule.yaml new file mode 100755 index 00000000..fe07a7e3 --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/templates/prometheusrule.yaml @@ -0,0 +1,61 @@ +{{ if and .Values.metricsExporter.enabled .Values.prometheus.operator.enabled .Values.prometheus.operator.prometheusRule.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: {{ template "percona-xtradb-cluster.fullname" . }} +{{- if .Values.prometheus.operator.prometheusRule.namespace }} + namespace: {{ .Values.prometheus.operator.prometheusRule.namespace }} +{{- end }} + labels: + app: {{ template "percona-xtradb-cluster.fullname" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + heritage: "{{ .Release.Service }}" + release: "{{ .Release.Name }}" +{{- if .Values.prometheus.operator.prometheusRule.selector }} +{{ toYaml .Values.prometheus.operator.prometheusRule.selector | indent 4 }} +{{- end }} +spec: + groups: + - name: {{ template "percona-xtradb-cluster.fullname" . }}-alerts + rules: + - alert: MySQLGaleraNotReady + annotations: + message: Galera cluster node not ready + expr: mysql_global_status_wsrep_ready{service="{{ template "percona-xtradb-cluster.fullname" . }}-metrics"} != 1 + for: 5m + labels: + severity: critical +{{- if .Values.prometheus.operator.prometheusRule.labels }} +{{ toYaml .Values.prometheus.operator.prometheusRule.labels | indent 8 }} +{{- end }} + - alert: MySQLGaleraOutOfSync + annotations: + message: Galera cluster node out of sync + expr: (mysql_global_status_wsrep_local_state{service="{{ template "percona-xtradb-cluster.fullname" . }}-metrics"} != 4 and mysql_global_variables_wsrep_desync{service="{{ template "percona-xtradb-cluster.fullname" . }}-metrics"} == 0) + for: 5m + labels: + severity: critical +{{- if .Values.prometheus.operator.prometheusRule.labels }} +{{ toYaml .Values.prometheus.operator.prometheusRule.labels | indent 8 }} +{{- end }} + - alert: MySQLGaleraDonorFallingBehind + annotations: + message: xtradb cluster donor node falling behind + expr: (mysql_global_status_wsrep_local_state{service="{{ template "percona-xtradb-cluster.fullname" . }}-metrics"} == 2 and mysql_global_status_wsrep_local_recv_queue{service="{{ template "percona-xtradb-cluster.fullname" . }}-metrics"} > 100) + for: 5m + labels: + severity: warning +{{- if .Values.prometheus.operator.prometheusRule.labels }} +{{ toYaml .Values.prometheus.operator.prometheusRule.labels | indent 8 }} +{{- end }} + - alert: MySQLInnoDBLogWaits + annotations: + message: MySQL innodb log writes stalling + expr: rate(mysql_global_status_innodb_log_waits{service="{{ template "percona-xtradb-cluster.fullname" . }}-metrics"}[15m]) > 10 + for: 5m + labels: + severity: warning +{{- if .Values.prometheus.operator.prometheusRule.labels }} +{{ toYaml .Values.prometheus.operator.prometheusRule.labels | indent 8 }} +{{- end }} +{{ end }} diff --git a/integration/mysql-percona/percona-xtradb-cluster/templates/secrets.yaml b/integration/mysql-percona/percona-xtradb-cluster/templates/secrets.yaml new file mode 100755 index 00000000..0d150ed6 --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/templates/secrets.yaml @@ -0,0 +1,47 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "percona-xtradb-cluster.fullname" . }} + labels: + app: {{ template "percona-xtradb-cluster.fullname" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +type: Opaque +data: + {{ if .Values.mysqlRootPassword }} + mysql-root-password: {{ .Values.mysqlRootPassword | b64enc | quote }} + {{ else }} + mysql-root-password: {{ randAlphaNum 10 | b64enc | quote }} + {{ end }} + {{ if .Values.mysqlPassword }} + mysql-password: {{ .Values.mysqlPassword | b64enc | quote }} + {{ else }} + mysql-password: {{ randAlphaNum 10 | b64enc | quote }} + {{ end }} + {{ if .Values.xtraBackupPassword }} + xtrabackup-password: {{ .Values.xtraBackupPassword | b64enc | quote }} + {{ else }} + xtrabackup-password: {{ randAlphaNum 10 | b64enc | quote }} + {{ end }} +{{- if .Values.ssl.enabled }} +{{ if .Values.ssl.certificates }} +{{- range .Values.ssl.certificates }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ .name }} + labels: + app: {{ template "percona-xtradb-cluster.fullname" $ }} + chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}" + release: "{{ $.Release.Name }}" + heritage: "{{ $.Release.Service }}" +type: Opaque +data: + ca.pem: {{ .ca | b64enc }} + server-cert.pem: {{ .cert | b64enc }} + server-key.pem: {{ .key | b64enc }} +{{- end }} +{{- end }} +{{- end }} diff --git a/integration/mysql-percona/percona-xtradb-cluster/templates/service-metrics.yaml b/integration/mysql-percona/percona-xtradb-cluster/templates/service-metrics.yaml new file mode 100755 index 00000000..5bcc5541 --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/templates/service-metrics.yaml @@ -0,0 +1,20 @@ +{{ if .Values.metricsExporter.enabled }} +--- +apiVersion: v1 +kind: Service +metadata: + name: "{{ template "percona-xtradb-cluster.fullname" . }}-metrics" + labels: + app: {{ template "percona-xtradb-cluster.fullname" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +spec: + clusterIP: None + ports: + - name: metrics + port: 9104 + selector: + app: {{ template "percona-xtradb-cluster.fullname" . }} + release: "{{ .Release.Name }}" +{{ end }} diff --git a/integration/mysql-percona/percona-xtradb-cluster/templates/service-percona.yaml b/integration/mysql-percona/percona-xtradb-cluster/templates/service-percona.yaml new file mode 100755 index 00000000..659fc573 --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/templates/service-percona.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "percona-xtradb-cluster.fullname" . }} + labels: + app: {{ template "percona-xtradb-cluster.fullname" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +spec: + {{- if .Values.service.percona.headless }} + clusterIP: None + {{- end }} + ports: + - name: mysql + port: 3306 + targetPort: mysql + selector: + app: {{ template "percona-xtradb-cluster.fullname" . }} + release: "{{ .Release.Name }}" diff --git a/integration/mysql-percona/percona-xtradb-cluster/templates/service-repl.yaml b/integration/mysql-percona/percona-xtradb-cluster/templates/service-repl.yaml new file mode 100755 index 00000000..e488492c --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/templates/service-repl.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: "{{ template "percona-xtradb-cluster.fullname" . }}-repl" + labels: + app: {{ template "percona-xtradb-cluster.fullname" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +spec: + clusterIP: None + ports: + - name: galera + port: 4567 + - name: state-xfer + port: 4568 + - name: state-snap + port: 4444 + selector: + app: {{ template "percona-xtradb-cluster.fullname" . }} + release: "{{ .Release.Name }}" diff --git a/integration/mysql-percona/percona-xtradb-cluster/templates/servicemonitor.yaml b/integration/mysql-percona/percona-xtradb-cluster/templates/servicemonitor.yaml new file mode 100755 index 00000000..7b721682 --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/templates/servicemonitor.yaml @@ -0,0 +1,27 @@ +{{ if and .Values.metricsExporter.enabled .Values.prometheus.operator.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ template "percona-xtradb-cluster.fullname" . }} +{{- if .Values.prometheus.operator.serviceMonitor.namespace }} + namespace: {{ .Values.prometheus.operator.serviceMonitor.namespace }} +{{- end }} + labels: + app: {{ template "percona-xtradb-cluster.fullname" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + heritage: "{{ .Release.Service }}" + release: "{{ .Release.Name }}" +{{- if .Values.prometheus.operator.serviceMonitor.selector }} +{{ toYaml .Values.prometheus.operator.serviceMonitor.selector | indent 4 }} +{{- end }} +spec: + selector: + matchLabels: + app: {{ template "percona-xtradb-cluster.fullname" . }} + release: {{ .Release.Name }} + endpoints: + - port: metrics + interval: {{ .Values.prometheus.operator.serviceMonitor.interval }} + namespaceSelector: + any: true +{{ end }} diff --git a/integration/mysql-percona/percona-xtradb-cluster/templates/statefulset.yaml b/integration/mysql-percona/percona-xtradb-cluster/templates/statefulset.yaml new file mode 100755 index 00000000..0055d6c9 --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/templates/statefulset.yaml @@ -0,0 +1,211 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ template "percona-xtradb-cluster.fullname" . }} + labels: + app: {{ template "percona-xtradb-cluster.fullname" . }} + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +spec: + replicas: {{ .Values.replicas }} + selector: + matchLabels: + app: {{ template "percona-xtradb-cluster.fullname" . }} + release: "{{ .Release.Name }}" + serviceName: {{ template "percona-xtradb-cluster.fullname" . }} + template: + metadata: + labels: + app: {{ template "percona-xtradb-cluster.fullname" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" + {{- if .Values.podAnnotations }} + annotations: +{{ toYaml .Values.podAnnotations | indent 8 }} + {{- end }} + spec: + initContainers: + - name: "remove-lost-found" + image: "busybox:1.25.0" + imagePullPolicy: IfNotPresent + command: + - "rm" + - "-fr" + - "/var/lib/mysql/lost+found" + volumeMounts: + - name: mysql-data + mountPath: /var/lib/mysql + containers: + - name: database + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + command: + - "/bin/bash" + - "/startup-scripts/entrypoint.sh" + resources: +{{ toYaml .Values.resources | indent 10 }} + env: + {{- if .Values.mysqlAllowEmptyPassword }} + - name: MYSQL_ALLOW_EMPTY_PASSWORD + value: "true" + {{- else }} + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "percona-xtradb-cluster.fullname" . }} + key: mysql-root-password + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "percona-xtradb-cluster.fullname" . }} + key: mysql-password + {{- end }} + - name: XTRABACKUP_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "percona-xtradb-cluster.fullname" . }} + key: xtrabackup-password + - name: MYSQL_USER + value: {{ default "" .Values.mysqlUser | quote }} + - name: MYSQL_DATABASE + value: {{ default "" .Values.mysqlDatabase | quote }} + - name: ALLOW_ROOT_FROM + value: {{ .Values.allowRootFrom | quote }} + - name: CLUSTER_NAME + value: {{ template "percona-xtradb-cluster.fullname" . }} + - name: SHORT_CLUSTER_NAME + value: {{ template "percona-xtradb-cluster.shortname" . }} + - name: K8S_SERVICE_NAME + value: {{ template "percona-xtradb-cluster.fullname" . }}-repl + - name: PXC_STRICT_MODE + value: {{ default "ENFORCING" .Values.pxc_strict_mode | quote }} + - name: DEBUG + value: "true" + ports: + - name: mysql + containerPort: 3306 + - name: galera-repl + containerPort: 4567 + - name: state-transfer + containerPort: 4568 + - name: state-snapshot + containerPort: 4444 + livenessProbe: + exec: + command: + - "/bin/bash" + - "-c" + - "mysqladmin ping || test -e /var/lib/mysql/sst_in_progress" + initialDelaySeconds: 30 + timeoutSeconds: 2 + readinessProbe: + exec: + command: ["mysql", "-h", "127.0.0.1", "-e", "SELECT 1"] + initialDelaySeconds: 30 + timeoutSeconds: 2 + volumeMounts: + - name: mysql-data + mountPath: /var/lib/mysql + - name: mysql-startup-scripts + mountPath: /startup-scripts + - name: mysql-config-files + mountPath: /etc/mysql/conf.d + - name: slash-root + mountPath: /root + - name: var-log + mountPath: /var/log + {{- if .Values.ssl.enabled }} + - name: certificates + mountPath: /ssl + {{- end }} + {{ if .Values.logTail }} + - name: "logs" + image: "busybox:1.25.0" + imagePullPolicy: IfNotPresent + command: + - "tail" + - "-f" + - "/var/log/mysqld.log" + volumeMounts: + - name: var-log + mountPath: /var/log + {{ end }} + {{ if .Values.metricsExporter.enabled }} + - name: metrics + {{- if .Values.metricsExporter.tag }} + image: prom/mysqld-exporter:{{ .Values.metricsExporter.tag }} + {{- else }} + image: prom/mysqld-exporter + {{- end }} + imagePullPolicy: IfNotPresent +{{- if .Values.metricsExporter.commandOverrides }} + command: +{{ toYaml .Values.metricsExporter.commandOverrides | indent 8 }} +{{- end }} +{{- if .Values.metricsExporter.argsOverrides }} + args: +{{ toYaml .Values.metricsExporter.argsOverrides | indent 8 }} +{{- end }} + ports: + - name: metrics + containerPort: 9104 + volumeMounts: + - name: slash-root + mountPath: /root + livenessProbe: + exec: + command: ["wget","-q","-O","-","localhost:9104"] + initialDelaySeconds: 30 + timeoutSeconds: 2 + readinessProbe: + exec: + command: ["wget","-q","-O","-","localhost:9104"] + initialDelaySeconds: 30 + timeoutSeconds: 2 + {{ end }} + volumes: + - name: slash-root + emptyDir: {} + - name: var-log + emptyDir: {} + - name: mysql-config-files + configMap: + name: {{ template "percona-xtradb-cluster.fullname" . }}-config-files + - name: mysql-startup-scripts + configMap: + name: {{ template "percona-xtradb-cluster.fullname" . }}-startup-scripts + {{- if not .Values.persistence.enabled }} + - name: mysql-data + emptyDir: {} + {{- end -}} + {{- if .Values.ssl.enabled }} + - name: certificates + secret: + secretName: {{ .Values.ssl.secret }} + {{- end }} + {{- if .Values.tolerations }} + tolerations: +{{ toYaml .Values.tolerations | indent 8 }} + {{- end -}} + {{- if .Values.nodeSelector }} + nodeSelector: +{{ toYaml .Values.nodeSelector | indent 8 }} + {{- end -}} + {{- if .Values.persistence.enabled }} + volumeClaimTemplates: + - metadata: + name: mysql-data + spec: + accessModes: [{{ .Values.persistence.accessMode | quote }}] + {{- if .Values.persistence.storageClass }} + {{- if (eq "-" .Values.persistence.storageClass) }} + storageClassName: "" + {{- else }} + storageClassName: "{{ .Values.persistence.storageClass }}" + {{- end }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size | quote }} + {{- end -}} diff --git a/integration/mysql-percona/percona-xtradb-cluster/templates/tests/pxc-test-cm.yaml b/integration/mysql-percona/percona-xtradb-cluster/templates/tests/pxc-test-cm.yaml new file mode 100755 index 00000000..98f83470 --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/templates/tests/pxc-test-cm.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "percona-xtradb-cluster.fullname" . }}-tests +data: + run.sh: |- + echo "Testing Percona XtraDB Cluster" + creds="-uroot -p${MYSQL_ROOT_PASSWORD} -h {{ template "percona-xtradb-cluster.fullname" . }}" + echo "==> basic mysql ping" + mysqladmin $creds ping + [[ $? != "0" ]] && exit $? + echo "==> test basic query" + mysql $creds -e "select now() \G" + [[ $? != "0" ]] && exit $? + echo "==> checking if cluster is ready" + mysql $creds -e "SHOW GLOBAL STATUS LIKE 'wsrep_ready' \G" | grep Value | awk '{ print $2}' | grep ON + [[ $? != "0" ]] && exit $? + echo "==> checking if cluster size matches replica count" + cluster_size=$(mysql $creds -e "SHOW GLOBAL STATUS LIKE 'wsrep_cluster_size' \G" | grep Value | awk '{ print $2}') + [[ $? != "0" ]] && exit $? + [[ "${cluster_size}" == "{{ .Values.replicas }}" ]] || exit 1 + echo "SUCCESS" + exit 0 diff --git a/integration/mysql-percona/percona-xtradb-cluster/templates/tests/pxc-test.yaml b/integration/mysql-percona/percona-xtradb-cluster/templates/tests/pxc-test.yaml new file mode 100755 index 00000000..8e415c49 --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/templates/tests/pxc-test.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ .Release.Name }}-test" + annotations: + "helm.sh/hook": test-success +spec: + containers: + - name: pxc-test + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + command: ["bash", "/tests/run.sh"] + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "percona-xtradb-cluster.fullname" . }} + key: mysql-root-password + volumeMounts: + - mountPath: /tests + name: tests + volumes: + - name: tests + configMap: + name: {{ template "percona-xtradb-cluster.fullname" . }}-tests + restartPolicy: Never diff --git a/integration/mysql-percona/percona-xtradb-cluster/values.yaml b/integration/mysql-percona/percona-xtradb-cluster/values.yaml new file mode 100755 index 00000000..4d10525c --- /dev/null +++ b/integration/mysql-percona/percona-xtradb-cluster/values.yaml @@ -0,0 +1,169 @@ +# Default values for Percona XtraDB Cluster + +## percona image and version +## ref: https://hub.docker.com/r/percona/percona-xtradb-cluster/tags/ +image: + repository: "percona/percona-xtradb-cluster" + tag: "5.7.19" + pullPolicy: IfNotPresent + +# Desired number of members of xtradb cluster +replicas: 3 + +## Specify password for root user +## +# mysqlRootPassword: not-a-secure-password + +## Specify password for xtradb backup user +## +# xtraBackupPassword: replicate-my-data + +## Uncomment to create a database user +## +# mysqlUser: test +# mysqlPassword: test + +## Allow unauthenticated access, uncomment to enable +## +# mysqlAllowEmptyPassword: true + +## Uncomment to Create a database +## +# mysqlDatabase: test + +## Configure pxc_strict_mode +## ref: https://www.percona.com/doc/percona-xtradb-cluster/LATEST/features/pxc-strict-mode.html +## pxc_strict_mode: ENFORCING + +## hosts to allow root user access from +# set to "127.0.0.1" to deny remote root. +allowRootFrom: "%" + +## Persist data to a persistent volume +persistence: + enabled: false + ## percona data Persistent Volume Storage Class + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + accessMode: ReadWriteOnce + size: 8Gi + +## Node labels for pod assignment +## Ref: https://kubernetes.io/docs/user-guide/node-selection/ +## +nodeSelector: {} + +## Pod annotations +## Ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +## +podAnnotations: {} + +## Tolerations labels for pod assignment +## Allow the scheduling on tainted nodes (requires Kubernetes >= 1.6) +## Ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ +## +tolerations: [] + +## Configure resource requests and limits +## ref: http://kubernetes.io/docs/user-guide/compute-resources/ +## +# resources: +# requests: +# memory: 256Mi +# cpu: 100m + +configFiles: + node.cnf: |+ + [mysqld] + datadir=/var/lib/mysql + default_storage_engine=InnoDB + binlog_format=ROW + innodb_flush_log_at_trx_commit = 0 + innodb_flush_method = O_DIRECT + innodb_file_per_table = 1 + innodb_autoinc_lock_mode=2 + bind_address = 0.0.0.0 + wsrep_slave_threads=2 + wsrep_cluster_address=gcomm:// + wsrep_provider=/usr/lib/galera3/libgalera_smm.so + wsrep_cluster_name=galera + wsrep_sst_method=xtrabackup-v2 + +## When set to true will create sidecar for `prom/mysqld-exporter` +## metrics exporting +metricsExporter: + enabled: false + commandOverrides: [] + argsOverrides: [] + +prometheus: + ## Are you using [Prometheus Operator](https://coreos.com/operators/prometheus/docs/latest/user-guides/getting-started.html)? + operator: + ## Setting to true will create Prometheus-Operator specific resources like ServiceMonitors + enabled: false + + ## Configures alerts for Prometheus to pick up + prometheusRule: + enabled: true + + ## Labels to add to alerts + labels: {} + + ## Namespace which Prometheus is installed in + # namespace: monitoring + + ## Label Selector for Prometheus to find alert rules + # selector: + # prometheus: kube-prometheus + + ## Configures targets for Prometheus to pick up + serviceMonitor: + ## Interval at which Prometheus will scrape metrics exporter + interval: 10s + + ## Namespace which Prometheus is installed in + # namespace: monitoring + + ## Label Selector for Prometheus to find ServiceMonitors + # selector: + # prometheus: kube-prometheus + +## When set to true will create sidecar to tail mysql log +logTail: true + +ssl: + enabled: false + secret: mysql-ssl-certs + certificates: +# - name: mysql-ssl-certs +# ca: |- +# -----BEGIN CERTIFICATE----- +# ... +# -----END CERTIFICATE----- +# cert: |- +# -----BEGIN CERTIFICATE----- +# ... +# -----END CERTIFICATE----- +# key: |- +# -----BEGIN RSA PRIVATE KEY----- +# ... +# -----END RSA PRIVATE KEY----- + +## Configure PodDisruptionBudget +## ref: https://kubernetes.io/docs/concepts/workloads/pods/disruptions/ +# +podDisruptionBudget: + enabled: false + # minAvailable: 1 + maxUnavailable: 1 + +## Set the percona kubernetes service headless (no load-balancing) +## ref: https://kubernetes.io/docs/concepts/services-networking/service/#headless-services +service: + percona: + headless: false diff --git a/integration/mysql-percona/requirements.yaml b/integration/mysql-percona/requirements.yaml new file mode 100644 index 00000000..bc50365a --- /dev/null +++ b/integration/mysql-percona/requirements.yaml @@ -0,0 +1,5 @@ +dependencies: +- name: percona-xtradb-cluster + repository: "file://./percona-xtradb-cluster" + version: 1.0.3 + alias: percona \ No newline at end of file diff --git a/integration/mysql-percona/scripts/test_read.sh b/integration/mysql-percona/scripts/test_read.sh new file mode 100644 index 00000000..2a2efc88 --- /dev/null +++ b/integration/mysql-percona/scripts/test_read.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +retry=3 +interval=5 + +for i in `seq 1 $retry` +do + sleep $interval + if [ ! -f "${MYSQL_PASSWORD_FILE}" ]; then + echo "Password file does not exists" + exit 1; + else + MYSQL_PASSWORD=$(cat ${MYSQL_PASSWORD_FILE}) + fi + + TESTDATA="$(cat /tmp/checkdata)" + + echo "reading data from mysql..." + FOUNDDATA=$(mysql \ + -h ${MYSQL_HOST} \ + -u ${MYSQL_USERNAME} \ + -p${MYSQL_PASSWORD} ${MYSQL_DB} \ + -e "SELECT data FROM test WHERE data = '${TESTDATA}';") + + echo "$FOUNDDATA" | grep "$TESTDATA" && break; +done \ No newline at end of file diff --git a/integration/mysql-percona/scripts/test_write.sh b/integration/mysql-percona/scripts/test_write.sh new file mode 100755 index 00000000..47a0961b --- /dev/null +++ b/integration/mysql-percona/scripts/test_write.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +TESTDATA="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)" +echo ${TESTDATA} > /tmp/checkdata + +retry=3 +interval=5 + +for i in `seq 1 $retry` +do + sleep $interval + if [ ! -f "${MYSQL_PASSWORD_FILE}" ]; then + echo "Password file does not exists" + exit 1; + else + MYSQL_PASSWORD=$(cat ${MYSQL_PASSWORD_FILE}) + fi + + echo "writing data into mysql database..." + mysql -h ${MYSQL_HOST} -u ${MYSQL_USERNAME} -p${MYSQL_PASSWORD} ${MYSQL_DB} \ + -e "CREATE TABLE IF NOT EXISTS test (no INT NOT NULL AUTO_INCREMENT PRIMARY KEY, data VARCHAR(100)); INSERT INTO test (data) VALUES('${TESTDATA}');"\ + && break +done \ No newline at end of file diff --git a/integration/mysql-percona/templates/db.yaml b/integration/mysql-percona/templates/db.yaml new file mode 100644 index 00000000..efcb0765 --- /dev/null +++ b/integration/mysql-percona/templates/db.yaml @@ -0,0 +1,11 @@ +apiVersion: "kci.rocks/v1alpha1" +kind: "Database" +metadata: + name: {{ .Values.db.name }} + labels: + env: test +spec: + secretName: {{ .Values.db.name }}-credentials # where to save db name user, password for application + instance: {{ .Values.instance.name }} + backup: + enable: false diff --git a/integration/mysql-percona/templates/instance.yaml b/integration/mysql-percona/templates/instance.yaml new file mode 100644 index 00000000..07804eca --- /dev/null +++ b/integration/mysql-percona/templates/instance.yaml @@ -0,0 +1,38 @@ +apiVersion: kci.rocks/v1alpha1 +kind: DbInstance +metadata: + name: {{ .Values.instance.name }} +spec: + adminSecretRef: + Namespace: {{ .Release.Namespace }} + Name: {{ .Values.instance.name }}-admin-password + engine: mysql + percona: + servers: + - host: {{ .Release.Name }}-pxc-0.{{ .Release.Name }}-pxc.{{ .Release.Namespace }} + port: 3306 + maxConn: 100 + - host: {{ .Release.Name }}-pxc-1.{{ .Release.Name }}-pxc.{{ .Release.Namespace }} + port: 3306 + maxConn: 100 + monitorUserSecretRef: + Namespace: {{ .Release.Namespace }} + Name: {{ .Values.instance.name }}-monitoruser-secret +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.instance.name }}-admin-password +type: Opaque +data: + password: {{ .Values.percona.mysqlRootPassword | b64enc }} + user: {{ print "root" | b64enc }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.instance.name }}-monitoruser-secret +type: Opaque +data: + password: {{ .Values.percona.mysqlPassword | b64enc }} + user: {{ .Values.percona.mysqlUser | b64enc }} \ No newline at end of file diff --git a/integration/mysql-percona/templates/test/test.yaml b/integration/mysql-percona/templates/test/test.yaml new file mode 100644 index 00000000..9a758bfc --- /dev/null +++ b/integration/mysql-percona/templates/test/test.yaml @@ -0,0 +1,95 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ .Values.db.name }}-tester-app-{{ randAlphaNum 5 | lower }} + annotations: + "helm.sh/hook": test-success + labels: + app: mysql + role: tester +spec: + containers: + - name: mysql-writer + image: imega/mysql-client + command: + - sh + - -c + - exec sh /app/write.sh + env: + - name: MYSQL_PASSWORD_FILE + value: /run/secrets/mysql/PASSWORD + - name: MYSQL_USERNAME + valueFrom: + secretKeyRef: + name: {{ .Values.db.name }}-credentials + key: USER + - name: MYSQL_DB + valueFrom: + secretKeyRef: + name: {{ .Values.db.name }}-credentials + key: DB + - name: MYSQL_HOST + valueFrom: + configMapKeyRef: + name: {{ .Values.db.name }}-credentials + key: DB_CONN + volumeMounts: + - name: db-secret + mountPath: /run/secrets/mysql/ + readOnly: true + - name: script + mountPath: /app/ + - name: shared-data + mountPath: /tmp + imagePullPolicy: IfNotPresent + resources: + limits: + cpu: 100m + memory: 128Mi + - name: mysql-reader + image: imega/mysql-client + command: + - sh + - -c + - exec sh /app/read.sh + env: + - name: MYSQL_PASSWORD_FILE + value: /run/secrets/mysql/PASSWORD + - name: MYSQL_USERNAME + valueFrom: + secretKeyRef: + name: {{ .Values.db.name }}-credentials + key: USER + - name: MYSQL_DB + valueFrom: + secretKeyRef: + name: {{ .Values.db.name }}-credentials + key: DB + - name: MYSQL_HOST + valueFrom: + configMapKeyRef: + name: {{ .Values.db.name }}-credentials + key: DB_HOST + volumeMounts: + - name: db-secret + mountPath: /run/secrets/mysql/ + readOnly: true + - name: script + mountPath: /app/ + - name: shared-data + mountPath: /tmp + imagePullPolicy: IfNotPresent + resources: + limits: + cpu: 100m + memory: 128Mi + restartPolicy: Never + volumes: + - name: db-secret + secret: + secretName: {{ .Values.db.name }}-credentials + - name: script + configMap: + name: {{ .Values.db.name }}-test-script + - name: shared-data + emptyDir: {} \ No newline at end of file diff --git a/integration/mysql-percona/templates/testscript.yaml b/integration/mysql-percona/templates/testscript.yaml new file mode 100644 index 00000000..c58e6996 --- /dev/null +++ b/integration/mysql-percona/templates/testscript.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.db.name }}-test-script +data: + write.sh: | +{{ .Files.Get "scripts/test_write.sh" | indent 4}} + read.sh: | +{{ .Files.Get "scripts/test_read.sh" | indent 4}} \ No newline at end of file diff --git a/integration/mysql-percona/values.yaml b/integration/mysql-percona/values.yaml new file mode 100644 index 00000000..f87f7f30 --- /dev/null +++ b/integration/mysql-percona/values.yaml @@ -0,0 +1,11 @@ +instance: + name: my-percona-instance + +db: + name: my-percona-db + +percona: + mysqlRootPassword: "test1234" + mysqlUser: "monitor" + mysqlPassword: "test4321" + replicas: 2 diff --git a/integration/mysql/templates/db.yaml b/integration/mysql/templates/db.yaml deleted file mode 100644 index b3951689..00000000 --- a/integration/mysql/templates/db.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: "kci.rocks/v1alpha1" -kind: "Database" -metadata: - name: "my-db-test" - labels: - env: test -spec: - secretName: my-db-test-credentials # where to save db name user, password for application - instance: my-local-test - backup: - enable: false \ No newline at end of file diff --git a/integration/mysql/values.yaml b/integration/mysql/values.yaml deleted file mode 100644 index 64b82135..00000000 --- a/integration/mysql/values.yaml +++ /dev/null @@ -1,5 +0,0 @@ -mysql: - serviceName: my-test-db - adminUser: root - adminPassword: test1234 - image: mysql:5.7 \ No newline at end of file diff --git a/integration/test.sh b/integration/test.sh index d26266a3..44fd32e6 100755 --- a/integration/test.sh +++ b/integration/test.sh @@ -49,7 +49,9 @@ check_instance_status() { create_test_resources() { echo "[Test] creating" - $HELM_CMD upgrade --install --namespace ${TEST_NAMESPACE} test-mysql integration/mysql \ + $HELM_CMD upgrade --install --namespace ${TEST_NAMESPACE} test-mysql-generic integration/mysql-generic \ + && $HELM_CMD dependency build integration/mysql-percona \ + && $HELM_CMD upgrade --install --namespace ${TEST_NAMESPACE} test-mysql-percona integration/mysql-percona \ && $HELM_CMD upgrade --install --namespace ${TEST_NAMESPACE} test-pg integration/postgres \ && echo "[Test] created" if [ $? -ne 0 ]; then @@ -86,13 +88,13 @@ check_databases_status() { run_test() { echo "[Test] testing read write to database" - $HELM_CMD test test-mysql && $HELM_CMD test test-pg \ + $HELM_CMD test test-mysql-generic && $HELM_CMD test test-mysql-percona && $HELM_CMD test test-pg \ && echo "[Test] OK!" } delete_databases() { echo "[Database] deleting" - $KUBECTL_CMD delete db my-db-test -n ${TEST_NAMESPACE} && $KUBECTL_CMD delete db pg-db-test -n ${TEST_NAMESPACE} \ + $KUBECTL_CMD delete db -n ${TEST_NAMESPACE} --all \ && echo "[Database] deleted!" } diff --git a/pkg/apis/kci/v1alpha1/database_types.go b/pkg/apis/kci/v1alpha1/database_types.go index ff297d91..9b49dc63 100644 --- a/pkg/apis/kci/v1alpha1/database_types.go +++ b/pkg/apis/kci/v1alpha1/database_types.go @@ -25,9 +25,19 @@ type DatabaseSpec struct { type DatabaseStatus struct { // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html - Phase string `json:"phase"` - Status bool `json:"status"` - InstanceRef *DbInstance `json:"instanceRef"` + Phase string `json:"phase"` + Status bool `json:"status"` + InstanceRef *DbInstance `json:"instanceRef"` + MonitorUserSecretName string `json:"monitorUserSecret,omitempty"` + ProxyStatus DatabaseProxyStatus `json:"proxyStatus,omitempty"` +} + +// DatabaseProxyStatus defines whether proxy for database is enabled or not +// if so, provide information +type DatabaseProxyStatus struct { + Status bool `json:"status"` + ServiceName string `json:"serviceName"` + SQLPort int32 `json:"sqlPort"` } // DatabaseBackup defines the desired state of backup and schedule diff --git a/pkg/apis/kci/v1alpha1/dbinstance_types.go b/pkg/apis/kci/v1alpha1/dbinstance_types.go index 2c7278cb..7bf1ef5e 100644 --- a/pkg/apis/kci/v1alpha1/dbinstance_types.go +++ b/pkg/apis/kci/v1alpha1/dbinstance_types.go @@ -13,12 +13,19 @@ import ( // +k8s:openapi-gen=true type DbInstanceSpec struct { // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file - Engine string `json:"engine"` - AdminUserSecret types.NamespacedName `json:"adminSecretRef"` - Backup DbInstanceBackup `json:"backup"` - Monitoring DbInstanceMonitoring `json:"monitoring"` - Google *GoogleInstance `json:"google,omitempty"` - Generic *GenericInstance `json:"generic,omitempty"` + Engine string `json:"engine"` + AdminUserSecret types.NamespacedName `json:"adminSecretRef"` + Backup DbInstanceBackup `json:"backup"` + Monitoring DbInstanceMonitoring `json:"monitoring"` + DbInstanceSource `json:",inline"` +} + +// DbInstanceSource represents the source of a instance. +// Only one of its members may be specified. +type DbInstanceSource struct { + Google *GoogleInstance `json:"google,omitempty" protobuf:"bytes,1,opt,name=google"` + Generic *GenericInstance `json:"generic,omitempty" protobuf:"bytes,2,opt,name=generic"` + Percona *PerconaCluster `json:"percona,omitempty" protobuf:"bytes,3,opt,name=percona"` } // DbInstanceStatus defines the observed state of DbInstance @@ -38,12 +45,26 @@ type GoogleInstance struct { ConfigmapName types.NamespacedName `json:"configmapRef"` } +// PerconaCluster is used when instance type is percona cluster +type PerconaCluster struct { + ServerList []BackendServer `json:"servers"` // hostgroup: host address + MonitorUserSecret types.NamespacedName `json:"monitorUserSecretRef"` +} + +// BackendServer defines backend database server +type BackendServer struct { + Host string `json:"host"` + Port uint16 `json:"port"` + MaxConnection uint8 `json:"maxConn"` + ReadOnly bool `json:"readonly,omitempty"` +} + // GenericInstance is used when instance type is generic // and describes necessary informations to use instance // generic instance can be any backend, it must be reachable by described address and port type GenericInstance struct { Host string `json:"host"` - Port int32 `json:"port"` + Port uint16 `json:"port"` PublicIP string `json:"publicIp,omitempty"` // BackupHost address will be used for dumping database for backup // Usually slave address for master-slave setup or cluster lb address @@ -101,14 +122,30 @@ func (dbin *DbInstance) ValidateEngine() error { // returns error when more than one backend types are defined // or when no backend type is defined func (dbin *DbInstance) ValidateBackend() error { - if (dbin.Spec.Google != nil) && (dbin.Spec.Generic != nil) { - return errors.New("more than one instance type defined") - } + source := dbin.Spec.DbInstanceSource - if (dbin.Spec.Google == nil) && (dbin.Spec.Generic == nil) { + if (source.Google == nil) && (source.Generic == nil) && (source.Percona == nil) { return errors.New("no instance type defined") } + numSources := 0 + + if source.Google != nil { + numSources++ + } + + if source.Generic != nil { + numSources++ + } + + if source.Percona != nil { + numSources++ + } + + if numSources > 1 { + return errors.New("may not specify more than 1 instance type") + } + return nil } @@ -119,14 +156,21 @@ func (dbin *DbInstance) GetBackendType() (string, error) { if err != nil { return "", err } - if dbin.Spec.Google != nil { + + source := dbin.Spec.DbInstanceSource + + if source.Google != nil { return "google", nil } - if dbin.Spec.Generic != nil { + if source.Generic != nil { return "generic", nil } + if source.Percona != nil { + return "percona", nil + } + return "", errors.New("no backend type defined") } diff --git a/pkg/apis/kci/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/kci/v1alpha1/zz_generated.deepcopy.go index 4ca54bfd..d60118f7 100644 --- a/pkg/apis/kci/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/kci/v1alpha1/zz_generated.deepcopy.go @@ -8,6 +8,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackendServer) DeepCopyInto(out *BackendServer) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendServer. +func (in *BackendServer) DeepCopy() *BackendServer { + if in == nil { + return nil + } + out := new(BackendServer) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Database) DeepCopyInto(out *Database) { *out = *in @@ -85,6 +101,22 @@ func (in *DatabaseList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DatabaseProxyStatus) DeepCopyInto(out *DatabaseProxyStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseProxyStatus. +func (in *DatabaseProxyStatus) DeepCopy() *DatabaseProxyStatus { + if in == nil { + return nil + } + out := new(DatabaseProxyStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DatabaseSpec) DeepCopyInto(out *DatabaseSpec) { *out = *in @@ -115,6 +147,7 @@ func (in *DatabaseStatus) DeepCopyInto(out *DatabaseStatus) { *out = new(DbInstance) (*in).DeepCopyInto(*out) } + out.ProxyStatus = in.ProxyStatus return } @@ -222,11 +255,8 @@ func (in *DbInstanceMonitoring) DeepCopy() *DbInstanceMonitoring { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DbInstanceSpec) DeepCopyInto(out *DbInstanceSpec) { +func (in *DbInstanceSource) DeepCopyInto(out *DbInstanceSource) { *out = *in - out.AdminUserSecret = in.AdminUserSecret - out.Backup = in.Backup - out.Monitoring = in.Monitoring if in.Google != nil { in, out := &in.Google, &out.Google *out = new(GoogleInstance) @@ -237,6 +267,31 @@ func (in *DbInstanceSpec) DeepCopyInto(out *DbInstanceSpec) { *out = new(GenericInstance) **out = **in } + if in.Percona != nil { + in, out := &in.Percona, &out.Percona + *out = new(PerconaCluster) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DbInstanceSource. +func (in *DbInstanceSource) DeepCopy() *DbInstanceSource { + if in == nil { + return nil + } + out := new(DbInstanceSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DbInstanceSpec) DeepCopyInto(out *DbInstanceSpec) { + *out = *in + out.AdminUserSecret = in.AdminUserSecret + out.Backup = in.Backup + out.Monitoring = in.Monitoring + in.DbInstanceSource.DeepCopyInto(&out.DbInstanceSource) return } @@ -312,3 +367,25 @@ func (in *GoogleInstance) DeepCopy() *GoogleInstance { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PerconaCluster) DeepCopyInto(out *PerconaCluster) { + *out = *in + if in.ServerList != nil { + in, out := &in.ServerList, &out.ServerList + *out = make([]BackendServer, len(*in)) + copy(*out, *in) + } + out.MonitorUserSecret = in.MonitorUserSecret + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PerconaCluster. +func (in *PerconaCluster) DeepCopy() *PerconaCluster { + if in == nil { + return nil + } + out := new(PerconaCluster) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/config/test/config_ok.yaml b/pkg/config/test/config_ok.yaml index b3995bdb..78e255ab 100644 --- a/pkg/config/test/config_ok.yaml +++ b/pkg/config/test/config_ok.yaml @@ -5,6 +5,9 @@ instance: nodeSelector: {} image: gcr.io/cloudsql-docker/gce-proxy:1.11 generic: {} + percona: + proxy: + image: severalnines/proxysql:2.0 backup: nodeSelector: {} postgres: diff --git a/pkg/config/types.go b/pkg/config/types.go index fde95c55..a6c7db2b 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -10,6 +10,7 @@ type Config struct { type instanceConfig struct { Google googleInstanceConfig `yaml:"google"` Generic genericInstanceConfig `yaml:"generic"` + Percona perconaClusterConfig `yaml:"percona"` } type googleInstanceConfig struct { @@ -21,6 +22,10 @@ type genericInstanceConfig struct { // TODO } +type perconaClusterConfig struct { + ProxyConfig proxyConfig `yaml:"proxy"` +} + type proxyConfig struct { NodeSelector map[string]string `yaml:"nodeSelector"` Image string `yaml:"image"` diff --git a/pkg/controller/database/controller.go b/pkg/controller/database/controller.go index d7ab830a..896eaec5 100644 --- a/pkg/controller/database/controller.go +++ b/pkg/controller/database/controller.go @@ -87,9 +87,9 @@ type ReconcileDatabase struct { var ( phaseCreate = "Creating" - phaseConfigMap = "InfoConfigMapCreating" phaseInstanceAccessSecret = "InstanceAccessSecretCreating" phaseProxy = "ProxyCreating" + phaseConfigMap = "InfoConfigMapCreating" phaseMonitoring = "MonitoringCreating" phaseBackupJob = "BackupJobCreating" phaseFinish = "Finishing" @@ -225,6 +225,12 @@ func (r *ReconcileDatabase) Reconcile(request reconcile.Request) (reconcile.Resu if err != nil { return r.manageError(dbcr, err, true) } + dbcr.Status.Phase = phaseConfigMap + case phaseConfigMap: + err := r.createInfoConfigMap(dbcr) + if err != nil { + return r.manageError(dbcr, err, true) + } dbcr.Status.Phase = phaseBackupJob case phaseBackupJob: err := r.createBackupJob(dbcr) diff --git a/pkg/controller/database/databaseHelper.go b/pkg/controller/database/databaseHelper.go index 90636132..aa6c512a 100644 --- a/pkg/controller/database/databaseHelper.go +++ b/pkg/controller/database/databaseHelper.go @@ -53,7 +53,7 @@ func determinDatabaseType(dbcr *kciv1alpha1.Database, dbCred database.Credential db := database.Postgres{ Backend: backend, Host: host, - Port: int32(port), + Port: uint16(port), Database: dbCred.Name, User: dbCred.Username, Password: dbCred.Password, @@ -66,7 +66,7 @@ func determinDatabaseType(dbcr *kciv1alpha1.Database, dbCred database.Credential db := database.Mysql{ Backend: backend, Host: host, - Port: int32(port), + Port: uint16(port), Database: dbCred.Name, User: dbCred.Username, Password: dbCred.Password, diff --git a/pkg/controller/database/helper_test.go b/pkg/controller/database/helper_test.go index a8fc433f..e725e400 100644 --- a/pkg/controller/database/helper_test.go +++ b/pkg/controller/database/helper_test.go @@ -17,9 +17,11 @@ func newPostgresTestDbInstanceCr() kciv1alpha1.DbInstance { return kciv1alpha1.DbInstance{ Spec: kciv1alpha1.DbInstanceSpec{ Engine: "postgres", - Generic: &kciv1alpha1.GenericInstance{ - Host: test.GetPostgresHost(), - Port: test.GetPostgresPort(), + DbInstanceSource: kciv1alpha1.DbInstanceSource{ + Generic: &kciv1alpha1.GenericInstance{ + Host: test.GetPostgresHost(), + Port: test.GetPostgresPort(), + }, }, }, Status: kciv1alpha1.DbInstanceStatus{Info: info}, @@ -55,9 +57,11 @@ func newMysqlTestDbCr() *kciv1alpha1.Database { InstanceRef: &kciv1alpha1.DbInstance{ Spec: kciv1alpha1.DbInstanceSpec{ Engine: "mysql", - Generic: &kciv1alpha1.GenericInstance{ - Host: test.GetMysqlHost(), - Port: test.GetMysqlPort(), + DbInstanceSource: kciv1alpha1.DbInstanceSource{ + Generic: &kciv1alpha1.GenericInstance{ + Host: test.GetMysqlHost(), + Port: test.GetMysqlPort(), + }, }, }, Status: kciv1alpha1.DbInstanceStatus{Info: info}, diff --git a/pkg/controller/database/proxyHelper.go b/pkg/controller/database/proxyHelper.go index 6b172e75..bc84b4eb 100644 --- a/pkg/controller/database/proxyHelper.go +++ b/pkg/controller/database/proxyHelper.go @@ -7,6 +7,7 @@ import ( kciv1alpha1 "github.com/kloeckner-i/db-operator/pkg/apis/kci/v1alpha1" "github.com/kloeckner-i/db-operator/pkg/utils/kci" proxy "github.com/kloeckner-i/db-operator/pkg/utils/proxy" + "github.com/kloeckner-i/db-operator/pkg/utils/proxy/proxysql" "github.com/sirupsen/logrus" ) @@ -31,15 +32,15 @@ func determinProxyType(dbcr *kciv1alpha1.Database) (proxy.Proxy, error) { return nil, err } + portString := instance.Status.Info["DB_PORT"] + port, err := strconv.Atoi(portString) + if err != nil { + logrus.Errorf("can not convert DB_PORT to int - %s", err) + return nil, err + } + switch backend { case "google": - portString := instance.Status.Info["DB_PORT"] - port, err := strconv.Atoi(portString) - if err != nil { - logrus.Errorf("can not convert DB_PORT to int - %s", err) - return nil, err - } - labels := map[string]string{ "app": "cloudproxy", "db-name": dbcr.Name, @@ -54,6 +55,32 @@ func determinProxyType(dbcr *kciv1alpha1.Database) (proxy.Proxy, error) { Port: int32(port), Labels: kci.LabelBuilder(labels), }, nil + case "percona": + labels := map[string]string{ + "app": "proxysql", + "db-name": dbcr.Name, + } + + var backends []proxysql.Backend + for _, s := range instance.Spec.Percona.ServerList { + backend := proxysql.Backend{ + Host: s.Host, + Port: strconv.FormatInt(int64(s.Port), 10), + MaxConn: strconv.FormatInt(int64(s.MaxConnection), 10), + ReadOnly: s.ReadOnly, + } + backends = append(backends, backend) + } + + return &proxy.ProxySQL{ + NamePrefix: "db-" + dbcr.Name, + Namespace: dbcr.Namespace, + Servers: backends, + UserSecretName: dbcr.Spec.SecretName, + MonitorUserSecretName: dbcr.Status.MonitorUserSecretName, + Engine: engine, + Labels: kci.LabelBuilder(labels), + }, nil default: err := errors.New("not supported backend type") return nil, err diff --git a/pkg/controller/database/reconcileDatabase.go b/pkg/controller/database/reconcileDatabase.go index c64c207e..f173009e 100644 --- a/pkg/controller/database/reconcileDatabase.go +++ b/pkg/controller/database/reconcileDatabase.go @@ -114,39 +114,6 @@ func (r *ReconcileDatabase) createDatabase(dbcr *kciv1alpha1.Database) error { return err } - instance, err := dbcr.GetInstanceRef() - if err != nil { - return err - } - - databaseConfigResource := &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - Kind: "ConfigMap", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: dbcr.Namespace, - Name: dbcr.Spec.SecretName, - Labels: kci.BaseLabelBuilder(), - }, - Data: instance.Status.Info, - } - - err = r.client.Create(context.TODO(), databaseConfigResource) - if err != nil { - if k8serrors.IsAlreadyExists(err) { - // if configmap resource already exists, update - err = r.client.Update(context.TODO(), databaseConfigResource) - if err != nil { - logrus.Errorf("DB: namespace=%s, name=%s failed updating database info configmap", dbcr.Namespace, dbcr.Name) - return err - } - } else { - logrus.Errorf("DB: namespace=%s, name=%s failed creating database info configmap", dbcr.Namespace, dbcr.Name) - return err - } - } - logrus.Infof("DB: namespace=%s, name=%s successfully created", dbcr.Namespace, dbcr.Name) return nil } diff --git a/pkg/controller/database/reconcileInfoConfigMap.go b/pkg/controller/database/reconcileInfoConfigMap.go new file mode 100644 index 00000000..2933478a --- /dev/null +++ b/pkg/controller/database/reconcileInfoConfigMap.go @@ -0,0 +1,59 @@ +package database + +import ( + "context" + "strconv" + + kciv1alpha1 "github.com/kloeckner-i/db-operator/pkg/apis/kci/v1alpha1" + "github.com/kloeckner-i/db-operator/pkg/utils/kci" + "github.com/sirupsen/logrus" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (r *ReconcileDatabase) createInfoConfigMap(dbcr *kciv1alpha1.Database) error { + instance, err := dbcr.GetInstanceRef() + if err != nil { + return err + } + + info := instance.Status.DeepCopy().Info + proxyStatus := dbcr.Status.ProxyStatus + + if proxyStatus.Status == true { + info["DB_HOST"] = proxyStatus.ServiceName + info["DB_PORT"] = strconv.FormatInt(int64(proxyStatus.SQLPort), 10) + } + + databaseConfigResource := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: dbcr.Namespace, + Name: dbcr.Spec.SecretName, + Labels: kci.BaseLabelBuilder(), + }, + Data: info, + } + + err = r.client.Create(context.TODO(), databaseConfigResource) + if err != nil { + if k8serrors.IsAlreadyExists(err) { + // if configmap resource already exists, update + err = r.client.Update(context.TODO(), databaseConfigResource) + if err != nil { + logrus.Errorf("DB: namespace=%s, name=%s failed updating database info configmap", dbcr.Namespace, dbcr.Name) + return err + } + } else { + logrus.Errorf("DB: namespace=%s, name=%s failed creating database info configmap", dbcr.Namespace, dbcr.Name) + return err + } + } + + logrus.Infof("DB: namespace=%s, name=%s database info configmap created", dbcr.Namespace, dbcr.Name) + return nil +} diff --git a/pkg/controller/database/reconcileProxy.go b/pkg/controller/database/reconcileProxy.go index 16d49cc4..851c1edb 100644 --- a/pkg/controller/database/reconcileProxy.go +++ b/pkg/controller/database/reconcileProxy.go @@ -2,24 +2,59 @@ package database import ( "context" + kciv1alpha1 "github.com/kloeckner-i/db-operator/pkg/apis/kci/v1alpha1" + "github.com/kloeckner-i/db-operator/pkg/utils/kci" proxy "github.com/kloeckner-i/db-operator/pkg/utils/proxy" "github.com/sirupsen/logrus" + v1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" ) func (r *ReconcileDatabase) createProxy(dbcr *kciv1alpha1.Database) error { - if backend, _ := dbcr.GetBackendType(); backend != "google" { + backend, _ := dbcr.GetBackendType() + if backend == "generic" { logrus.Infof("DB: namespace=%s, name=%s %s proxy creation is not yet implemented skipping...", dbcr.Namespace, dbcr.Name, backend) return nil } + if backend == "percona" { + err := r.replicateMonitorUserSecret(dbcr) + if err != nil { + return err + } + } + proxyInterface, err := determinProxyType(dbcr) if err != nil { return err } + + // create proxy configmap + cm, err := proxy.BuildConfigmap(proxyInterface) + if err != nil { + return err + } + if cm != nil { // if configmap is not null + err = r.client.Create(context.TODO(), cm) + if err != nil { + if k8serrors.IsAlreadyExists(err) { + // if resource already exists, update + err = r.client.Update(context.TODO(), cm) + if err != nil { + logrus.Errorf("DB: namespace=%s, name=%s failed updating proxy configmap", dbcr.Namespace, dbcr.Name) + return err + } + } else { + // failed creating configmap + logrus.Errorf("DB: namespace=%s, name=%s failed updating proxy configmap", dbcr.Namespace, dbcr.Name) + return err + } + } + } + // create proxy deployment deploy, err := proxy.BuildDeployment(proxyInterface) if err != nil { @@ -62,6 +97,53 @@ func (r *ReconcileDatabase) createProxy(dbcr *kciv1alpha1.Database) error { return err } } + + engine, _ := dbcr.GetEngineType() + dbcr.Status.ProxyStatus.ServiceName = svc.Name + for _, svcPort := range svc.Spec.Ports { + if svcPort.Name == engine { + dbcr.Status.ProxyStatus.SQLPort = svcPort.Port + } + } + dbcr.Status.ProxyStatus.Status = true + logrus.Infof("DB: namespace=%s, name=%s proxy created", dbcr.Namespace, dbcr.Name) return nil } + +func (r *ReconcileDatabase) replicateMonitorUserSecret(dbcr *kciv1alpha1.Database) error { + dbin, err := dbcr.GetInstanceRef() + if err != nil { + return err + } + source := dbin.Spec.DbInstanceSource + + key := source.Percona.MonitorUserSecret + monitorUserSecret := &v1.Secret{} + + err = r.client.Get(context.TODO(), key, monitorUserSecret) + if err != nil { + logrus.Errorf("DB: namespace=%s, name=%s couldn't get monitor user secret - %s", dbcr.Namespace, dbcr.Name, err) + return err + } + + newSecret := kci.SecretBuilder(dbcr.Name+"-proxysql-monitoruser", dbcr.Namespace, monitorUserSecret.Data) + err = r.client.Create(context.TODO(), newSecret) + if err != nil { + if k8serrors.IsAlreadyExists(err) { + // if resource already exists, update + err = r.client.Update(context.TODO(), monitorUserSecret) + if err != nil { + logrus.Errorf("DB: namespace=%s, name=%s failed replicating monitor user secret", dbcr.Namespace, dbcr.Name) + return err + } + } else { + // failed to create deployment + logrus.Errorf("DB: namespace=%s, name=%s failed replicating monitor user secret", dbcr.Namespace, dbcr.Name) + return err + } + } + + dbcr.Status.MonitorUserSecretName = dbcr.Name + "-proxysql-monitoruser" + return nil +} diff --git a/pkg/controller/dbinstance/proxyHelper.go b/pkg/controller/dbinstance/proxyHelper.go index 47f5b723..24cdacf8 100644 --- a/pkg/controller/dbinstance/proxyHelper.go +++ b/pkg/controller/dbinstance/proxyHelper.go @@ -28,7 +28,13 @@ func determinProxyType(dbin *kciv1alpha1.DbInstance) (proxy.Proxy, error) { return nil, err } - if dbin.Spec.Google != nil { + backend, err := dbin.GetBackendType() + if err != nil { + return nil, err + } + + switch backend { + case "google": portString := dbin.Status.Info["DB_PORT"] port, err := strconv.Atoi(portString) if err != nil { @@ -50,7 +56,7 @@ func determinProxyType(dbin *kciv1alpha1.DbInstance) (proxy.Proxy, error) { Port: int32(port), Labels: kci.LabelBuilder(labels), }, nil + default: + return nil, ErrNoProxySupport } - - return nil, ErrNoProxySupport } diff --git a/pkg/controller/dbinstance/reconcileDbInstance.go b/pkg/controller/dbinstance/reconcileDbInstance.go index 86e8b9cf..6c1c99d3 100644 --- a/pkg/controller/dbinstance/reconcileDbInstance.go +++ b/pkg/controller/dbinstance/reconcileDbInstance.go @@ -52,6 +52,19 @@ func (r *ReconcileDbInstance) create(dbin *kciv1alpha1.DbInstance) error { User: cred.Username, Password: cred.Password, } + case "percona": + if dbin.Spec.Engine != "mysql" { + logrus.Errorf("Instance: name=%s - non mysql percona instance not supported", dbin.Name) + return errors.New("non mysql percona instance not supported") + } + + instance = &dbinstance.Generic{ + Host: dbin.Spec.Percona.ServerList[0].Host, + Port: dbin.Spec.Percona.ServerList[0].Port, + Engine: dbin.Spec.Engine, + User: cred.Username, + Password: cred.Password, + } default: return errors.New("not supported backend type") } diff --git a/pkg/test/helper.go b/pkg/test/helper.go index 4822b0f7..2ef182ff 100644 --- a/pkg/test/helper.go +++ b/pkg/test/helper.go @@ -16,13 +16,13 @@ func GetMysqlHost() string { } // GetMysqlPort set mysql port which used by unit test -func GetMysqlPort() int32 { +func GetMysqlPort() uint16 { if value, ok := os.LookupEnv("MYSQL_PORT"); ok { port, err := strconv.Atoi(value) if err != nil { logrus.Fatal(err) } - return int32(port) + return uint16(port) } return 3306 } @@ -44,13 +44,13 @@ func GetPostgresHost() string { } // GetPostgresPort set postgres port which used by unit test -func GetPostgresPort() int32 { +func GetPostgresPort() uint16 { if value, ok := os.LookupEnv("POSTGRES_PORT"); ok { port, err := strconv.Atoi(value) if err != nil { logrus.Fatal(err) } - return int32(port) + return uint16(port) } return 5432 } diff --git a/pkg/utils/database/mysql.go b/pkg/utils/database/mysql.go index c6165e45..3419048f 100644 --- a/pkg/utils/database/mysql.go +++ b/pkg/utils/database/mysql.go @@ -21,7 +21,7 @@ import ( type Mysql struct { Backend string Host string - Port int32 + Port uint16 Database string User string Password string diff --git a/pkg/utils/database/postgres.go b/pkg/utils/database/postgres.go index 6e0ab6d8..0f3acd0a 100644 --- a/pkg/utils/database/postgres.go +++ b/pkg/utils/database/postgres.go @@ -23,7 +23,7 @@ import ( type Postgres struct { Backend string Host string - Port int32 + Port uint16 Database string User string Password string diff --git a/pkg/utils/dbinstance/generic.go b/pkg/utils/dbinstance/generic.go index 6360c324..9ebeb1c2 100644 --- a/pkg/utils/dbinstance/generic.go +++ b/pkg/utils/dbinstance/generic.go @@ -12,7 +12,7 @@ import ( // Generic represents database instance which can be connected by address and port type Generic struct { Host string - Port int32 + Port uint16 Engine string User string Password string diff --git a/pkg/utils/proxy/cloudproxy.go b/pkg/utils/proxy/cloudproxy.go index 4b8673a6..1582792c 100644 --- a/pkg/utils/proxy/cloudproxy.go +++ b/pkg/utils/proxy/cloudproxy.go @@ -11,6 +11,17 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// CloudProxy for google sql instance +type CloudProxy struct { + NamePrefix string + Namespace string + InstanceConnectionName string + AccessSecretName string + Engine string + Port int32 + Labels map[string]string +} + var conf = config.Config{} const instanceAccessSecretVolumeName string = "gcloud-secret" @@ -40,7 +51,7 @@ func (cp *CloudProxy) buildService() (*v1.Service, error) { } func (cp *CloudProxy) buildDeployment() (*v1apps.Deployment, error) { - spec, err := deploymentSpec(cp.InstanceConnectionName, cp.Port, cp.Labels, cp.AccessSecretName) + spec, err := cp.deploymentSpec() if err != nil { return nil, err } @@ -57,27 +68,35 @@ func (cp *CloudProxy) buildDeployment() (*v1apps.Deployment, error) { }, Spec: spec, }, nil - } -func deploymentSpec(conn string, port int32, labels map[string]string, instanceAccessSecret string) (v1apps.DeploymentSpec, error) { +func (cp *CloudProxy) deploymentSpec() (v1apps.DeploymentSpec, error) { var replicas int32 = 2 - container, err := container(conn, port) + container, err := cp.container() if err != nil { return v1apps.DeploymentSpec{}, err } - volumes := buildVolumes(instanceAccessSecret) + volumes := []v1.Volume{ + v1.Volume{ + Name: instanceAccessSecretVolumeName, + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: cp.AccessSecretName, + }, + }, + }, + } return v1apps.DeploymentSpec{ Replicas: &replicas, Selector: &metav1.LabelSelector{ - MatchLabels: labels, + MatchLabels: cp.Labels, }, Template: v1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: labels, + Labels: cp.Labels, }, Spec: v1.PodSpec{ Containers: []v1.Container{container}, @@ -85,17 +104,17 @@ func deploymentSpec(conn string, port int32, labels map[string]string, instanceA RestartPolicy: v1.RestartPolicyAlways, Volumes: volumes, Affinity: &v1.Affinity{ - PodAntiAffinity: podAntiAffinity(labels), + PodAntiAffinity: podAntiAffinity(cp.Labels), }, }, }, }, nil } -func container(conn string, port int32) (v1.Container, error) { +func (cp *CloudProxy) container() (v1.Container, error) { RunAsUser := int64(2) AllowPrivilegeEscalation := false - instanceArg := fmt.Sprintf("-instances=%s=tcp:0.0.0.0:%s", conn, strconv.FormatInt(int64(port), 10)) + instanceArg := fmt.Sprintf("-instances=%s=tcp:0.0.0.0:%s", cp.InstanceConnectionName, strconv.FormatInt(int64(cp.Port), 10)) return v1.Container{ Name: "cloudsql-proxy", @@ -110,7 +129,7 @@ func container(conn string, port int32) (v1.Container, error) { Ports: []v1.ContainerPort{ v1.ContainerPort{ Name: "sqlport", - ContainerPort: port, + ContainerPort: cp.Port, Protocol: v1.ProtocolTCP, }, }, @@ -123,40 +142,6 @@ func container(conn string, port int32) (v1.Container, error) { }, nil } -func buildVolumes(instanceAccessSecretName string) []v1.Volume { - return []v1.Volume{ - v1.Volume{ - Name: instanceAccessSecretVolumeName, - VolumeSource: v1.VolumeSource{ - Secret: &v1.SecretVolumeSource{ - SecretName: instanceAccessSecretName, - }, - }, - }, - } -} - -func podAntiAffinity(labelSelector map[string]string) *v1.PodAntiAffinity { - var weight int32 = 1 - return &v1.PodAntiAffinity{ - RequiredDuringSchedulingIgnoredDuringExecution: []v1.PodAffinityTerm{ - v1.PodAffinityTerm{ - LabelSelector: &metav1.LabelSelector{ - MatchLabels: labelSelector, - }, - TopologyKey: "kubernetes.io/hostname", - }, - }, - PreferredDuringSchedulingIgnoredDuringExecution: []v1.WeightedPodAffinityTerm{ - v1.WeightedPodAffinityTerm{ - PodAffinityTerm: v1.PodAffinityTerm{ - LabelSelector: &metav1.LabelSelector{ - MatchLabels: labelSelector, - }, - TopologyKey: "failure-domain.beta.kubernetes.io/zone", - }, - Weight: weight, - }, - }, - } +func (cp *CloudProxy) buildConfigMap() (*v1.ConfigMap, error) { + return nil, nil } diff --git a/pkg/utils/proxy/create.go b/pkg/utils/proxy/create.go index 76d2fa2c..3a6f500f 100644 --- a/pkg/utils/proxy/create.go +++ b/pkg/utils/proxy/create.go @@ -10,7 +10,7 @@ import ( func BuildDeployment(proxy Proxy) (*v1apps.Deployment, error) { deploy, err := proxy.buildDeployment() if err != nil { - logrus.Error("failed building cloudsql proxy deployment") + logrus.Error("failed building proxy deployment") return nil, err } @@ -21,9 +21,20 @@ func BuildDeployment(proxy Proxy) (*v1apps.Deployment, error) { func BuildService(proxy Proxy) (*v1.Service, error) { svc, err := proxy.buildService() if err != nil { - logrus.Error("failed building cloudsql proxy service") + logrus.Error("failed building proxy service") return nil, err } return svc, nil } + +// BuildConfigmap builds kubernetes configmap object used by proxy container of the database +func BuildConfigmap(proxy Proxy) (*v1.ConfigMap, error) { + cm, err := proxy.buildConfigMap() + if err != nil { + logrus.Error("failed building proxy configmap") + return nil, err + } + + return cm, nil +} diff --git a/pkg/utils/proxy/helper.go b/pkg/utils/proxy/helper.go new file mode 100644 index 00000000..c20a84b6 --- /dev/null +++ b/pkg/utils/proxy/helper.go @@ -0,0 +1,31 @@ +package proxy + +import ( + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func podAntiAffinity(labelSelector map[string]string) *v1.PodAntiAffinity { + var weight int32 = 1 + return &v1.PodAntiAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: []v1.PodAffinityTerm{ + v1.PodAffinityTerm{ + LabelSelector: &metav1.LabelSelector{ + MatchLabels: labelSelector, + }, + TopologyKey: "kubernetes.io/hostname", + }, + }, + PreferredDuringSchedulingIgnoredDuringExecution: []v1.WeightedPodAffinityTerm{ + v1.WeightedPodAffinityTerm{ + PodAffinityTerm: v1.PodAffinityTerm{ + LabelSelector: &metav1.LabelSelector{ + MatchLabels: labelSelector, + }, + TopologyKey: "failure-domain.beta.kubernetes.io/zone", + }, + Weight: weight, + }, + }, + } +} diff --git a/pkg/utils/proxy/proxysql.go b/pkg/utils/proxy/proxysql.go new file mode 100644 index 00000000..59ea5eb0 --- /dev/null +++ b/pkg/utils/proxy/proxysql.go @@ -0,0 +1,280 @@ +package proxy + +import ( + "bytes" + "log" + "strconv" + "text/template" + + proxysql "github.com/kloeckner-i/db-operator/pkg/utils/proxy/proxysql" + + v1apps "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ProxySQL for percona cluster +type ProxySQL struct { + NamePrefix string + Namespace string + Servers []proxysql.Backend + UserSecretName string + MonitorUserSecretName string + Engine string + Labels map[string]string +} + +const sqlPort = 6033 +const adminPort = 6032 + +func (ps *ProxySQL) configMapName() string { + return ps.NamePrefix + "-proxysql-config-template" +} + +func (ps *ProxySQL) serviceName() string { + return ps.NamePrefix + "-proxysql" +} + +func (ps *ProxySQL) buildService() (*v1.Service, error) { + return &v1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: ps.serviceName(), + Namespace: ps.Namespace, + Labels: ps.Labels, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + v1.ServicePort{ + Name: ps.Engine, + Protocol: v1.ProtocolTCP, + Port: sqlPort, + }, + }, + Selector: ps.Labels, + }, + }, nil +} + +func (ps *ProxySQL) buildDeployment() (*v1apps.Deployment, error) { + spec, err := ps.deploymentSpec() + if err != nil { + return nil, err + } + + return &v1apps.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: "apps/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: ps.NamePrefix + "-proxysql", + Namespace: ps.Namespace, + Labels: ps.Labels, + }, + Spec: spec, + }, nil +} + +func (ps *ProxySQL) deploymentSpec() (v1apps.DeploymentSpec, error) { + var replicas int32 = 2 + + configGenContainer, err := ps.configGeneratorContainer() + if err != nil { + return v1apps.DeploymentSpec{}, err + } + + proxyContainer, err := ps.proxyContainer() + if err != nil { + return v1apps.DeploymentSpec{}, err + } + + volumes := []v1.Volume{ + v1.Volume{ + Name: "shared-data", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + }, + v1.Volume{ + Name: "proxysql-config-template", + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: ps.configMapName(), + }, + }, + }, + }, + v1.Volume{ + Name: "monitoruser-secret", + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: ps.MonitorUserSecretName, + Items: []v1.KeyToPath{ + v1.KeyToPath{ + Key: "password", + Path: "monitoruser-password", + }, + }, + }, + }, + }, + v1.Volume{ + Name: "user-secret", + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: ps.UserSecretName, + Items: []v1.KeyToPath{ + v1.KeyToPath{ + Key: "PASSWORD", + Path: "user-password", + }, + }, + }, + }, + }, + } + + return v1apps.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: ps.Labels, + }, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: ps.Labels, + }, + Spec: v1.PodSpec{ + InitContainers: []v1.Container{configGenContainer}, + Containers: []v1.Container{proxyContainer}, + NodeSelector: conf.Instances.Percona.ProxyConfig.NodeSelector, + RestartPolicy: v1.RestartPolicyAlways, + Volumes: volumes, + Affinity: &v1.Affinity{ + PodAntiAffinity: podAntiAffinity(ps.Labels), + }, + }, + }, + }, nil +} + +func (ps *ProxySQL) configGeneratorContainer() (v1.Container, error) { + return v1.Container{ + Name: "config-generator", + Image: "alpine", + ImagePullPolicy: v1.PullIfNotPresent, + Command: []string{"sh", "-c", "apk add --update gettext && MONITOR_PASSWORD=$(cat /run/secrets/monitoruser-password) DB_PASSWORD=$(cat /run/secrets/user-password) envsubst < /tmp/proxysql.cnf.tmpl > /mnt/proxysql.cnf"}, + Env: []v1.EnvVar{ + v1.EnvVar{ + Name: "MONITOR_USERNAME", ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{Name: ps.MonitorUserSecretName}, + Key: "user", + }, + }, + }, + v1.EnvVar{ + Name: "DB_USERNAME", ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{Name: ps.UserSecretName}, + Key: "USER", + }, + }, + }, + }, + VolumeMounts: []v1.VolumeMount{ + v1.VolumeMount{ + Name: "proxysql-config-template", + MountPath: "/tmp/proxysql.cnf.tmpl", + SubPath: "proxysql.cnf.tmpl", + }, + v1.VolumeMount{ + Name: "shared-data", + MountPath: "/mnt", + }, + v1.VolumeMount{ + Name: "monitoruser-secret", + MountPath: "/run/secrets/monitoruser-password", + SubPath: "monitoruser-password", + ReadOnly: true, + }, + v1.VolumeMount{ + Name: "user-secret", + MountPath: "/run/secrets/user-password", + SubPath: "user-password", + ReadOnly: true, + }, + }, + }, nil +} + +func (ps *ProxySQL) proxyContainer() (v1.Container, error) { + return v1.Container{ + Name: "proxysql", + Image: conf.Instances.Percona.ProxyConfig.Image, + ImagePullPolicy: v1.PullIfNotPresent, + Ports: []v1.ContainerPort{ + v1.ContainerPort{ + Name: "sql", + ContainerPort: sqlPort, + Protocol: v1.ProtocolTCP, + }, + v1.ContainerPort{ + Name: "admin", + ContainerPort: adminPort, + Protocol: v1.ProtocolTCP, + }, + }, + VolumeMounts: []v1.VolumeMount{ + v1.VolumeMount{ + Name: "shared-data", + MountPath: "/etc/proxysql.cnf", + SubPath: "proxysql.cnf", + }, + }, + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("50m"), + v1.ResourceMemory: resource.MustParse("128Mi"), + }, + }, + }, nil +} + +func (ps *ProxySQL) buildConfigMap() (*v1.ConfigMap, error) { + configTmpl := proxysql.PerconaMysqlConfigTemplate + t := template.Must(template.New("config").Parse(configTmpl)) + + config := proxysql.Config{ + AdminPort: strconv.FormatInt(int64(adminPort), 10), + SQLPort: strconv.FormatInt(int64(sqlPort), 10), + Backends: ps.Servers, + } + + var outputBuf bytes.Buffer + if err := t.Execute(&outputBuf, config); err != nil { + log.Fatal(err) + } + + data := map[string]string{ + "proxysql.cnf.tmpl": outputBuf.String(), + } + + return &v1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: ps.Namespace, + Name: ps.configMapName(), + Labels: ps.Labels, + }, + Data: data, + }, nil +} diff --git a/pkg/utils/proxy/proxysql/template.go b/pkg/utils/proxy/proxysql/template.go new file mode 100644 index 00000000..04c2e7f9 --- /dev/null +++ b/pkg/utils/proxy/proxysql/template.go @@ -0,0 +1,110 @@ +package proxysql + +// Config defines list of configurable items +// Items will be applied in config by templating +type Config struct { + AdminPort string + SQLPort string + Backends []Backend +} + +// Backend defines list of servers behind of proxysql +type Backend struct { + Host, Port, MaxConn string + ReadOnly bool +} + +// PerconaMysqlConfigTemplate defines proxysql config template for mysql percona cluster +// Later this could be moved out completely to outside so that it's more configurable. +const PerconaMysqlConfigTemplate = `datadir="/var/lib/proxysql" + +admin_variables= +{ + mysql_ifaces="0.0.0.0:{{.AdminPort}}" + refresh_interval=2000 + web_enabled=false + web_port=6080 + stats_credentials="stats:admin" +} + +mysql_variables= +{ + threads=4 + max_connections=2048 + default_query_delay=0 + default_query_timeout=36000000 + have_compress=true + poll_timeout=2000 + interfaces="0.0.0.0:{{.SQLPort}};/tmp/proxysql.sock" + default_schema="information_schema" + stacksize=1048576 + server_version="5.7.28" + connect_timeout_server=10000 + monitor_history=60000 + monitor_connect_interval=200000 + monitor_ping_interval=200000 + ping_interval_server_msec=10000 + ping_timeout_server=200 + commands_stats=true + sessions_sort=true + monitor_username="$MONITOR_USERNAME" + monitor_password="$MONITOR_PASSWORD" + monitor_galera_healthcheck_interval=2000 + monitor_galera_healthcheck_timeout=800 +} + +mysql_galera_hostgroups = +( + { + writer_hostgroup=10 + backup_writer_hostgroup=20 + reader_hostgroup=30 + offline_hostgroup=9999 + max_writers=2 + writer_is_also_reader=2 + max_transactions_behind=30 + active=1 + } +) + +mysql_servers = +( + {{- range .Backends }} + {{- if .ReadOnly }} + { address="{{.Host}}", port={{.Port}}, hostgroup=20, max_connections={{.MaxConn}} }, + {{- else }} + { address="{{.Host}}", port={{.Port}}, hostgroup=10, max_connections={{.MaxConn}} }, + {{- end }} + {{- end }} +) + +mysql_users = +( + { username = "$DB_USERNAME", password = "$DB_PASSWORD", default_hostgroup = 10, transaction_persistent = 0, active = 1 } +) + +mysql_query_rules = +( + { + rule_id=100 + active=1 + match_pattern="^SELECT .* FOR UPDATE" + destination_hostgroup=10 + apply=1 + }, + { + rule_id=200 + active=1 + match_pattern="^SELECT .*" + destination_hostgroup=20 + apply=1 + }, + { + rule_id=300 + active=1 + match_pattern=".*" + destination_hostgroup=10 + apply=1 + } +) +` diff --git a/pkg/utils/proxy/types.go b/pkg/utils/proxy/types.go index 3b5f62dd..deed6011 100644 --- a/pkg/utils/proxy/types.go +++ b/pkg/utils/proxy/types.go @@ -5,19 +5,9 @@ import ( v1 "k8s.io/api/core/v1" ) -// CloudProxy for google sql instance -type CloudProxy struct { - NamePrefix string - Namespace string - InstanceConnectionName string - AccessSecretName string - Engine string - Port int32 - Labels map[string]string -} - // Proxy for database type Proxy interface { buildDeployment() (*v1apps.Deployment, error) buildService() (*v1.Service, error) + buildConfigMap() (*v1.ConfigMap, error) }