diff --git a/src/inmanta/data_pg.py b/src/inmanta/data_pg.py index 3776394013..3ba02f8915 100644 --- a/src/inmanta/data_pg.py +++ b/src/inmanta/data_pg.py @@ -1828,13 +1828,13 @@ def done(self) -> int: return self._done @classmethod - async def _get_status_field(cls, environment, values): + async def _get_status_field(cls, environment: uuid.UUID, values: str) -> dict: """ This field is required to ensure backward compatibility on the API. """ result = {} + values = json.loads(values) for value_entry in values: - value_entry = json.loads(value_entry) entry_uuid = str(uuid.uuid5(environment, value_entry['id'])) result[entry_uuid] = value_entry return result @@ -1850,10 +1850,10 @@ async def get_list(cls, order_by_column=None, order="ASC", limit=None, offset=No limit_statement = f"LIMIT {limit} " if limit is not None and limit > 0 else "" offset_statement = f"OFFSET {offset} " if offset is not None and offset > 0 else "" query = f"SELECT c.*, SUM(CASE WHEN r.status NOT IN({transient_states}) THEN 1 ELSE 0 END) AS done ," + \ - f"array(SELECT jsonb_build_object('status', r2.status, 'id', r2.resource_version_id) " + \ + f"to_json(array(SELECT jsonb_build_object('status', r2.status, 'id', r2.resource_version_id) " + \ f" FROM {Resource.table_name()} AS r2 " \ f" WHERE c.environment=r2.environment AND c.version=r2.model" \ - f" ) AS status " + \ + f" )) AS status " + \ f"FROM {cls.table_name()} AS c LEFT OUTER JOIN {Resource.table_name()} AS r " + \ f"ON c.environment = r.environment AND c.version = r.model " + \ f"{where_statement} " + \ @@ -1989,6 +1989,31 @@ async def mark_done(self): self.result = const.VersionState[result] self.deployed = True + @classmethod + async def mark_done_if_done(cls, environment, version): + query = f"""UPDATE {ConfigurationModel.table_name()} + SET deployed=True, + result=(CASE WHEN ( + EXISTS(SELECT 1 + FROM {Resource.table_name()} + WHERE environment=$1 AND model=$2 AND status != $3) + )::boolean + THEN $4::versionstate + ELSE $5::versionstate END + ) + WHERE environment=$1 AND version=$2 AND + total=(SELECT COUNT(*) + FROM Resource + WHERE environment=$1 AND model=$2 AND status NOT IN('available', 'deploying' + ) + )""" + values = [cls._get_value(environment), + cls._get_value(version), + cls._get_value(ResourceState.deployed), + cls._get_value(const.VersionState.failed), + cls._get_value(const.VersionState.success)] + await cls._execute_query(query, *values) + async def get_increment(self): """ Find resources incremented by this version compared to deployment state transitions per resource diff --git a/src/inmanta/server/server.py b/src/inmanta/server/server.py index 1f94384ab0..71b5d97443 100644 --- a/src/inmanta/server/server.py +++ b/src/inmanta/server/server.py @@ -1450,11 +1450,7 @@ def resource_action_update(self, env, resource_ids, action_id, action, started, if "purged" in res.attributes and res.attributes["purged"] and status == const.ResourceState.deployed: yield data.Parameter.delete_all(environment=env.id, resource_id=res.resource_id) - model = yield data.ConfigurationModel.get_version(env.id, model_version) - - if model.done == model.total: - LOGGER.info("marking model %s %d as done", env.id, model_version) - yield model.mark_done() + yield data.ConfigurationModel.mark_done_if_done(env.id, model_version) waiting_agents = set([(Id.parse_id(prov).get_agent_name(), res.resource_version_id) for res in resources for prov in res.provides]) diff --git a/tests/test_data_pg.py b/tests/test_data_pg.py index d7ac4876ae..4470b14097 100644 --- a/tests/test_data_pg.py +++ b/tests/test_data_pg.py @@ -576,6 +576,50 @@ async def test_model_set_ready(init_dataclasses_and_load_schema): assert cm.done == 1 +@pytest.mark.parametrize("resource_state, should_be_deployed", [ + (const.ResourceState.unavailable, True), + (const.ResourceState.skipped, True), + (const.ResourceState.dry, True), + (const.ResourceState.deployed, True), + (const.ResourceState.failed, True), + (const.ResourceState.deploying, False), + (const.ResourceState.available, False), + (const.ResourceState.cancelled, True), + (const.ResourceState.undefined, True), + (const.ResourceState.skipped_for_undefined, True), + (const.ResourceState.processing_events, True), +]) +@pytest.mark.asyncio +async def test_model_mark_done_if_done(init_dataclasses_and_load_schema, resource_state, should_be_deployed): + project = data.Project(name="test") + await project.insert() + + env = data.Environment(name="dev", project=project.id, repo_url="", repo_branch="") + await env.insert() + + version = int(time.time()) + cm = data.ConfigurationModel(environment=env.id, version=version, date=datetime.datetime.now(), total=1, version_info={}) + await cm.insert() + + assert cm.done == 0 + + path = "/etc/file" + key = "std::File[agent1,path=" + path + "]" + resource = data.Resource.new(environment=env.id, resource_version_id=key + ",v=%d" % version, + attributes={"path": path}) + await resource.insert() + + assert not cm.deployed + await data.ConfigurationModel.mark_done_if_done(env.id, cm.version) + cm = await data.ConfigurationModel.get_one(version=version, environment=env.id) + assert not cm.deployed + + await resource.update_fields(status=resource_state) + await data.ConfigurationModel.mark_done_if_done(env.id, cm.version) + cm = await data.ConfigurationModel.get_one(version=version, environment=env.id) + assert cm.deployed == should_be_deployed + + @pytest.mark.asyncio async def test_model_get_list(init_dataclasses_and_load_schema): project = data.Project(name="test")