├── roles └── mariadb │ ├── defaults │ └── main.yml │ ├── templates │ ├── mariadb_secret.yaml.j2 │ ├── mariadb_service.yaml.j2 │ └── mariadb_statefulset.yaml.j2 │ ├── tasks │ └── main.yml │ ├── README.md │ └── meta │ └── main.yml ├── main.yml ├── requirements.yml ├── .ansible-lint ├── .github ├── FUNDING.yml └── workflows │ └── stale.yml ├── watches.yaml ├── deploy ├── service_account.yaml ├── crds │ ├── mariadb_v1alpha1_mariadb_cr.yaml │ └── mariadb_v1alpha1_mariadb_crd.yaml ├── role_binding.yaml ├── role.yaml ├── operator.yaml └── mariadb-operator.yaml ├── .yamllint ├── molecule ├── default │ ├── converge.yml │ ├── molecule.yml │ ├── prepare.yml │ └── asserts.yml ├── test-cluster │ ├── molecule.yml │ └── converge.yml └── test-local │ ├── prepare.yml │ ├── molecule.yml │ └── converge.yml ├── .travis.yml └── README.md /roles/mariadb/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # defaults file for mariadb 3 | -------------------------------------------------------------------------------- /main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | gather_facts: no 4 | roles: 5 | - mariadb 6 | -------------------------------------------------------------------------------- /requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: 3 | - community.kubernetes 4 | - operator_sdk.util 5 | -------------------------------------------------------------------------------- /.ansible-lint: -------------------------------------------------------------------------------- 1 | skip_list: 2 | - '306' 3 | - '602' 4 | - '503' 5 | 6 | exclude_paths: 7 | - deploy/ 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | --- 3 | github: geerlingguy 4 | patreon: geerlingguy 5 | -------------------------------------------------------------------------------- /watches.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - version: v1alpha1 3 | group: mariadb.mariadb.com 4 | kind: MariaDB 5 | playbook: /opt/ansible/main.yml 6 | -------------------------------------------------------------------------------- /deploy/service_account.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: mariadb-operator 6 | namespace: default 7 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | rules: 5 | truthy: disable 6 | line-length: 7 | max: 160 8 | level: warning 9 | 10 | ignore: | 11 | .github/workflows/stale.yml 12 | -------------------------------------------------------------------------------- /roles/mariadb/templates/mariadb_secret.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: '{{ meta.name }}-pass' 6 | namespace: {{ meta.namespace }} 7 | data: 8 | password: {{ mariadb_password | b64encode }} 9 | -------------------------------------------------------------------------------- /molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: localhost 4 | connection: local 5 | vars: 6 | ansible_python_interpreter: '{{ ansible_playbook_python }}' 7 | roles: 8 | - mariadb 9 | 10 | - import_playbook: '{{ playbook_dir }}/asserts.yml' 11 | -------------------------------------------------------------------------------- /roles/mariadb/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure configured MariaDB resources exist in the cluster. 3 | k8s: 4 | definition: "{{ lookup('template', item) | from_yaml }}" 5 | with_items: 6 | - mariadb_secret.yaml.j2 7 | - mariadb_statefulset.yaml.j2 8 | - mariadb_service.yaml.j2 9 | -------------------------------------------------------------------------------- /roles/mariadb/templates/mariadb_service.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: '{{ meta.name }}' 6 | namespace: '{{ meta.namespace }}' 7 | labels: 8 | app: mariadb 9 | spec: 10 | ports: 11 | - port: 3306 12 | clusterIP: None 13 | selector: 14 | app: mariadb 15 | -------------------------------------------------------------------------------- /deploy/crds/mariadb_v1alpha1_mariadb_cr.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: mariadb.mariadb.com/v1alpha1 3 | kind: MariaDB 4 | metadata: 5 | name: example-mariadb 6 | namespace: example-mariadb 7 | spec: 8 | masters: 1 9 | replicas: 0 10 | mariadb_password: CHANGEME 11 | mariadb_image: mariadb:10.4 12 | mariadb_pvc_storage_request: 1Gi 13 | -------------------------------------------------------------------------------- /deploy/role_binding.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: ClusterRoleBinding 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | name: mariadb-operator 6 | subjects: 7 | - kind: ServiceAccount 8 | name: mariadb-operator 9 | namespace: default 10 | roleRef: 11 | kind: ClusterRole 12 | name: mariadb-operator 13 | apiGroup: rbac.authorization.k8s.io 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: required 3 | services: docker 4 | language: python 5 | 6 | before_install: 7 | # Upgrade Docker to work with docker-py. 8 | - curl https://gist.githubusercontent.com/geerlingguy/ce883ad4aec6a5f1187ef93bd338511e/raw/36612d28981d92863f839c5aefe5b7dd7193d6c6/travis-ci-docker-upgrade.sh | sudo bash 9 | 10 | install: 11 | - pip install ansible molecule[docker] yamllint ansible-lint docker openshift jmespath 12 | 13 | script: 14 | - molecule test -s test-local 15 | -------------------------------------------------------------------------------- /deploy/crds/mariadb_v1alpha1_mariadb_crd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1beta1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: mariadbs.mariadb.mariadb.com 6 | spec: 7 | group: mariadb.mariadb.com 8 | names: 9 | kind: MariaDB 10 | listKind: MariaDBList 11 | plural: mariadbs 12 | singular: mariadb 13 | scope: Namespaced 14 | subresources: 15 | status: {} 16 | version: v1alpha1 17 | versions: 18 | - name: v1alpha1 19 | served: true 20 | storage: true 21 | -------------------------------------------------------------------------------- /roles/mariadb/README.md: -------------------------------------------------------------------------------- 1 | MariaDB 2 | ======= 3 | 4 | This role builds and maintains a single or multiple-instance MariaDB database cluster inside of Kubernetes. 5 | 6 | Requirements 7 | ------------ 8 | 9 | TODO. 10 | 11 | Role Variables 12 | -------------- 13 | 14 | TODO. 15 | 16 | Dependencies 17 | ------------ 18 | 19 | N/A 20 | 21 | Example Playbook 22 | ---------------- 23 | 24 | - hosts: localhost 25 | connection: local 26 | roles: 27 | - mariadb 28 | 29 | License 30 | ------- 31 | 32 | MIT / BSD 33 | -------------------------------------------------------------------------------- /roles/mariadb/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: Jeff Geerling 4 | description: MariaDB role for MariaDB Operator for Kubernetes. 5 | company: Midwestern Mac, LLC 6 | 7 | license: MIT 8 | 9 | min_ansible_version: 2.8 10 | 11 | platforms: 12 | - name: EL 13 | versions: 14 | - all 15 | - name: Debian 16 | versions: 17 | - all 18 | 19 | galaxy_tags: 20 | - mariadb 21 | - mysql 22 | - database 23 | - rdbms 24 | - db 25 | - cluster 26 | 27 | dependencies: [] 28 | -------------------------------------------------------------------------------- /molecule/test-cluster/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: delegated 6 | options: 7 | managed: False 8 | ansible_connection_options: {} 9 | lint: | 10 | set -e 11 | yamllint . 12 | ansible-lint 13 | platforms: 14 | - name: test-cluster 15 | groups: 16 | - k8s 17 | provisioner: 18 | name: ansible 19 | inventory: 20 | group_vars: 21 | all: 22 | namespace: ${TEST_NAMESPACE:-default} 23 | lint: 24 | name: ansible-lint 25 | enabled: False 26 | env: 27 | ANSIBLE_ROLES_PATH: ${MOLECULE_PROJECT_DIRECTORY}/roles 28 | scenario: 29 | test_sequence: 30 | - lint 31 | - destroy 32 | - dependency 33 | - syntax 34 | - create 35 | - prepare 36 | - converge 37 | - side_effect 38 | - verify 39 | - destroy 40 | -------------------------------------------------------------------------------- /molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: docker 6 | lint: | 7 | set -e 8 | yamllint . 9 | ansible-lint 10 | platforms: 11 | - name: kind-default 12 | groups: 13 | - k8s 14 | image: bsycorp/kind:latest-1.15 15 | privileged: True 16 | override_command: no 17 | exposed_ports: 18 | - 8443/tcp 19 | - 10080/tcp 20 | published_ports: 21 | - 0.0.0.0:${TEST_CLUSTER_PORT:-9443}:8443/tcp 22 | pre_build_image: yes 23 | provisioner: 24 | name: ansible 25 | log: True 26 | inventory: 27 | group_vars: 28 | all: 29 | operator_namespace: ${TEST_NAMESPACE:-default} 30 | env: 31 | K8S_AUTH_KUBECONFIG: /tmp/molecule/kind-default/kubeconfig 32 | KUBECONFIG: /tmp/molecule/kind-default/kubeconfig 33 | ANSIBLE_ROLES_PATH: ${MOLECULE_PROJECT_DIRECTORY}/roles 34 | KIND_PORT: '${TEST_CLUSTER_PORT:-9443}' 35 | -------------------------------------------------------------------------------- /molecule/test-local/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - import_playbook: ../default/prepare.yml 3 | 4 | - name: Prepare operator resources 5 | hosts: localhost 6 | connection: local 7 | vars: 8 | ansible_python_interpreter: '{{ ansible_playbook_python }}' 9 | deploy_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/deploy" 10 | tasks: 11 | - name: Create Custom Resource Definition 12 | k8s: 13 | definition: "{{ lookup('file', '/'.join([deploy_dir, 'crds/mariadb_v1alpha1_mariadb_crd.yaml'])) }}" 14 | 15 | - name: Ensure specified namespace is present 16 | k8s: 17 | api_version: v1 18 | kind: Namespace 19 | name: '{{ operator_namespace }}' 20 | 21 | - name: Create RBAC resources 22 | k8s: 23 | definition: "{{ lookup('template', '/'.join([deploy_dir, item])) }}" 24 | namespace: '{{ operator_namespace }}' 25 | with_items: 26 | - role.yaml 27 | - role_binding.yaml 28 | - service_account.yaml 29 | -------------------------------------------------------------------------------- /molecule/default/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Prepare 3 | hosts: k8s 4 | gather_facts: no 5 | vars: 6 | kubeconfig: "{{ lookup('env', 'KUBECONFIG') }}" 7 | tasks: 8 | - name: delete the kubeconfig if present 9 | file: 10 | path: '{{ kubeconfig }}' 11 | state: absent 12 | delegate_to: localhost 13 | 14 | - name: Fetch the kubeconfig 15 | fetch: 16 | dest: '{{ kubeconfig }}' 17 | flat: yes 18 | src: /root/.kube/config 19 | 20 | - name: Change the kubeconfig port to the proper value 21 | replace: 22 | regexp: 8443 23 | replace: "{{ lookup('env', 'KIND_PORT') }}" 24 | path: '{{ kubeconfig }}' 25 | mode: 0644 26 | delegate_to: localhost 27 | 28 | - name: Wait for the Kubernetes API to become available (this could take a minute) 29 | uri: 30 | url: "http://localhost:10080/kubernetes-ready" 31 | status_code: 200 32 | validate_certs: no 33 | register: result 34 | until: (result.status|default(-1)) == 200 35 | retries: 60 36 | delay: 5 37 | -------------------------------------------------------------------------------- /molecule/test-local/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: docker 6 | lint: | 7 | set -e 8 | yamllint . 9 | ansible-lint 10 | platforms: 11 | - name: kind-test-local 12 | groups: 13 | - k8s 14 | image: bsycorp/kind:latest-1.15 15 | privileged: True 16 | override_command: no 17 | exposed_ports: 18 | - 8443/tcp 19 | - 10080/tcp 20 | published_ports: 21 | - 0.0.0.0:${TEST_CLUSTER_PORT:-10443}:8443/tcp 22 | pre_build_image: yes 23 | volumes: 24 | - ${MOLECULE_PROJECT_DIRECTORY}:/build:Z 25 | provisioner: 26 | name: ansible 27 | log: True 28 | inventory: 29 | group_vars: 30 | all: 31 | operator_namespace: ${TEST_NAMESPACE:-default} 32 | env: 33 | K8S_AUTH_KUBECONFIG: /tmp/molecule/kind-test-local/kubeconfig 34 | KUBECONFIG: /tmp/molecule/kind-test-local/kubeconfig 35 | ANSIBLE_ROLES_PATH: ${MOLECULE_PROJECT_DIRECTORY}/roles 36 | KIND_PORT: '${TEST_CLUSTER_PORT:-10443}' 37 | scenario: 38 | test_sequence: 39 | - lint 40 | - destroy 41 | - dependency 42 | - syntax 43 | - create 44 | - prepare 45 | - converge 46 | - side_effect 47 | - verify 48 | - destroy 49 | -------------------------------------------------------------------------------- /deploy/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | creationTimestamp: null 6 | name: mariadb-operator 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - pods 12 | - services 13 | - services/finalizers 14 | - endpoints 15 | - persistentvolumeclaims 16 | - events 17 | - configmaps 18 | - secrets 19 | verbs: 20 | - '*' 21 | - apiGroups: 22 | - apps 23 | - extensions 24 | resources: 25 | - deployments 26 | - daemonsets 27 | - replicasets 28 | - statefulsets 29 | verbs: 30 | - '*' 31 | - apiGroups: 32 | - monitoring.coreos.com 33 | resources: 34 | - servicemonitors 35 | verbs: 36 | - get 37 | - create 38 | - apiGroups: 39 | - apps 40 | resourceNames: 41 | - mariadb-operator 42 | resources: 43 | - deployments/finalizers 44 | verbs: 45 | - update 46 | - apiGroups: 47 | - "" 48 | resources: 49 | - pods 50 | verbs: 51 | - get 52 | - apiGroups: 53 | - apps 54 | resources: 55 | - replicasets 56 | verbs: 57 | - get 58 | - apiGroups: 59 | - mariadb.mariadb.com 60 | resources: 61 | - '*' 62 | verbs: 63 | - '*' 64 | -------------------------------------------------------------------------------- /deploy/operator.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: mariadb-operator 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | name: mariadb-operator 11 | template: 12 | metadata: 13 | labels: 14 | name: mariadb-operator 15 | spec: 16 | serviceAccountName: mariadb-operator 17 | containers: 18 | - name: ansible 19 | command: 20 | - /usr/local/bin/ao-logs 21 | - /tmp/ansible-operator/runner 22 | - stdout 23 | image: "{{ operator_image }}" 24 | imagePullPolicy: "{{ pull_policy|default('Always') }}" 25 | volumeMounts: 26 | - mountPath: /tmp/ansible-operator/runner 27 | name: runner 28 | readOnly: true 29 | - name: operator 30 | image: "{{ operator_image }}" 31 | imagePullPolicy: "{{ pull_policy|default('Always') }}" 32 | volumeMounts: 33 | - mountPath: /tmp/ansible-operator/runner 34 | name: runner 35 | env: 36 | # Watch all namespaces (cluster-scoped). 37 | - name: WATCH_NAMESPACE 38 | value: "" 39 | - name: POD_NAME 40 | valueFrom: 41 | fieldRef: 42 | fieldPath: metadata.name 43 | - name: OPERATOR_NAME 44 | value: "mariadb-operator" 45 | volumes: 46 | - name: runner 47 | emptyDir: {} 48 | -------------------------------------------------------------------------------- /molecule/test-cluster/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: localhost 4 | connection: local 5 | 6 | vars: 7 | ansible_python_interpreter: '{{ ansible_playbook_python }}' 8 | deploy_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/deploy" 9 | image_name: mariadb.mariadb.com/mariadb-operator:testing 10 | custom_resource: "{{ lookup('file', '/'.join([deploy_dir, 'crds/mariadb_v1alpha1_mariadb_cr.yaml'])) | from_yaml }}" 11 | 12 | tasks: 13 | - name: Create the mariadb.mariadb.com/v1alpha1.MariaDB 14 | k8s: 15 | namespace: '{{ namespace }}' 16 | definition: "{{ lookup('file', '/'.join([deploy_dir, 'crds/mariadb_v1alpha1_mariadb_cr.yaml'])) }}" 17 | 18 | - name: Get the newly created Custom Resource 19 | debug: 20 | msg: "{{ lookup('k8s', 21 | group='mariadb.mariadb.com', 22 | api_version='v1alpha1', 23 | kind='MariaDB', 24 | namespace=namespace, 25 | resource_name=custom_resource.metadata.name 26 | )}}" 27 | 28 | - name: Wait 60s for reconciliation to run 29 | k8s_info: 30 | api_version: 'v1alpha1' 31 | kind: 'MariaDB' 32 | namespace: '{{ namespace }}' 33 | name: '{{ custom_resource.metadata.name }}' 34 | register: reconcile_cr 35 | until: 36 | - "'Successful' in (reconcile_cr | json_query('resources[].status.conditions[].reason'))" 37 | delay: 6 38 | retries: 10 39 | 40 | - import_playbook: '{{ playbook_dir }}/../default/asserts.yml' 41 | -------------------------------------------------------------------------------- /roles/mariadb/templates/mariadb_statefulset.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: StatefulSet 4 | metadata: 5 | name: '{{ meta.name }}' 6 | namespace: '{{ meta.namespace }}' 7 | labels: 8 | app: mariadb 9 | spec: 10 | selector: 11 | matchLabels: 12 | app: mariadb 13 | serviceName: '{{ meta.name }}' 14 | replicas: 1 15 | updateStrategy: 16 | type: RollingUpdate 17 | template: 18 | metadata: 19 | labels: 20 | app: mariadb 21 | spec: 22 | containers: 23 | - image: '{{ mariadb_image }}' 24 | name: mariadb 25 | env: 26 | - name: MYSQL_DATABASE 27 | value: db 28 | - name: MYSQL_USER 29 | value: db_user 30 | - name: MYSQL_PASSWORD 31 | valueFrom: 32 | secretKeyRef: 33 | name: '{{ meta.name }}-pass' 34 | key: password 35 | - name: MYSQL_RANDOM_ROOT_PASSWORD 36 | value: 'true' 37 | # resources: 38 | # limits: 39 | # cpu: "2" 40 | # memory: 2048Mi 41 | # requests: 42 | # cpu: "500m" 43 | # memory: 512Mi 44 | ports: 45 | - containerPort: 3306 46 | name: mariadb 47 | volumeMounts: 48 | - name: mariadb 49 | mountPath: /var/lib/mysql 50 | subPath: mysql 51 | volumeClaimTemplates: 52 | - metadata: 53 | name: mariadb 54 | spec: 55 | accessModes: 56 | - ReadWriteOnce 57 | resources: 58 | requests: 59 | storage: '{{ mariadb_pvc_storage_request }}' 60 | -------------------------------------------------------------------------------- /molecule/default/asserts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify cluster resources 3 | hosts: localhost 4 | connection: local 5 | 6 | vars: 7 | ansible_python_interpreter: '{{ ansible_playbook_python }}' 8 | 9 | tasks: 10 | - name: Get mariadb Pod data 11 | k8s_info: 12 | kind: Pod 13 | namespace: example-mariadb 14 | label_selectors: 15 | - app=mariadb 16 | register: mariadb_pods 17 | 18 | - name: Verify there is one mariadb pod 19 | assert: 20 | that: '{{ (mariadb_pods.resources | length) == 1 }}' 21 | 22 | - name: Verify database functionality 23 | hosts: k8s 24 | 25 | vars: 26 | mariadb_db_host: example-mariadb-0.example-mariadb.example-mariadb.svc.cluster.local 27 | mariadb_db_name: db 28 | mariadb_db_username: db_user 29 | mariadb_db_password: CHANGEME 30 | 31 | tasks: 32 | - name: Wait 60s for database to initialize. 33 | pause: 34 | seconds: 60 35 | 36 | - name: Create a table in the test database. 37 | command: > 38 | kubectl -n example-mariadb run -it --rm mysql-client --image=arey/mysql-client --restart=Never -- 39 | -h {{ mariadb_db_host }} -u {{ mariadb_db_username }} -p{{ mariadb_db_password }} -D {{ mariadb_db_name }} 40 | -e "CREATE TABLE IF NOT EXISTS test (title VARCHAR(255));" 41 | changed_when: true 42 | 43 | - name: Verify the table exists. 44 | command: > 45 | kubectl -n example-mariadb run -it --rm mysql-client --image=arey/mysql-client --restart=Never -- 46 | -h {{ mariadb_db_host }} -u {{ mariadb_db_username }} -p{{ mariadb_db_password }} -D {{ mariadb_db_name }} 47 | -e "SELECT 1 FROM test LIMIT 1;" 48 | changed_when: false 49 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Close inactive issues 3 | 'on': 4 | schedule: 5 | - cron: "55 17 * * 4" # semi-random time 6 | 7 | jobs: 8 | close-issues: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | steps: 14 | - uses: actions/stale@v8 15 | with: 16 | days-before-stale: 120 17 | days-before-close: 60 18 | exempt-issue-labels: bug,pinned,security,planned 19 | exempt-pr-labels: bug,pinned,security,planned 20 | stale-issue-label: "stale" 21 | stale-pr-label: "stale" 22 | stale-issue-message: | 23 | This issue has been marked 'stale' due to lack of recent activity. If there is no further activity, the issue will be closed in another 30 days. Thank you for your contribution! 24 | 25 | Please read [this blog post](https://www.jeffgeerling.com/blog/2020/enabling-stale-issue-bot-on-my-github-repositories) to see the reasons why I mark issues as stale. 26 | close-issue-message: | 27 | This issue has been closed due to inactivity. If you feel this is in error, please reopen the issue or file a new issue with the relevant details. 28 | stale-pr-message: | 29 | This pr has been marked 'stale' due to lack of recent activity. If there is no further activity, the issue will be closed in another 30 days. Thank you for your contribution! 30 | 31 | Please read [this blog post](https://www.jeffgeerling.com/blog/2020/enabling-stale-issue-bot-on-my-github-repositories) to see the reasons why I mark issues as stale. 32 | close-pr-message: | 33 | This pr has been closed due to inactivity. If you feel this is in error, please reopen the issue or file a new issue with the relevant details. 34 | repo-token: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /deploy/mariadb-operator.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | creationTimestamp: null 6 | name: mariadb-operator 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - pods 12 | - services 13 | - services/finalizers 14 | - endpoints 15 | - persistentvolumeclaims 16 | - events 17 | - configmaps 18 | - secrets 19 | verbs: 20 | - '*' 21 | - apiGroups: 22 | - apps 23 | - extensions 24 | resources: 25 | - deployments 26 | - daemonsets 27 | - replicasets 28 | - statefulsets 29 | verbs: 30 | - '*' 31 | - apiGroups: 32 | - monitoring.coreos.com 33 | resources: 34 | - servicemonitors 35 | verbs: 36 | - get 37 | - create 38 | - apiGroups: 39 | - apps 40 | resourceNames: 41 | - mariadb-operator 42 | resources: 43 | - deployments/finalizers 44 | verbs: 45 | - update 46 | - apiGroups: 47 | - "" 48 | resources: 49 | - pods 50 | verbs: 51 | - get 52 | - apiGroups: 53 | - apps 54 | resources: 55 | - replicasets 56 | verbs: 57 | - get 58 | - apiGroups: 59 | - mariadb.mariadb.com 60 | resources: 61 | - '*' 62 | verbs: 63 | - '*' 64 | 65 | --- 66 | kind: ClusterRoleBinding 67 | apiVersion: rbac.authorization.k8s.io/v1 68 | metadata: 69 | name: mariadb-operator 70 | subjects: 71 | - kind: ServiceAccount 72 | name: mariadb-operator 73 | namespace: default 74 | roleRef: 75 | kind: ClusterRole 76 | name: mariadb-operator 77 | apiGroup: rbac.authorization.k8s.io 78 | 79 | --- 80 | apiVersion: v1 81 | kind: ServiceAccount 82 | metadata: 83 | name: mariadb-operator 84 | namespace: default 85 | 86 | --- 87 | apiVersion: apps/v1 88 | kind: Deployment 89 | metadata: 90 | name: mariadb-operator 91 | spec: 92 | replicas: 1 93 | selector: 94 | matchLabels: 95 | name: mariadb-operator 96 | template: 97 | metadata: 98 | labels: 99 | name: mariadb-operator 100 | spec: 101 | serviceAccountName: mariadb-operator 102 | containers: 103 | - name: ansible 104 | command: 105 | - /usr/local/bin/ao-logs 106 | - /tmp/ansible-operator/runner 107 | - stdout 108 | image: "geerlingguy/mariadb-operator:0.0.3" 109 | imagePullPolicy: "Always" 110 | volumeMounts: 111 | - mountPath: /tmp/ansible-operator/runner 112 | name: runner 113 | readOnly: true 114 | - name: operator 115 | image: "geerlingguy/mariadb-operator:0.0.3" 116 | imagePullPolicy: "Always" 117 | volumeMounts: 118 | - mountPath: /tmp/ansible-operator/runner 119 | name: runner 120 | env: 121 | # Watch all namespaces (cluster-scoped). 122 | - name: WATCH_NAMESPACE 123 | value: "" 124 | - name: POD_NAME 125 | valueFrom: 126 | fieldRef: 127 | fieldPath: metadata.name 128 | - name: OPERATOR_NAME 129 | value: "mariadb-operator" 130 | volumes: 131 | - name: runner 132 | emptyDir: {} 133 | 134 | --- 135 | apiVersion: apiextensions.k8s.io/v1beta1 136 | kind: CustomResourceDefinition 137 | metadata: 138 | name: mariadbs.mariadb.mariadb.com 139 | spec: 140 | group: mariadb.mariadb.com 141 | names: 142 | kind: MariaDB 143 | listKind: MariaDBList 144 | plural: mariadbs 145 | singular: mariadb 146 | scope: Namespaced 147 | subresources: 148 | status: {} 149 | version: v1alpha1 150 | versions: 151 | - name: v1alpha1 152 | served: true 153 | storage: true 154 | -------------------------------------------------------------------------------- /molecule/test-local/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Build Operator in Kubernetes docker container 3 | hosts: k8s 4 | 5 | vars: 6 | image_name: mariadb.mariadb.com/mariadb-operator:testing 7 | 8 | tasks: 9 | # using command so we don't need to install any dependencies 10 | - name: Get existing image hash 11 | command: docker images -q {{ image_name }} 12 | register: prev_hash 13 | changed_when: false 14 | 15 | - name: Build Operator Image 16 | command: docker build -f /build/build/Dockerfile -t {{ image_name }} /build 17 | register: build_cmd 18 | changed_when: not prev_hash.stdout or (prev_hash.stdout and prev_hash.stdout not in ''.join(build_cmd.stdout_lines[-2:])) 19 | 20 | - name: Converge 21 | hosts: localhost 22 | connection: local 23 | 24 | vars: 25 | ansible_python_interpreter: '{{ ansible_playbook_python }}' 26 | deploy_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/deploy" 27 | pull_policy: Never 28 | operator_image: mariadb.mariadb.com/mariadb-operator:testing 29 | custom_resource: "{{ lookup('file', '/'.join([deploy_dir, 'crds/mariadb_v1alpha1_mariadb_cr.yaml'])) | from_yaml }}" 30 | 31 | tasks: 32 | - block: 33 | - name: Delete the Operator Deployment 34 | k8s: 35 | state: absent 36 | namespace: '{{ operator_namespace }}' 37 | definition: "{{ lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) }}" 38 | register: delete_deployment 39 | when: hostvars[groups.k8s.0].build_cmd.changed 40 | 41 | - name: Wait 30s for Operator Deployment to terminate 42 | k8s_info: 43 | api_version: '{{ definition.apiVersion }}' 44 | kind: '{{ definition.kind }}' 45 | namespace: '{{ operator_namespace }}' 46 | name: '{{ definition.metadata.name }}' 47 | vars: 48 | definition: "{{ lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) | from_yaml }}" 49 | register: deployment 50 | until: not deployment.resources 51 | delay: 3 52 | retries: 10 53 | when: delete_deployment.changed 54 | 55 | - name: Create the Operator Deployment 56 | k8s: 57 | namespace: '{{ operator_namespace }}' 58 | definition: "{{ lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) }}" 59 | 60 | - name: Ensure the MariaDB custom_resource namespace exists 61 | k8s: 62 | state: present 63 | name: '{{ custom_resource.metadata.namespace }}' 64 | kind: Namespace 65 | api_version: v1 66 | 67 | - name: Create the mariadb.mariadb.com/v1alpha1.MariaDB 68 | k8s: 69 | state: present 70 | namespace: '{{ custom_resource.metadata.namespace }}' 71 | definition: '{{ custom_resource }}' 72 | 73 | - name: Wait 5m for reconciliation to run 74 | k8s_info: 75 | api_version: '{{ custom_resource.apiVersion }}' 76 | kind: '{{ custom_resource.kind }}' 77 | namespace: '{{ custom_resource.metadata.namespace }}' 78 | name: '{{ custom_resource.metadata.name }}' 79 | register: cr 80 | until: 81 | - "'Successful' in (cr | json_query('resources[].status.conditions[].reason'))" 82 | delay: 6 83 | retries: 50 84 | 85 | rescue: 86 | 87 | - name: debug cr 88 | ignore_errors: yes 89 | failed_when: false 90 | debug: 91 | var: debug_cr 92 | vars: 93 | debug_cr: '{{ lookup("k8s", 94 | kind=custom_resource.kind, 95 | api_version=custom_resource.apiVersion, 96 | namespace=custom_resource.metadata.namespace, 97 | resource_name=custom_resource.metadata.name 98 | )}}' 99 | 100 | - name: debug mariadb deployment 101 | ignore_errors: yes 102 | failed_when: false 103 | debug: 104 | var: deploy 105 | vars: 106 | deploy: '{{ lookup("k8s", 107 | kind="Deployment", 108 | api_version="apps/v1", 109 | namespace=custom_resource.metadata.namespace, 110 | label_selector="app=mariadb" 111 | )}}' 112 | 113 | - name: get operator logs 114 | ignore_errors: yes 115 | failed_when: false 116 | command: kubectl logs deployment/{{ definition.metadata.name }} -n {{ operator_namespace }} -c operator 117 | environment: 118 | KUBECONFIG: '{{ lookup("env", "KUBECONFIG") }}' 119 | vars: 120 | definition: "{{ lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) | from_yaml }}" 121 | register: log 122 | 123 | - name: print debug output 124 | debug: var=log.stdout_lines 125 | 126 | - name: fail if converge didn't succeed 127 | fail: 128 | msg: "Failed on action: converge" 129 | 130 | - import_playbook: '{{ playbook_dir }}/../default/asserts.yml' 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MariaDB Operator for Kubernetes 2 | 3 | [![Build Status](https://travis-ci.com/geerlingguy/mariadb-operator.svg?branch=master)](https://travis-ci.com/geerlingguy/mariadb-operator) 4 | 5 | This is a MariaDB Operator, which makes management of MariaDB instances (or clusters) running inside Kubernetes clusters easy. It was built with the [Operator SDK](https://github.com/operator-framework/operator-sdk) using [Ansible](https://www.ansible.com/blog/ansible-operator). 6 | 7 | ## Usage 8 | 9 | This Kubernetes Operator is meant to be deployed in your Kubernetes cluster(s) and can manage one or more MariaDB database instances or clusters in any namespace. 10 | 11 | First you need to deploy MariaDB Operator into your cluster: 12 | 13 | kubectl apply -f https://raw.githubusercontent.com/geerlingguy/mariadb-operator/master/deploy/mariadb-operator.yaml 14 | 15 | Then you can create instances of MariaDB in any namespace, for example: 16 | 17 | 1. Create a file named `my-database.yml` with the following contents: 18 | 19 | ``` 20 | --- 21 | apiVersion: mariadb.mariadb.com/v1alpha1 22 | kind: MariaDB 23 | metadata: 24 | name: example-mariadb 25 | namespace: example-mariadb 26 | spec: 27 | masters: 1 28 | replicas: 0 29 | mariadb_password: CHANGEME 30 | mariadb_image: mariadb:10.4 31 | mariadb_pvc_storage_request: 1Gi 32 | ``` 33 | 34 | 2. Use `kubectl` to create the MariaDB instance in your cluster: 35 | 36 | ``` 37 | kubectl apply -f my-database.yml 38 | ``` 39 | 40 | > You can also deploy `MariaDB` instances into other namespaces by changing `metadata.namespace`, or deploy multiple `MariaDB` instances into the same namespace by changing `metadata.name`. 41 | 42 | ### Connecting to the running database 43 | 44 | Once the database instance has been deployed and initialized (this can take up to a few minutes), you can connect to it using the MySQL CLI from within the same namespace as your database instance, for example: 45 | 46 | kubectl -n example-mariadb run -it --rm mysql-client --image=arey/mysql-client --restart=Never -- -h example-mariadb-0.example-mariadb.example-mariadb.svc.cluster.local -u db_user -pCHANGEME -D db 47 | 48 | Other applications can connect to the database instance from within the same namespace using the following connection parameters: 49 | 50 | - Host: `example-mariadb-0.example-mariadb.example-mariadb.svc.cluster.local` 51 | - Username: `db_user` 52 | - Password: `CHANGEME` 53 | - Database: `db` 54 | 55 | ### Exposing an instance to the outside world 56 | 57 | You can also expose a database instance to the outside world by adding a `Service` to connect to port `3360`: 58 | 59 | --- 60 | apiVersion: v1 61 | kind: Service 62 | metadata: 63 | name: mariadb-master 64 | namespace: example-mariadb 65 | spec: 66 | type: NodePort 67 | selector: 68 | statefulset.kubernetes.io/example-mariadb: example-mariadb-0 69 | ports: 70 | - protocol: TCP 71 | port: 3360 72 | targetPort: 3360 73 | 74 | Once you create that service in the same namespace, you can connect to MariaDB via the NodePort assigned to the service. 75 | 76 | ## Development 77 | 78 | ### Release Process 79 | 80 | There are a few moving parts to this project: 81 | 82 | 1. The Docker image which powers MariaDB Operator. 83 | 2. The `mariadb-operator.yaml` Kubernetes manifest file which initially deploys the Operator into a cluster. 84 | 85 | Each of these must be appropriately built in preparation for a new tag: 86 | 87 | #### Build a new release of the Operator for Docker Hub 88 | 89 | Run the following command inside this directory: 90 | 91 | operator-sdk build geerlingguy/mariadb-operator:0.0.3 92 | 93 | Then push the generated image to Docker Hub: 94 | 95 | docker push geerlingguy/mariadb-operator:0.0.3 96 | 97 | #### Build a new version of the `mariadb-operator.yaml` file 98 | 99 | Verify the `build/chain-operator-files.yml` playbook has the most recent version/tag of the Docker image, then run the playbook in the `build/` directory: 100 | 101 | ansible-playbook chain-operator-files.yml 102 | 103 | After it is built, test it on a local cluster: 104 | 105 | minikube start 106 | minikube addons enable ingress 107 | kubectl apply -f deploy/mariadb-operator.yaml 108 | kubectl create namespace example-mariadb 109 | kubectl apply -f deploy/crds/mariadb_v1alpha1_mariadb_cr.yaml 110 | 111 | minikube delete 112 | 113 | If everything is deployed correctly, commit the updated version and push it up to GitHub, tagging a new repository release with the same tag as the Docker image. 114 | 115 | ### Testing 116 | 117 | #### Local tests with Molecule and KIND 118 | 119 | Ensure you have the testing dependencies installed (in addition to Docker): 120 | 121 | pip install docker molecule openshift jmespath 122 | 123 | Run the local molecule test scenario: 124 | 125 | molecule test -s test-local 126 | 127 | #### Local tests with minikube 128 | 129 | TODO. 130 | 131 | ## Authors 132 | 133 | This operator is maintained by [Jeff Geerling](https://www.jeffgeerling.com), author of [Ansible for DevOps](https://www.ansiblefordevops.com) and [Ansible for Kubernetes](https://www.ansibleforkubernetes.com). 134 | --------------------------------------------------------------------------------