diff --git a/charm.py b/charm.py new file mode 100644 index 0000000..3fa51ec --- /dev/null +++ b/charm.py @@ -0,0 +1,120 @@ +from charmhelpers.core import host + +from juju import charm, model, unit, endpoint +from juju.errors import ConfigError + +import mariadb + + +@model.configure +def configure_workload(): + """ + This is called when the charm needs to be configured in some manner. + This will run before any workload has been started. + """ + root_password = model.config.get('root-password') + + if not root_password: + root_password = host.pwgen(32) + + model.config.set('root-password', root_password) + + +@model.prepare_workload +def prepare_workload(workload): + """ + This handler is called when the charm has been configured and is ready + to deploy workloads. + + This should potentially work for both the machine and k8s world? + Assuming that the VM is running Docker/LXD. + """ + root_password = model.config.get('root-password') + + if not root_password: + raise ConfigError("Missing db root password (charm not configured?)") + + workload.set_oci_image(charm.resources['mariadb']) + workload.open_port('db', containerPort=3306, protocol='TCP') + workload.env.set('MYSQL_ROOT_PASSWORD', root_password) + + # TODO: who/what is dealing with storage? + + +@endpoint.join('database') +def handle_join(db, request): + """ + Called when a request for a new database endpoint is made. + + If a database does not exists for this request, one will be created. + + @param db: The `database` endpoint. + @param request: The request being made to connect to this db. + """ + creds = model.data.get('credentials', {}) + + context = creds.get(request.application_name, None) + + if not context: + context = creds[request.application_name] = { + 'username': host.pwgen(20), + 'password': host.pwgen(20), + 'database': request.database_name or request.application_name + } + + username = context['username'] + password = context['password'] + db_name = context['database'] + + with get_admin_connection() as connection: + with mariadb.cursor(connection) as cursor: + mariadb.create_database(cursor, db_name) + mariadb.ensure_grant( + cursor, + db_name, + username, + password, + # TODO: think about this from a ephemeral pod perspective + request.address + ) + + request.set_state(context) + request.ack() + + +@endpoint.leave('database') +def handle_leave(db_endpoint, request): + """ + Called when a request to leave/drop an endpoint is made. + + @param db: The `database` endpoint. + @param request: The request being made to abandon this db. + """ + with get_admin_connection() as connection: + with mariadb.cursor(connection) as cursor: + mariadb.cleanup_grant( + cursor, + request.username, + request.address, + ) + + request.ack() + + +def get_admin_connection(): + """ + Returns a `mysql.connector.Connection` object that has 'root' privileges + over the underlying mariadb. + + Use with care. + """ + root_password = model.config.get('root-password') + + if not root_password: + raise ConfigError("Missing root password (charm not configured yet?)") + + return mariadb.get_connection( + host=unit.host_name, + password=root_password, + user='root', + ) diff --git a/metadata.yaml b/charm.yaml similarity index 77% rename from metadata.yaml rename to charm.yaml index fb0dd40..56568a4 100644 --- a/metadata.yaml +++ b/charm.yaml @@ -20,7 +20,7 @@ provides: series: - kubernetes resources: - mariadb-image: + mariadb: type: oci-image description: "Image used for mariadb workload." auto-fetch: true @@ -29,3 +29,14 @@ storage: database: type: filesystem location: /var/lib/mysql +modules: + - caas-base + - docker-resource +interfaces: + - database +config: + root-password: + description: | + Password to use for the database root user. If not specified, + one will be generated automatically. + type: string \ No newline at end of file diff --git a/config.yaml b/config.yaml deleted file mode 100644 index 4e849e0..0000000 --- a/config.yaml +++ /dev/null @@ -1,7 +0,0 @@ -options: - root-password: - description: | - Password to use for the database root user. If not specified, - one will be generated automatically. - type: string - default: '' diff --git a/icon.svg b/icon.svg deleted file mode 100644 index 286fa17..0000000 --- a/icon.svg +++ /dev/null @@ -1,335 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - diff --git a/layer.yaml b/layer.yaml deleted file mode 100644 index ebe7b50..0000000 --- a/layer.yaml +++ /dev/null @@ -1,6 +0,0 @@ -includes: - - "layer:caas-base" - - "layer:docker-resource" - - 'interface:database' - -repo: https://github.com/wallyworld/caas.git diff --git a/lib/charms/layer/mariadb_k8s.py b/lib/charms/layer/mariadb_k8s.py deleted file mode 100644 index 4073c3a..0000000 --- a/lib/charms/layer/mariadb_k8s.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Helpers for the mariadb-k8s charm. -""" - -import mysql.connector - - -def create_database(cursor, db_name): - cursor.execute('CREATE DATABASE IF NOT EXISTS %s', db_name) - - -def grant_exists(cursor, db_name, username, address): - try: - cursor.execute("SHOW GRANTS for %s@%s", username, address) - grants = [i[0] for i in cursor.fetchall()] - except mysql.connector.Error: - return False - else: - return "GRANT ALL PRIVILEGES ON `{}`".format(db_name) in grants - - -def create_grant(cursor, db_name, username, password, address): - cursor.execute("GRANT ALL PRIVILEGES ON %s.* TO %s@%s IDENTIFIED BY %s", - db_name, username, address, password) - - -def cleanup_grant(cursor, username, address): - cursor.execute("DROP FROM mysql.user WHERE user=%s AND HOST=%s", - username, address) diff --git a/mariadb.py b/mariadb.py new file mode 100644 index 0000000..a91831d --- /dev/null +++ b/mariadb.py @@ -0,0 +1,83 @@ +""" +Helpers for the mariadb-k8s charm. +""" + +from contextlib import contextmanager +import mysql.connector + + +def get_connection(host, password, user): + """ + A helper function that returns a connection object. + """ + return mysql.connector.connect( + user=user, + password=password, + host=host, + ) + + +@contextmanager +def cursor(connection): + cursor = connection.cursor() + + try: + yield cursor + except Exception: + connection.rollback() + else: + connection.commit() + finally: + cursor.close() + + +def create_database(cursor, db_name): + cursor.execute( + "CREATE DATABASE IF NOT EXISTS %s", + (db_name,), + ) + + +def grant_exists(cursor, db_name, username, address): + try: + cursor.execute("SHOW GRANTS for %s@%s", (username, address)) + grants = [i[0] for i in cursor.fetchall()] + except mysql.connector.Error: + return False + else: + # TODO: ??? + return "GRANT ALL PRIVILEGES ON `{}`".format(db_name) in grants + + +def create_grant(cursor, db_name, username, password, address): + cursor.execute( + "GRANT ALL PRIVILEGES ON %s.* TO %s@%s IDENTIFIED BY %s", + (db_name, username, address, password), + ) + + +def cleanup_grant(cursor, db_name, username, address): + cursor.execute( + "REVOKE ALL ON %s FROM %s@%s", + (db_name, username, address), + ) + + +def ensure_grant(connection, db_name, username, password, address=None): + exists = grant_exists( + cursor, + db_name, + username, + address, + ) + + if exists: + return + + create_grant( + cursor, + db_name, + username, + password, + address, + ) diff --git a/reactive/mariadb_k8s.py b/reactive/mariadb_k8s.py deleted file mode 100644 index a9a23fc..0000000 --- a/reactive/mariadb_k8s.py +++ /dev/null @@ -1,129 +0,0 @@ -from charmhelpers.core import hookenv, host, unitdata -from charms.reactive import set_flag, clear_flag -from charms.reactive import when_all, when_any, when_not -from charms.reactive import endpoint_from_name - -from charms import layer - -import mysql.connector - - -@when_all('resource.my-image.available') -@when_not('charm.mariadb.started') -def configure_workload(): - layer.status.maintenance('starting workload') - - config = hookenv.config() - - root_password = (unitdata.kv().get('charm.mariadb.root-password') or - config['root-password'] or - host.pwgen(32)) - - # we need this later to connect to the DB, but - # this is an insecure way to store it; unfortunately, - # we don't have a secure way - unitdata.kv().set('charm.mariadb.root-password', root_password) - - # fetch the info (registry path, auth info) about the image - image_info = layer.docker_resource.get_info('my-image') - - # this can also be handed raw YAML, so some charm authors - # choose to use templated YAML in a file instead - layer.caas_base.pod_spec_set({ - 'containers': [ - { - 'name': 'mariadb', - 'imageDetails': { - 'imagePath': image_info.registry_path, - 'username': image_info.username, - 'password': image_info.password, - }, - 'command': [], - 'ports': [ - { - 'name': 'db', - 'containerPort': 3306, - }, - ], - 'config': { - # juju doesn't support secrets yet - 'MYSQL_ROOT_PASSWORD': root_password, - }, - }, - ], - }) - - layer.status.active('ready') - set_flag('charm.mariadb.started') - - -@when_any('resource.mariadb.changed') -def update_image(): - # handle a new image resource becoming available - configure_workload() - - -@when_all('charm.started', - 'endpoint.database.new-requests') -def handle_requests(): - db = endpoint_from_name('database') - users = unitdata.kv().get('charm.users', {}) - root_password = unitdata.kv().get('charm.root-password') - connection = mysql.connector.connect(user='root', - password=root_password, - host='mariadb') - cursor = None - try: - cursor = connection.cursor() - for request in db.new_requests: - # determine db_name, username, and password for request, - # generating each if needed - if request.application_name not in users: - users[request.application_name] = (host.pwgen(20), - host.pwgen(20)) - username, password = users[request.application_name] - db_name = request.database_name or request.application_name - - # create the database and grant the user access - layer.mariadb_k8s.create_database(cursor, db_name) - if not layer.mariadb_k8s.grant_exists(cursor, - db_name, - username, - request.address): - layer.mariadb_k8s.create_grant(cursor, - db_name, - username, - password, - request.address) - - # fulfill this request - request.provide_database(db_name, username, password) - cursor.commit() - finally: - if cursor: - cursor.close() - connection.close() - - -@when_all('charm.mariadb.started', - 'endpoint.database.new-departs') -def handle_departs(): - db = endpoint_from_name('database') - root_password = unitdata.kv().get('charm.mariadb.root-password') - connection = mysql.connector.connect(user='root', - password=root_password, - host='mariadb') - cursor = None - try: - cursor = connection.cursor() - for depart in db.new_departs: - if depart.username: - layer.mariadb_k8s.cleanup_grant(cursor, - depart.username, - depart.address) - depart.ack() # acknowledge this departure - cursor.commit() - finally: - if cursor: - cursor.close() - connection.close() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..721e4c9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +mysql-connector-python>=8.0,<8.1 diff --git a/wheelhouse.txt b/wheelhouse.txt deleted file mode 100644 index 8d471fc..0000000 --- a/wheelhouse.txt +++ /dev/null @@ -1 +0,0 @@ -mysql-connector-python>=8.0,<9.0