class TestCharm(unittest.TestCase): def setUp(self): self.harness = Harness(MongoDBCharm) self.addCleanup(self.harness.cleanup) mongo_resource = { "registrypath": "mongodb:4.4.1", "username": "******", "password": "******" } self.harness.add_oci_resource("mongodb-image", mongo_resource) self.harness.begin() def test_replica_set_name_can_be_changed(self): self.harness.set_leader(True) # check default replica set name self.harness.charm.on.config_changed.emit() pod_spec = self.harness.get_pod_spec() self.assertEqual(replica_set_name(pod_spec), "rs0") # check replica set name can be changed self.harness.update_config({"replica_set_name": "new_name"}) pod_spec = self.harness.get_pod_spec() self.assertEqual(replica_set_name(pod_spec), "new_name") @patch("mongo.Mongo.reconfigure_replica_set") def test_replica_set_is_reconfigured_when_peer_joins(self, mock_reconf): self.harness.set_leader(True) rel_id = self.harness.add_relation('mongodb', 'mongodb') self.harness.add_relation_unit(rel_id, 'mongodb/1') self.harness.update_relation_data(rel_id, 'mongodb/1', {'private-address': '10.0.0.1'}) self.assertEqual(self.harness.charm.num_peers, 2) peers = ['mongodb-0.mongodb-endpoints', 'mongodb-1.mongodb-endpoints'] mock_reconf.assert_called_once_with(peers)
class KafkaCharmTest(unittest.TestCase): def setUp(self) -> None: self.harness = Harness(KafkaOperator) self.addCleanup(self.harness.cleanup) self.harness.begin() self.harness.add_oci_resource("kafka-image") self.harness.update_config(BASE_CONFIG) def test_kafka_layer(self): expected = { "summary": "kafka layer", "description": "kafka layer", "services": { "kafka-setup": { "override": "replace", "summary": "kafka setup step - initialize & format storage", "command": "/opt/bitnami/kafka/bin/kafka-storage.sh format -t test-id -c /opt/bitnami/kafka/config/kraft/server.properties", "startup": "enabled", }, "kafka": { "override": "replace", "summary": "kafka service", "command": "/opt/bitnami/kafka/bin/kafka-server-start.sh /opt/bitnami/kafka/config/kraft/server.properties", "startup": "enabled", "requires": ["kafka-setup"], }, }, } assert self.harness.charm._kafka_layer() == expected
class TestCharm(unittest.TestCase): def setUp(self): self.harness = Harness(MongoDBCharm) self.addCleanup(self.harness.cleanup) mongo_resource = { "registrypath": "mongodb:4.4.1", "username": "******", "password": "******" } self.harness.add_oci_resource("mongodb-image", mongo_resource) self.harness.begin() def test_replica_set_name_can_be_changed(self): self.harness.set_leader(True) # check default replica set name self.harness.charm.on.config_changed.emit() pod_spec = self.harness.get_pod_spec() self.assertEqual(replica_set_name(pod_spec), "rs0") # check replica set name can be changed self.harness.update_config({"replica_set_name": "new_name"}) pod_spec = self.harness.get_pod_spec() self.assertEqual(replica_set_name(pod_spec), "new_name") @patch("mongoserver.MongoDB.reconfigure_replica_set") def test_replica_set_is_reconfigured_when_peer_joins(self, mock_reconf): self.harness.set_leader(True) rel_id = self.harness.add_relation('mongodb', 'mongodb') self.harness.add_relation_unit(rel_id, 'mongodb/1') self.harness.update_relation_data(rel_id, 'mongodb/1', {'private-address': '10.0.0.1'}) peers = ['mongodb-0.mongodb-endpoints', 'mongodb-1.mongodb-endpoints'] mock_reconf.assert_called_once_with(peers) def test_uri_data_is_generated_correctly(self): self.harness.set_leader(True) standalone_uri = self.harness.charm.mongo.standalone_uri replica_set_uri = self.harness.charm.mongo.replica_set_uri self.assertEqual(standalone_uri, 'mongodb://mongodb:27017/') self.assertEqual(replica_set_uri, 'mongodb://mongodb-0.mongodb-endpoints:27017/') def test_database_relation_data_is_set_correctly(self): self.harness.set_leader(True) rel_id = self.harness.add_relation('database', 'client') self.harness.add_relation_unit(rel_id, 'client/1') rel = self.harness.framework.model.get_relation('database', rel_id) unit = self.harness.framework.model.get_unit('client/1') self.harness.charm.on['database'].relation_changed.emit(rel, unit) got = self.harness.get_relation_data(rel_id, self.harness.framework.model.unit.name) expected = { 'replicated': 'False', 'replica_set_name': 'rs0', 'standalone_uri': 'mongodb://mongodb:27017/', 'replica_set_uri': 'mongodb://mongodb-0.mongodb-endpoints:27017/' } self.assertDictEqual(got, expected)
def test_add_oci_resource_no_image(self): harness = Harness(CharmBase, meta=''' name: test-app resources: image: type: file description: "Image to deploy." ''') with self.assertRaises(RuntimeError): harness.add_oci_resource("image") with self.assertRaises(RuntimeError): harness.add_oci_resource("missing-resource") self.assertEqual(len(harness._backend._resources_map), 0)
def test_add_oci_resource_custom(self): harness = Harness(CharmBase, meta=''' name: test-app resources: image: type: oci-image description: "Image to deploy." ''') custom = { "registrypath": "custompath", "username": "******", "password": "******", } harness.add_oci_resource('image', custom) resource = harness._resource_dir / "image" / "contents.yaml" with resource.open('r') as resource_file: contents = yaml.safe_load(resource_file.read()) self.assertEqual(contents['registrypath'], 'custompath') self.assertEqual(contents['username'], 'custom_username') self.assertEqual(contents['password'], 'custom_password') self.assertEqual(len(harness._backend._resources_map), 1)
class TestCharm(unittest.TestCase): def setUp(self): self.harness = Harness(RedisCharm) self.addCleanup(self.harness.cleanup) redis_resource = { "registrypath": "ubuntu/redis" } self.harness.add_oci_resource("redis-image", redis_resource) self.harness.begin() def test_on_start_when_unit_is_not_leader(self): # Given self.harness.set_leader(False) # When self.harness.charm.on.start.emit() # Then self.assertEqual( self.harness.charm.unit.status, ActiveStatus() ) @mock.patch.object(RedisClient, 'is_ready') def test_on_start_when_redis_is_not_ready(self, is_ready): # Given self.harness.set_leader(True) is_ready.return_value = False # When self.harness.charm.on.start.emit() # Then is_ready.assert_called_once_with() self.assertEqual( self.harness.charm.unit.status, WaitingStatus("Waiting for Redis ...") ) @mock.patch.object(RedisClient, 'is_ready') def test_on_start_when_redis_is_ready(self, is_ready): # Given self.harness.set_leader(True) is_ready.return_value = True # When self.harness.charm.on.start.emit() # Then is_ready.assert_called_once_with() self.assertEqual( self.harness.charm.unit.status, ActiveStatus() ) def test_on_stop(self): # When self.harness.charm.on.stop.emit() # Then self.assertEqual( self.harness.charm.unit.status, MaintenanceStatus('Pod is terminating.') ) def test_on_config_changed_when_unit_is_not_leader(self): # Given self.harness.set_leader(False) # When self.harness.charm.on.config_changed.emit() # Then self.assertEqual( self.harness.charm.unit.status, ActiveStatus() ) @mock.patch.object(RedisClient, 'is_ready') def test_on_config_changed_when_unit_is_leader_and_redis_is_ready(self, is_ready): # Given self.harness.set_leader(True) is_ready.return_value = True # When self.harness.charm.on.config_changed.emit() # Then self.assertEqual( self.harness.charm.unit.status, ActiveStatus() ) @mock.patch.object(OCIImageResource, 'fetch') def test_on_config_changed_when_unit_is_leader_but_image_fetch_breaks(self, fetch): # Given self.harness.set_leader(True) fetch.side_effect = OCIImageResourceError("redis-image") # When self.harness.charm.on.config_changed.emit() # Then fetch.assert_called_once_with() self.assertEqual( self.harness.charm.unit.status, BlockedStatus("Error fetching image information.") ) def test_on_update_status_when_unit_is_not_leader(self): # Given self.harness.set_leader(False) # When self.harness.charm.on.update_status.emit() # Then self.assertEqual( self.harness.charm.unit.status, ActiveStatus() ) @mock.patch.object(RedisClient, 'is_ready') def test_on_update_status_when_redis_is_not_ready(self, is_ready): # Given self.harness.set_leader(True) is_ready.return_value = False # When self.harness.charm.on.update_status.emit() # Then is_ready.assert_called_once_with() self.assertEqual( self.harness.charm.unit.status, WaitingStatus("Waiting for Redis ...") ) @mock.patch.object(RedisClient, 'is_ready') def test_on_update_status_when_redis_is_ready(self, is_ready): # Given self.harness.set_leader(True) is_ready.return_value = True # When self.harness.charm.on.update_status.emit() # Then is_ready.assert_called_once_with() self.assertEqual( self.harness.charm.unit.status, ActiveStatus() ) @mock.patch.object(RedisProvides, '_bind_address') def test_on_relation_changed_status_when_unit_is_leader(self, bind_address): # Given self.harness.set_leader(True) bind_address.return_value = '10.2.1.5' rel_id = self.harness.add_relation('redis', 'wordpress') self.harness.add_relation_unit(rel_id, 'wordpress/0') # When self.harness.update_relation_data(rel_id, 'wordpress/0', {}) rel_data = self.harness.get_relation_data( rel_id, self.harness.charm.unit.name ) # Then self.assertEqual(rel_data.get('hostname'), '10.2.1.5') self.assertEqual(rel_data.get('port'), '6379')
class TestKuberneteseInfluxdbCharm(unittest.TestCase): def setUp(self) -> None: """Setup the harness object.""" self.harness = Harness(KubernetesInfluxdbCharm) self.harness.begin() self.harness.add_oci_resource("influxdb2-image") def tearDown(self): """Cleanup the harness.""" self.harness.cleanup() # # Hooks # def test__on_config_changed(self) -> None: self.harness.update_config({"port": "9999"}) self.assertEqual(self.harness.charm.unit.status, ActiveStatus("Pod is ready")) def test__on_config_changed_pebble_api_connection_error_1(self) -> None: self.harness.charm.unit.get_container = mock.MagicMock() self.harness.charm.unit.get_container.return_value.get_plan = mock.MagicMock( side_effect=ConnectionError("connection timeout")) with self.assertLogs(level="DEBUG") as logger: self.harness.update_config({"port": "9999"}) self.assertIn( "DEBUG:charm:The Pebble API is not ready yet. Error message: connection timeout", logger.output, ) self.assertNotIn( "DEBUG:charm:Pebble plan has already been loaded. No need to update the config.", logger.output, ) def test__on_config_changed_pebble_api_connection_error_2(self) -> None: self.harness.charm.unit.get_container = mock.MagicMock() self.harness.charm.unit.get_container.return_value.get_plan.return_value.to_dict = ( mock.MagicMock(return_value={})) self.harness.charm.unit.get_container.return_value.add_layer = mock.MagicMock( side_effect=ConnectionError("connection timeout")) with self.assertLogs(level="DEBUG") as logger: self.harness.update_config({"port": "9999"}) self.assertIn( "DEBUG:charm:The Pebble API is not ready yet. Error message: connection timeout", logger.output, ) self.assertNotIn( "DEBUG:charm:Pebble plan has already been loaded. No need to update the config.", logger.output, ) def test__on_config_changed_same_plan(self) -> None: self.harness.charm.unit.get_container = mock.MagicMock() self.harness.charm.unit.get_container.return_value.get_plan.return_value.to_dict = ( mock.MagicMock(return_value=self.harness.charm._influxdb2_layer())) with self.assertLogs(level="DEBUG") as logger: self.harness.update_config({"port": "9999"}) self.assertIn( "DEBUG:charm:Pebble plan has already been loaded. No need to update the config.", logger.output, ) self.assertNotIn( "DEBUG:charm:The Pebble API is not ready yet. Error message: connection timeout", logger.output, ) # # Test Relations # def test__grafana_source_data(self, expected_reldata: Optional[Dict] = None ) -> None: # Initialize values interface: str = "grafana-source" rel_app: str = "grafana" rel_unit: str = "grafana/0" rel_data: Dict[str, str] = {} expected: Dict[str, str] = {} if expected_reldata is None: # relation not initialized expected_reldata = { key: "" for key in ["private-address", "port", "source-type"] } # Initialize unit state (related to grafana) rel_id = self.harness.add_relation(interface, rel_app) self.harness.add_relation_unit(rel_id, rel_unit) # Trigger the -relation-changed hook, which will call the observed event self.harness.update_relation_data(rel_id, rel_app, rel_data) self.assertIsInstance(rel_id, int) # Verify the relation data set by the influxdb2 charm relation = self.harness.model.get_relation(interface) for key, expected_val in expected.items(): self.assertEqual( relation.data[self.harness.charm.unit].get(key, ""), expected_val) @mock.patch("subprocess.check_output") def test__grafana_source_data_leader( self, mock_check_output: mock.MagicMock) -> None: mock_check_output.return_value = b"10.0.0.1" expected_reldata: Dict[str, str] = { "private-address": "10.0.0.1", "port": "8086", "source-type": "influxdb", } self.harness.set_leader(True) self.test__grafana_source_data(expected_reldata=expected_reldata) # # Test Helpers # def test__influxdb2_layer(self) -> None: expected = { "summary": "influxdb2 layer", "description": "pebble config layer for influxdb2", "services": { "influxdb2": { "override": "replace", "summary": "influxdb2 service", "command": "/entrypoint.sh influxd", "startup": "enabled", "environment": { "DOCKER_INFLUXDB_INIT_MODE": "setup", "DOCKER_INFLUXDB_INIT_USERNAME": "******", "DOCKER_INFLUXDB_INIT_PASSWORD": "******", "DOCKER_INFLUXDB_INIT_ORG": "influxdata", "DOCKER_INFLUXDB_INIT_BUCKET": "default", "DOCKER_INFLUXDB_INIT_RETENTION": "0s", "DOCKER_INFLUXDB_INIT_ADMIN_TOKEN": "asdfasdfasdf", "INFLUXD_BOLT_PATH": "/var/lib/influxdbv2/influxd.bolt", "INFLUXD_ENGINE_PATH": "/var/lib/influxdbv2", "INFLUXD_HTTP_BIND_ADDRESS": ":8086", }, } }, } self.assertEqual(set(self.harness.charm._influxdb2_layer()), set(expected)) def test__is_running(self) -> None: container = self.harness.charm.unit.get_container(WORKLOAD_CONTAINER) service_not_running = self.harness.charm._is_running( container, "influxd") self.assertFalse(service_not_running)
class TestCharm(unittest.TestCase): def setUp(self): self.harness = Harness(MongoDBCharm) self.addCleanup(self.harness.cleanup) mongo_resource = { "registrypath": "mongodb:4.4.1", "username": "******", "password": "******" } self.harness.add_oci_resource("mongodb-image", mongo_resource) self.harness.begin() self.peer_rel_id = self.harness.add_relation('mongodb', 'mongodb') @patch('ops.testing._TestingPebbleClient.pull') def test_replica_set_name_can_be_changed(self, _): self.harness.set_leader(True) self.harness.container_pebble_ready("mongodb") # check default replica set name plan = self.harness.get_container_pebble_plan("mongodb") self.assertEqual(replica_set_name(plan), "rs0") # check replica set name can be changed self.harness.update_config({"replica_set_name": "new_name"}) plan = self.harness.get_container_pebble_plan("mongodb") self.assertEqual(replica_set_name(plan), "new_name") @patch("mongoserver.MongoDB.reconfigure_replica_set") def test_replica_set_is_reconfigured_when_peer_joins(self, mock_reconf): self.harness.set_leader(True) self.harness.add_relation_unit(self.peer_rel_id, 'mongodb/1') self.harness.update_relation_data(self.peer_rel_id, 'mongodb/1', {'private-address': '10.0.0.1'}) peers = [ 'mongodb-k8s-0.mongodb-k8s-endpoints', 'mongodb-k8s-1.mongodb-k8s-endpoints' ] mock_reconf.assert_called_once_with(peers) def test_replica_set_uri_data_is_generated_correctly(self): self.harness.set_leader(True) replica_set_uri = self.harness.charm.mongo.replica_set_uri() data = self.harness.get_relation_data(self.peer_rel_id, self.harness.model.app.name) cred = "root:{}".format(data['root_password']) self.assertEqual( replica_set_uri, 'mongodb://{}@mongodb-k8s-0.mongodb-k8s-endpoints:27017/admin'. format(cred)) def test_leader_sets_key_and_root_credentials(self): self.harness.set_leader(False) self.harness.set_leader(True) data = self.harness.get_relation_data(self.peer_rel_id, self.harness.model.app.name) self.assertIsNotNone(data['root_password']) self.assertIsNotNone(data['security_key']) @patch('mongoserver.MongoDB.version') def test_charm_provides_version(self, mock_version): self.harness.set_leader(True) mock_version.return_value = "4.4.1" version = self.harness.charm.mongo.version() self.assertEqual(version, "4.4.1") @patch('mongoserver.MongoDB.is_ready') def test_start_is_deferred_if_monog_is_not_ready(self, is_ready): is_ready.return_value = False self.harness.set_leader(True) with self.assertLogs(level="DEBUG") as logger: self.harness.charm.on.start.emit() is_ready.assert_called() for message in sorted(logger.output): if "DEBUG:ops.framework:Deferring" in message: self.assertIn("StartEvent", message) @patch('mongoserver.MongoDB.initialize_replica_set') @patch('mongoserver.MongoDB.is_ready') def test_start_is_deffered_if_monog_is_not_initialized( self, is_ready, initialize): is_ready.return_value = True initialize.side_effect = RuntimeError("Not Initialized") self.harness.set_leader(True) with self.assertLogs(level="DEBUG") as logger: self.harness.charm.on.start.emit() is_ready.assert_called() self.assertIn( "INFO:charm:Deferring on_start since : error=Not Initialized", sorted(logger.output))
class TestCharm(unittest.TestCase): def setUp(self): self.harness = Harness(RedisCharm) self.addCleanup(self.harness.cleanup) redis_resource = { "registrypath": "redis:6.0", # "username" and "password" are useless, but oci-resource # library fetch() fails if we do not provide them ... "username": "", "password": "" } self.harness.add_oci_resource("redis-image", redis_resource) self.harness.begin() def test_on_start_when_unit_is_not_leader(self): # Given self.harness.set_leader(False) # When self.harness.charm.on.start.emit() # Then self.assertEqual(self.harness.charm.unit.status, ActiveStatus()) @mock.patch.object(RedisClient, 'is_ready') def test_on_start_when_redis_is_not_ready(self, is_ready): # Given self.harness.set_leader(True) is_ready.return_value = False # When self.harness.charm.on.start.emit() # Then is_ready.assert_called_once_with() self.assertEqual(self.harness.charm.unit.status, WaitingStatus("Waiting for Redis ...")) @mock.patch.object(RedisClient, 'is_ready') def test_on_start_when_redis_is_ready(self, is_ready): # Given self.harness.set_leader(True) is_ready.return_value = True # When self.harness.charm.on.start.emit() # Then is_ready.assert_called_once_with() self.assertEqual(self.harness.charm.unit.status, ActiveStatus()) def test_on_stop(self): # When self.harness.charm.on.stop.emit() # Then self.assertEqual(self.harness.charm.unit.status, MaintenanceStatus('Pod is terminating.')) def test_on_config_changed_when_unit_is_not_leader(self): # Given self.harness.set_leader(False) # When self.harness.charm.on.config_changed.emit() # Then self.assertEqual(self.harness.charm.unit.status, ActiveStatus()) @mock.patch.object(RedisClient, 'is_ready') def test_on_config_changed_when_unit_is_leader_and_redis_is_ready( self, is_ready): # Given self.harness.set_leader(True) is_ready.return_value = True # When self.harness.charm.on.config_changed.emit() # Then self.assertIsNotNone(self.harness.charm.state.pod_spec) self.assertEqual(self.harness.charm.unit.status, ActiveStatus()) @mock.patch.object(OCIImageResource, 'fetch') def test_on_config_changed_when_unit_is_leader_but_image_fetch_breaks( self, fetch): # Given self.harness.set_leader(True) fetch.side_effect = OCIImageResourceError("redis-image") # When self.harness.charm.on.config_changed.emit() # Then fetch.assert_called_once_with() self.assertEqual(self.harness.charm.unit.status, BlockedStatus("Error fetching image information.")) def test_on_update_status_when_unit_is_not_leader(self): # Given self.harness.set_leader(False) # When self.harness.charm.on.update_status.emit() # Then self.assertEqual(self.harness.charm.unit.status, ActiveStatus()) @mock.patch.object(RedisClient, 'is_ready') def test_on_update_status_when_redis_is_not_ready(self, is_ready): # Given self.harness.set_leader(True) is_ready.return_value = False # When self.harness.charm.on.update_status.emit() # Then is_ready.assert_called_once_with() self.assertEqual(self.harness.charm.unit.status, WaitingStatus("Waiting for Redis ...")) @mock.patch.object(RedisClient, 'is_ready') def test_on_update_status_when_redis_is_ready(self, is_ready): # Given self.harness.set_leader(True) is_ready.return_value = True # When self.harness.charm.on.update_status.emit() # Then is_ready.assert_called_once_with() self.assertEqual(self.harness.charm.unit.status, ActiveStatus())
class TestCharm(unittest.TestCase): def setUp(self) -> None: # charm setup self.harness = Harness(GraylogCharm) self.addCleanup(self.harness.cleanup) self.harness.begin() self.harness.add_oci_resource('graylog-image') # patches self.mock_bind_address = \ mock.patch('charm.GraylogCharm.bind_address', new_callable=mock.PropertyMock) self.mock_external_uri = \ mock.patch('charm.GraylogCharm.external_uri', new_callable=mock.PropertyMock) self.mock_bind_address.start() self.mock_external_uri.start() # cleanup self.addCleanup(self.mock_bind_address.stop) self.addCleanup(self.mock_external_uri.stop) def test_pod_spec_port(self): self.harness.set_leader(True) # pretend to have mongo and elasticsearch self.harness.charm._stored.mongodb_uri = 'mongo://test_uri/' self.harness.charm._stored.elasticsearch_uri = 'http://test_es_uri' self.harness.update_config(BASE_CONFIG) self.harness.charm.on.config_changed.emit() spec, _ = self.harness.get_pod_spec() expected_port = 9000 actual_port = spec['containers'][0]['ports'][0]['containerPort'] self.assertEqual(expected_port, actual_port) def test_elasticsearch_and_mongodb_conn_strings(self): self.harness.set_leader(True) self.harness.update_config(BASE_CONFIG) # add the elasticsearch relation es_rel_id = self.harness.add_relation('elasticsearch', 'elasticsearch') mongo_rel_id = self.harness.add_relation('mongodb', 'mongodb') self.harness.add_relation_unit(es_rel_id, 'elasticsearch/0') self.harness.add_relation_unit(mongo_rel_id, 'mongodb/0') # add elasticsearch relation data es_rel_data = { 'ingress-address': '10.183.1.2', 'port': 9200, } self.harness.update_relation_data(es_rel_id, 'elasticsearch/0', es_rel_data) self.assertTrue(self.harness.charm.has_elasticsearch) # add mongodb relation data mongo_rel_data = { 'replica_set_uri': 'mongo://10.0.0.2:14001,10.0.0.3:14002', 'replicated': 'True', 'replica_set_name': 'rs0', } self.harness.update_relation_data(mongo_rel_id, 'mongodb/0', mongo_rel_data) self.assertTrue(self.harness.charm.has_mongodb) # test that elasticsearch-uri properly made it to the _stored variable expected_uri = 'http://10.183.1.2:9200' self.assertEqual(expected_uri, self.harness.charm._stored.elasticsearch_uri) # now emit the relation broken events and make sure the _stored variables are cleared es_rel = self.harness.model.get_relation('elasticsearch') mongo_rel = self.harness.model.get_relation('mongodb') self.harness.charm.on.elasticsearch_relation_broken.emit(es_rel) self.harness.charm.on.mongodb_relation_broken.emit(mongo_rel) self.assertEqual(str(), self.harness.charm._stored.elasticsearch_uri) self.assertEqual(str(), self.harness.charm._stored.mongodb_uri) def test_blocking_without_mongodb_and_elasticsearch(self): self.harness.set_leader(True) with self.assertLogs(level='WARNING') as logger: self.harness.update_config(BASE_CONFIG) msg = 'WARNING:charm:Need both mongodb and Elasticsearch ' \ 'relation for Graylog to function properly. Blocking.' self.assertEqual(sorted(logger.output), [msg]) def test_check_config_with_missing_option(self): self.harness.set_leader(True) missing_password_config = {'port': 9000, 'admin-password': ''} with self.assertLogs(level='WARNING') as logger: self.harness.update_config(missing_password_config) msg = 'ERROR:charm:Need admin-password config option before setting pod spec.' self.assertEqual(sorted(logger.output), [msg]) self.assertEqual( self.harness.model.unit.status, BlockedStatus("Need 'admin-password' config option."))
class TestCharm(unittest.TestCase): def setUp(self) -> None: self.harness = Harness(MySQLCharm) self.addCleanup(self.harness.cleanup) self.harness.begin() self.harness.add_oci_resource("mysql-image") def test_env_config(self): config_1 = { "MYSQL_ROOT_PASSWORD": "******", "MYSQL_USER": "******", "MYSQL_PASSWORD": "******", "MYSQL_DATABASE": "db_10", } self.harness.update_config(config_1) self.assertEqual(self.harness.charm.env_config["MYSQL_ROOT_PASSWORD"], "D10S") self.assertEqual(self.harness.charm.env_config["MYSQL_USER"], "DiegoArmando") self.assertEqual(self.harness.charm.env_config["MYSQL_PASSWORD"], "SegurolaYHabana") self.assertEqual(self.harness.charm.env_config["MYSQL_DATABASE"], "db_10") def test_generate_random_root_password(self): self.harness.set_leader(True) config_2 = { "MYSQL_ROOT_PASSWORD": "", } self.harness.update_config(config_2) self.assertEqual( len(self.harness.charm.env_config["MYSQL_ROOT_PASSWORD"]), 20) def test_pod_spec(self): self.harness.set_leader(True) config_1 = { "MYSQL_ROOT_PASSWORD": "******", } self.harness.update_config(config_1) pod_spec = self.harness.charm._build_pod_spec() self.assertEqual(pod_spec["containers"][0]["name"], "mysql") self.assertEqual( pod_spec["containers"][0]["ports"][0]["containerPort"], 3306) self.assertEqual(pod_spec["containers"][0]["ports"][0]["protocol"], "TCP") self.assertEqual( pod_spec["containers"][0]["envConfig"]["MYSQL_ROOT_PASSWORD"], "D10S", ) self.assertEqual(pod_spec["version"], 3) def test_status(self): config_1 = { "MYSQL_ROOT_PASSWORD": "******", } self.harness.set_leader(True) self.harness.update_config(config_1) self.assertEqual(self.harness.charm.unit.status, ActiveStatus()) def test_default_configs(self): config = self.harness.model.config self.assertEqual(config["port"], 3306) self.assertTrue("MYSQL_ROOT_PASSWORD" in config) self.assertEqual(config["MYSQL_ROOT_PASSWORD"], "") def test_ony_leader_can_configure_root_password(self): self.harness.set_leader(False) config = { "MYSQL_ROOT_PASSWORD": "******", } self.harness.update_config(config) self.assertNotIn("MYSQL_ROOT_PASSWORD", self.harness.charm._stored.mysql_setup) def test_new_unit_has_password(self): config = { "MYSQL_ROOT_PASSWORD": "******", } self.harness.update_config(config) relation_id = self.harness.add_relation("mysql", "mysql") self.harness.add_relation_unit(relation_id, "mysql/1") self.harness.update_relation_data(relation_id, "mysql", config) self.assertEqual( config["MYSQL_ROOT_PASSWORD"], self.harness.charm._stored.mysql_setup["MYSQL_ROOT_PASSWORD"], )
class GrafanaCharmTest(unittest.TestCase): def setUp(self) -> None: self.harness = Harness(GrafanaK8s) self.addCleanup(self.harness.cleanup) self.harness.begin() self.harness.add_oci_resource('grafana-image') def test__grafana_source_data(self): self.harness.set_leader(True) self.harness.update_config(BASE_CONFIG) self.assertEqual(self.harness.charm.datastore.sources, {}) rel_id = self.harness.add_relation('grafana-source', 'prometheus') self.harness.add_relation_unit(rel_id, 'prometheus/0') self.assertIsInstance(rel_id, int) # test that the unit data propagates the correct way # which is through the triggering of on_relation_changed self.harness.update_relation_data( rel_id, 'prometheus/0', { 'private-address': '192.0.2.1', 'port': 1234, 'source-type': 'prometheus', 'source-name': 'prometheus-app', }) expected_first_source_data = { 'private-address': '192.0.2.1', 'port': 1234, 'source-name': 'prometheus-app', 'source-type': 'prometheus', 'isDefault': 'true', 'unit_name': 'prometheus/0' } self.assertEqual(expected_first_source_data, dict(self.harness.charm.datastore.sources[rel_id])) # test that clearing the relation data leads to # the datastore for this data source being cleared self.harness.update_relation_data(rel_id, 'prometheus/0', { 'private-address': None, 'port': None, }) self.assertEqual(None, self.harness.charm.datastore.sources.get(rel_id)) def test__ha_database_and_status_check(self): """If there is a peer connection and no database (needed for HA), the charm should put the application in a blocked state.""" # start charm with one peer and no database relation self.harness.set_leader(True) self.harness.update_config(BASE_CONFIG) self.assertEqual(self.harness.charm.unit.status, ActiveStatus()) # ensure _check_high_availability() ends up with the correct status status = self.harness.charm._check_high_availability() self.assertEqual(status, MaintenanceStatus('Grafana ready on single node.')) # make sure that triggering 'update-status' hook does not # overwrite the current active status self.harness.charm.on.update_status.emit() self.assertEqual(self.harness.charm.unit.status, ActiveStatus()) peer_rel_id = self.harness.add_relation('grafana', 'grafana') # add main unit and its data # self.harness.add_relation_unit(peer_rel_id, 'grafana/0') # will trigger the grafana-changed hook self.harness.update_relation_data(peer_rel_id, 'grafana/0', {'private-address': '10.1.2.3'}) # add peer unit and its data self.harness.add_relation_unit(peer_rel_id, 'grafana/1') self.harness.update_relation_data(peer_rel_id, 'grafana/1', {'private-address': '10.0.0.1'}) self.assertTrue(self.harness.charm.has_peer) self.assertFalse(self.harness.charm.has_db) self.assertEqual(self.harness.charm.unit.status, BlockedStatus('Need database relation for HA.')) # ensure update-status hook doesn't overwrite this self.harness.charm.on.update_status.emit() self.assertEqual(self.harness.charm.unit.status, BlockedStatus('Need database relation for HA.')) # now add the database connection and the model should # not have a blocked status db_rel_id = self.harness.add_relation('database', 'mysql') self.harness.add_relation_unit(db_rel_id, 'mysql/0') self.harness.update_relation_data( db_rel_id, 'mysql/0', { 'type': 'mysql', 'host': '10.10.10.10:3306', 'name': 'test_mysql_db', 'user': '******', 'password': '******', }) self.assertTrue(self.harness.charm.has_db) self.assertEqual(self.harness.charm.unit.status, ActiveStatus()) # ensure _check_high_availability() ends up with the correct status status = self.harness.charm._check_high_availability() self.assertEqual(status, MaintenanceStatus('Grafana ready for HA.')) def test__database_relation_data(self): self.harness.set_leader(True) self.harness.update_config(BASE_CONFIG) self.assertEqual(self.harness.charm.datastore.database, {}) # add relation and update relation data rel_id = self.harness.add_relation('database', 'mysql') rel = self.harness.model.get_relation('database') self.harness.add_relation_unit(rel_id, 'mysql/0') test_relation_data = { 'type': 'mysql', 'host': '0.1.2.3:3306', 'name': 'my-test-db', 'user': '******', 'password': '******', } self.harness.update_relation_data(rel_id, 'mysql/0', test_relation_data) # check that charm datastore was properly set self.assertEqual(dict(self.harness.charm.datastore.database), test_relation_data) # now depart this relation and ensure the datastore is emptied self.harness.charm.on.database_relation_broken.emit(rel) self.assertEqual({}, dict(self.harness.charm.datastore.database)) def test__multiple_database_relation_handling(self): self.harness.set_leader(True) self.harness.update_config(BASE_CONFIG) self.assertEqual(self.harness.charm.datastore.database, {}) # add first database relation self.harness.add_relation('database', 'mysql') # add second database relation -- should fail here with self.assertRaises(TooManyRelatedAppsError): self.harness.add_relation('database', 'mysql') self.harness.charm.model.get_relation('database') def test__multiple_source_relations(self): """This will test data-source config text with multiple sources. Specifically, it will test multiple grafana-source relations.""" self.harness.set_leader(True) self.harness.update_config(BASE_CONFIG) self.assertEqual(self.harness.charm.datastore.sources, {}) # add first relation rel_id0 = self.harness.add_relation('grafana-source', 'prometheus') self.harness.add_relation_unit(rel_id0, 'prometheus/0') # add test data to grafana-source relation # and test that _make_data_source_config_text() works as expected prom_source_data = { 'private-address': '192.0.2.1', 'port': 4321, 'source-type': 'prometheus' } self.harness.update_relation_data(rel_id0, 'prometheus/0', prom_source_data) header_text = textwrap.dedent(""" apiVersion: 1 datasources:""") correct_config_text0 = header_text + textwrap.dedent(""" - name: prometheus_0 type: prometheus access: proxy url: http://192.0.2.1:4321 isDefault: true editable: true orgId: 1""") generated_text = self.harness.charm._make_data_source_config_text() self.assertEqual(correct_config_text0 + '\n', generated_text) # add another source relation and check the resulting config text jaeger_source_data = { 'private-address': '255.255.255.0', 'port': 7890, 'source-type': 'jaeger', 'source-name': 'jaeger-application' } rel_id1 = self.harness.add_relation('grafana-source', 'jaeger') self.harness.add_relation_unit(rel_id1, 'jaeger/0') self.harness.update_relation_data(rel_id1, 'jaeger/0', jaeger_source_data) correct_config_text1 = correct_config_text0 + textwrap.dedent(""" - name: jaeger-application type: jaeger access: proxy url: http://255.255.255.0:7890 isDefault: false editable: true orgId: 1""") generated_text = self.harness.charm._make_data_source_config_text() self.assertEqual(correct_config_text1 + '\n', generated_text) # test removal of second source results in config_text # that is the same as the original self.harness.update_relation_data(rel_id1, 'jaeger/0', { 'private-address': None, 'port': None, }) generated_text = self.harness.charm._make_data_source_config_text() correct_text_after_removal = textwrap.dedent(""" apiVersion: 1 deleteDatasources: - name: jaeger-application orgId: 1 datasources: - name: prometheus_0 type: prometheus access: proxy url: http://192.0.2.1:4321 isDefault: true editable: true orgId: 1""") self.assertEqual(correct_text_after_removal + '\n', generated_text) # now test that the 'deleteDatasources' is gone generated_text = self.harness.charm._make_data_source_config_text() self.assertEqual(correct_config_text0 + '\n', generated_text) def test__pod_spec_container_datasources(self): self.harness.set_leader(True) self.harness.update_config(BASE_CONFIG) self.assertEqual(self.harness.charm.datastore.sources, {}) # add first relation rel_id = self.harness.add_relation('grafana-source', 'prometheus') self.harness.add_relation_unit(rel_id, 'prometheus/0') # add test data to grafana-source relation # and test that _make_data_source_config_text() works as expected prom_source_data = { 'private-address': '192.0.2.1', 'port': 4321, 'source-type': 'prometheus' } self.harness.update_relation_data(rel_id, 'prometheus/0', prom_source_data) data_source_file_text = textwrap.dedent(""" apiVersion: 1 datasources: - name: prometheus_0 type: prometheus access: proxy url: http://192.0.2.1:4321 isDefault: true editable: true orgId: 1 """) config_ini_file_text = textwrap.dedent(""" [paths] provisioning = /etc/grafana/provisioning [log] mode = console level = {0} """).format(self.harness.model.config['grafana_log_level'], ) expected_container_files_spec = [{ 'name': 'grafana-datasources', 'mountPath': '/etc/grafana/provisioning/datasources', 'files': [{ 'path': 'datasources.yaml', 'content': data_source_file_text, }], }, { 'name': 'grafana-config-ini', 'mountPath': '/etc/grafana', 'files': [{ 'path': 'grafana.ini', 'content': config_ini_file_text, }] }] pod_spec, _ = self.harness.get_pod_spec() container = get_container(pod_spec, 'grafana') actual_container_files_spec = container['volumeConfig'] self.assertEqual(expected_container_files_spec, actual_container_files_spec) def test__access_sqlite_storage_location(self): expected_path = '/var/lib/grafana' actual_path = self.harness.charm.meta.storages['sqlitedb'].location self.assertEqual(expected_path, actual_path) def test__config_ini_without_database(self): self.harness.update_config(BASE_CONFIG) expected_config_text = textwrap.dedent(""" [paths] provisioning = /etc/grafana/provisioning [log] mode = console level = {0} """).format(self.harness.model.config['grafana_log_level'], ) actual_config_text = self.harness.charm._make_config_ini_text() self.assertEqual(expected_config_text, actual_config_text) def test__config_ini_with_database(self): self.harness.set_leader(True) self.harness.update_config(BASE_CONFIG) # add database relation and update relation data rel_id = self.harness.add_relation('database', 'mysql') self.harness.add_relation_unit(rel_id, 'mysql/0') test_relation_data = { 'type': 'mysql', 'host': '0.1.2.3:3306', 'name': 'my-test-db', 'user': '******', 'password': '******', } self.harness.update_relation_data(rel_id, 'mysql/0', test_relation_data) # test the results of _make_config_ini_text() expected_config_text = textwrap.dedent( """ [paths] provisioning = /etc/grafana/provisioning [log] mode = console level = {0} [database] type = mysql host = 0.1.2.3:3306 name = my-test-db user = test-user password = super!secret!password url = mysql://test-user:[email protected]:3306/my-test-db""" ).format(self.harness.model.config['grafana_log_level'], ) actual_config_text = self.harness.charm._make_config_ini_text() self.assertEqual(expected_config_text, actual_config_text) def test__duplicate_source_names(self): self.harness.set_leader(True) self.harness.update_config(BASE_CONFIG) self.assertEqual(self.harness.charm.datastore.sources, {}) # add first relation p_rel_id = self.harness.add_relation('grafana-source', 'prometheus') p_rel = self.harness.model.get_relation('grafana-source', p_rel_id) self.harness.add_relation_unit(p_rel_id, 'prometheus/0') # add test data to grafana-source relation prom_source_data0 = { 'private-address': '192.0.2.1', 'port': 4321, 'source-type': 'prometheus', 'source-name': 'duplicate-source-name' } self.harness.update_relation_data(p_rel_id, 'prometheus/0', prom_source_data0) expected_prom_source_data = { 'private-address': '192.0.2.1', 'port': 4321, 'source-name': 'duplicate-source-name', 'source-type': 'prometheus', 'isDefault': 'true', 'unit_name': 'prometheus/0' } self.assertEqual(dict(self.harness.charm.datastore.sources[p_rel_id]), expected_prom_source_data) # add second source with the same name as the first source g_rel_id = self.harness.add_relation('grafana-source', 'graphite') g_rel = self.harness.model.get_relation('grafana-source', g_rel_id) self.harness.add_relation_unit(g_rel_id, 'graphite/0') graphite_source_data0 = { 'private-address': '192.12.23.34', 'port': 4321, 'source-type': 'graphite', 'source-name': 'duplicate-source-name' } expected_graphite_source_data = { 'isDefault': 'false', 'port': 4321, 'private-address': '192.12.23.34', 'source-name': 'graphite_1', 'source-type': 'graphite', 'unit_name': 'graphite/0' } self.harness.update_relation_data(g_rel_id, 'graphite/0', graphite_source_data0) self.assertEqual( expected_graphite_source_data, dict(self.harness.charm.datastore.sources.get(g_rel_id))) self.assertEqual(2, len(self.harness.charm.datastore.sources)) # now remove the relation and ensure datastore source-name is removed self.harness.charm.on.grafana_source_relation_broken.emit(p_rel) self.assertEqual(None, self.harness.charm.datastore.sources.get(p_rel_id)) self.assertEqual(1, len(self.harness.charm.datastore.sources)) # remove graphite relation self.harness.charm.on.grafana_source_relation_broken.emit(g_rel) self.assertEqual(None, self.harness.charm.datastore.sources.get(g_rel_id)) self.assertEqual(0, len(self.harness.charm.datastore.sources)) def test__idempotent_datasource_file_hash(self): self.harness.set_leader(True) self.harness.update_config(BASE_CONFIG) rel_id = self.harness.add_relation('grafana-source', 'prometheus') self.harness.add_relation_unit(rel_id, 'prometheus/0') self.assertIsInstance(rel_id, int) # test that the unit data propagates the correct way # which is through the triggering of on_relation_changed self.harness.update_relation_data( rel_id, 'prometheus/0', { 'private-address': '192.0.2.1', 'port': 1234, 'source-type': 'prometheus', 'source-name': 'prometheus-app', }) # get a hash of the created file and check that it matches the pod spec pod_spec, _ = self.harness.get_pod_spec() container = get_container(pod_spec, 'grafana') hash_text = hashlib.md5(container['volumeConfig'][0]['files'][0] ['content'].encode()).hexdigest() self.assertEqual(container['envConfig']['DATASOURCES_YAML'], hash_text) # test the idempotence of the call by re-configuring the pod spec self.harness.charm.configure_pod() self.assertEqual(container['envConfig']['DATASOURCES_YAML'], hash_text)
class TestCharm(unittest.TestCase): def setUp(self) -> None: self.harness = Harness(MySQLCharm) self.addCleanup(self.harness.cleanup) self.harness.begin() self.harness.add_oci_resource("mysql-image") def test_pebble_layer_is_dict(self): self.harness.set_leader(True) config = {} relation_id = self.harness.add_relation("mysql", "mysql") self.harness.add_relation_unit(relation_id, "mysql/1") self.harness.update_config(config) layer = self.harness.charm._build_pebble_layer() self.assertIsInstance(layer["services"]["mysql"], dict) def test_pebble_layer_has_random_root_password(self): self.harness.set_leader(True) config = {} relation_id = self.harness.add_relation("mysql", "mysql") self.harness.add_relation_unit(relation_id, "mysql/1") self.harness.update_config(config) env = self.harness.charm._build_pebble_layer( )["services"]["mysql"]["environment"] self.assertEqual(len(env["MYSQL_ROOT_PASSWORD"]), 20) def test_pebble_layer_with_custom_config(self): self.harness.set_leader(True) config = { "mysql_root_password": "******", "mysql_user": "******", "mysql_password": "******", "mysql_database": "db_10", } relation_id = self.harness.add_relation("mysql", "mysql") self.harness.add_relation_unit(relation_id, "mysql/1") self.harness.update_config(config) env = self.harness.charm._build_pebble_layer( )["services"]["mysql"]["environment"] self.assertEqual(env["MYSQL_ROOT_PASSWORD"], config["mysql_root_password"]) self.assertEqual(env["MYSQL_USER"], config["mysql_user"]) self.assertEqual(env["MYSQL_PASSWORD"], config["mysql_password"]) self.assertEqual(env["MYSQL_DATABASE"], config["mysql_database"]) def test_default_configs(self): config = self.harness.model.config self.assertTrue("mysql_root_password" in config) self.assertEqual(config["mysql_root_password"], "") def test__on_config_changed_is_leader(self): self.harness.set_leader(True) config = { "mysql_root_password": "******", } relation_id = self.harness.add_relation("mysql", "mysql") self.harness.add_relation_unit(relation_id, "mysql/1") self.harness.update_config(config) peers_data = self.harness.charm.model.get_relation("mysql").data[ self.harness.charm.app] self.assertIn("mysql_root_password", peers_data) def test__on_config_changed_is_not_leader(self): self.harness.set_leader(False) config = { "mysql_root_password": "******", } relation_id = self.harness.add_relation("mysql", "mysql") self.harness.add_relation_unit(relation_id, "mysql/1") self.harness.update_config(config) peers_data = self.harness.charm.model.get_relation("mysql").data[ self.harness.charm.app] self.assertNotIn("mysql_root_password", peers_data) @patch("charm.MySQLCharm.unit_ip") def test__update_status_unit_is_leader_mysql_is_not_ready( self, mock_unit_ip): mock_unit_ip.return_value = "10.0.0.1" with patch("mysqlserver.MySQL.is_ready") as mock_is_ready: mock_is_ready.return_value = False self.harness.set_leader(True) config = { "mysql_root_password": "******", } relation_id = self.harness.add_relation("mysql", "mysql") self.harness.add_relation_unit(relation_id, "mysql/1") self.harness.update_config(config) self.assertEqual(self.harness.charm.on.update_status.emit(), None) self.assertEqual(type(self.harness.charm.unit.status), WaitingStatus) self.assertEqual(self.harness.charm.unit.status.message, "MySQL not ready yet") @patch("charm.MySQLCharm._is_mysql_initialized") @patch("mysqlserver.MySQL.is_ready") @patch("charm.MySQLCharm.unit_ip") def test__update_status_unit_is_leader_mysql_not_initialized( self, mock_unit_ip, mock_is_ready, mock_is_mysql_initialized): mock_unit_ip.return_value = "10.0.0.1" mock_is_ready.return_value = True mock_is_mysql_initialized.return_value = False self.harness.set_leader(True) config = { "mysql_root_password": "******", } relation_id = self.harness.add_relation("mysql", "mysql") self.harness.add_relation_unit(relation_id, "mysql/1") self.harness.update_config(config) self.harness.charm.on.update_status.emit() self.assertEqual(type(self.harness.charm.unit.status), WaitingStatus) self.assertEqual(self.harness.charm.unit.status.message, "MySQL not initialized") @patch("charm.MySQLCharm._is_mysql_initialized") @patch("mysqlserver.MySQL.is_ready") @patch("charm.MySQLCharm.unit_ip") def test__update_status_unit_is_leader_mysql_initialized( self, mock_unit_ip, mock_is_ready, mock_is_mysql_initialized): mock_unit_ip.return_value = "10.0.0.1" mock_is_ready.return_value = True mock_is_mysql_initialized.return_value = True self.harness.set_leader(True) config = { "mysql_root_password": "******", } relation_id = self.harness.add_relation("mysql", "mysql") self.harness.add_relation_unit(relation_id, "mysql/1") self.harness.update_config(config) self.harness.charm.on.update_status.emit() self.assertEqual(type(self.harness.charm.unit.status), ActiveStatus) def test__restart_service_model_error(self): self.harness.set_leader(True) with self.assertLogs(level="DEBUG") as logger: self.harness.charm._restart_service() self.assertEqual( sorted(logger.output), ["DEBUG:charm:MySQL service is not yet ready"], )