Skip to content

Commit 4223f75

Browse files
arnaudsjssanderr
authored andcommitted
Fix bug where the GET /api/v2/resource/<rid> and GET /api/v2/resource endpoints return an incorrect resourcestate if a resource moved back to the available state in a new version of the configurationmodel. (PR #7609)
Fix bug where the `GET /api/v2/resource/<rid>` and `GET /api/v2/resource` endpoints return an incorrect resourcestate if a resource moved back to the available state in a new version of the configurationmodel. - [ ] ~~Attached issue to pull request~~ - [x] Changelog entry - [x] Type annotations are present - [x] Code is clear and sufficiently documented - [x] No (preventable) type errors (check using make mypy or make mypy-diff) - [x] Sufficient test cases (reproduces the bug/tests the requested feature) - [x] Correct, in line with design - [ ] ~~End user documentation is included or an issue is created for end-user documentation~~ - [ ] ~~If this PR fixes a race condition in the test suite, also push the fix to the relevant stable branche(s) (see [test-fixes](https://internal.inmanta.com/development/core/tasks/build-master.html#test-fixes) for more info)~~
1 parent e2c64b4 commit 4223f75

File tree

6 files changed

+242
-39
lines changed

6 files changed

+242
-39
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
description: Fix bug where the `GET /api/v2/resource/<rid>` and `GET /api/v2/resource` endpoints return an incorrect resourcestate if a resource moved back to the available state in a new version of the configurationmodel.
3+
change-type: patch
4+
destination-branches: [master, iso7]
5+
sections:
6+
bugfix: "{{description}}"

src/inmanta/data/__init__.py

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4703,34 +4703,39 @@ async def get_current_resource_state(cls, env: uuid.UUID, rid: ResourceIdStr) ->
47034703
Return the state of the given resource in the latest version of the configuration model
47044704
or None if the resource is not present in the latest version.
47054705
"""
4706-
query = f"""
4706+
query = """
4707+
WITH latest_released_version AS (
4708+
SELECT max(version) AS version
4709+
FROM configurationmodel
4710+
WHERE environment=$1 AND released
4711+
)
47074712
SELECT (
4708-
SELECT r.status
4709-
FROM resource r
4710-
WHERE r.environment=$1
4711-
AND r.model=(
4712-
SELECT max(c.version) AS version
4713-
FROM {ConfigurationModel.table_name()} c
4714-
WHERE c.environment=$1 AND c.released
4715-
)
4716-
AND r.resource_id=$2
4717-
) AS status,
4718-
(
4719-
SELECT rps.last_non_deploying_status
4720-
FROM resource_persistent_state rps
4721-
WHERE rps.environment=$1 AND rps.resource_id=$2
4722-
) AS last_non_deploying_status
4713+
CASE
4714+
-- The resource_persistent_state.last_non_deploying_status column is only populated for
4715+
-- actual deployment operations to prevent locking issues. This case-statement calculates
4716+
-- the correct state from the combination of the resource table and the
4717+
-- resource_persistent_state table.
4718+
WHEN r.status::text IN('deploying', 'undefined', 'skipped_for_undefined')
4719+
-- The deploying, undefined and skipped_for_undefined states are not tracked in the
4720+
-- resource_persistent_state table.
4721+
THEN r.status::text
4722+
WHEN rps.last_deployed_attribute_hash != r.attribute_hash
4723+
-- The hash changed since the last deploy -> new desired state
4724+
THEN r.status::text
4725+
-- No override required, use last known state from actual deployment
4726+
ELSE rps.last_non_deploying_status::text
4727+
END
4728+
) AS status
4729+
FROM resource AS r
4730+
INNER JOIN resource_persistent_state AS rps ON r.environment=rps.environment AND r.resource_id=rps.resource_id
4731+
INNER JOIN configurationmodel AS c ON c.environment=r.environment AND c.version=r.model
4732+
WHERE r.environment=$1 AND r.model = (SELECT version FROM latest_released_version) AND r.resource_id=$2
47234733
"""
47244734
results = await cls.select_query(query, [env, rid], no_obj=True)
47254735
if not results:
47264736
return None
47274737
assert len(results) == 1
4728-
if any(results[0][column_name] is None for column_name in ["status", "last_non_deploying_status"]):
4729-
return None
4730-
if results[0]["status"] == "deploying":
4731-
return const.ResourceState("deploying")
4732-
else:
4733-
return const.ResourceState(results[0]["last_non_deploying_status"])
4738+
return const.ResourceState(results[0]["status"])
47344739

47354740
@classmethod
47364741
async def set_deployed_multi(
@@ -5054,14 +5059,26 @@ async def get_resource_details(cls, env: uuid.UUID, resource_id: m.ResourceIdStr
50545059
def status_sub_query(resource_table_name: str) -> str:
50555060
return f"""
50565061
(CASE
5062+
-- The resource_persistent_state.last_non_deploying_status column is only populated for
5063+
-- actual deployment operations to prevent locking issues. This case-statement calculates
5064+
-- the correct state from the combination of the resource table and the
5065+
-- resource_persistent_state table.
50575066
WHEN (SELECT {resource_table_name}.model < MAX(configurationmodel.version)
50585067
FROM configurationmodel
50595068
WHERE configurationmodel.released=TRUE
5060-
AND environment = $1)
5069+
AND environment = $1
5070+
)
5071+
-- Resource is no longer present in latest released configurationmodel
50615072
THEN 'orphaned'
5062-
WHEN {resource_table_name}.status::text = 'deploying'
5063-
then 'deploying'
5064-
ELSE ps.last_non_deploying_status::text
5073+
WHEN {resource_table_name}.status::text IN('deploying', 'undefined', 'skipped_for_undefined')
5074+
-- The deploying, undefined and skipped_for_undefined states are not tracked in the
5075+
-- resource_persistent_state table.
5076+
THEN {resource_table_name}.status::text
5077+
WHEN ps.last_deployed_attribute_hash != {resource_table_name}.attribute_hash
5078+
-- The hash changed since the last deploy -> new desired state
5079+
THEN {resource_table_name}.status::text
5080+
-- No override required, use last known state from actual deployment
5081+
ELSE ps.last_non_deploying_status::text
50655082
END
50665083
) as status
50675084
"""
@@ -5156,11 +5173,13 @@ async def get_resource_deploy_summary(cls, environment: uuid.UUID) -> m.Resource
51565173
inner_query = f"""
51575174
SELECT r.resource_id as resource_id,
51585175
(
5159-
CASE WHEN (r.status = 'deploying')
5160-
THEN
5161-
r.status::text
5162-
ELSE
5163-
rps.last_non_deploying_status::text
5176+
CASE WHEN r.status IN ('deploying', 'undefined', 'skipped_for_undefined')
5177+
THEN r.status::text
5178+
WHEN rps.last_deployed_attribute_hash != r.attribute_hash
5179+
-- The hash changed since the last deploy -> new desired state
5180+
THEN r.status::text
5181+
ELSE
5182+
rps.last_non_deploying_status::text
51645183
END
51655184
) as status
51665185
FROM {cls.table_name()} as r

src/inmanta/data/dataview.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -551,12 +551,21 @@ def get_base_query(self) -> SimpleQueryBuilder:
551551
rps.environment,
552552
(
553553
CASE
554+
-- The resource_persistent_state.last_non_deploying_status column is only populated for
555+
-- actual deployment operations to prevent locking issues. This case-statement calculates
556+
-- the correct state from the combination of the resource table and the
557+
-- resource_persistent_state table.
554558
WHEN r.model < (SELECT version FROM latest_version)
555559
THEN 'orphaned'
556-
WHEN (r.status = 'deploying')
560+
WHEN r.status::text IN('deploying', 'undefined', 'skipped_for_undefined')
561+
-- The deploying, undefined and skipped_for_undefined states are not tracked in the
562+
-- resource_persistent_state table.
557563
THEN r.status::text
558-
ELSE
559-
rps.last_non_deploying_status::text
564+
WHEN rps.last_deployed_attribute_hash != r.attribute_hash
565+
-- The hash changed since the last deploy -> new desired state
566+
THEN r.status::text
567+
-- No override required, use last known state from actual deployment
568+
ELSE rps.last_non_deploying_status::text
560569
END
561570
) as status
562571
FROM versioned_resource_state AS rps

src/inmanta/server/services/resourceservice.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -997,7 +997,11 @@ def convert_legacy_state(
997997
if not is_increment_notification:
998998
if status == ResourceState.deployed:
999999
extra_fields["last_success"] = resource_action.started
1000-
if status != ResourceState.deploying:
1000+
if status not in {
1001+
ResourceState.deploying,
1002+
ResourceState.undefined,
1003+
ResourceState.skipped_for_undefined,
1004+
}:
10011005
extra_fields["last_non_deploying_status"] = const.NonDeployingResourceState(status)
10021006

10031007
await res.update_persistent_state(

tests/server/test_resource_details.py

Lines changed: 158 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,17 @@
1818

1919
import datetime
2020
import itertools
21+
import uuid
2122
from collections import defaultdict
22-
from typing import Any
23+
from typing import Any, Optional
2324
from uuid import UUID
2425

2526
import pytest
2627
from dateutil.tz import UTC
2728

28-
from inmanta import const, data
29+
from inmanta import const, data, util
2930
from inmanta.const import ResourceState
30-
from inmanta.data.model import ResourceVersionIdStr
31+
from inmanta.data.model import ResourceIdStr, ResourceVersionIdStr
3132
from inmanta.util import parse_timestamp
3233

3334

@@ -491,3 +492,157 @@ async def test_resource_details(server, client, env_with_resources):
491492
assert result.result["data"]["status"] == "orphaned"
492493
await assert_matching_attributes(result.result["data"], resources[env.id][orphaned][0])
493494
assert result.result["data"]["requires_status"] == {"std::File[internal,path=/tmp/orphaned_req]": "orphaned"}
495+
496+
497+
async def test_move_to_available_state(server, environment, client, clienthelper, agent):
498+
"""
499+
Verify that the endpoints, that return the state of a resource, return the correct state
500+
when a resource moved back to the available state. This state is not written back to the
501+
resource_persistent_state table and should be determined based on the content of the
502+
resource table.
503+
"""
504+
# Create model version1
505+
version1 = await clienthelper.get_version()
506+
result = await client.put_version(
507+
tid=environment,
508+
version=version1,
509+
resources=[
510+
{
511+
"id": f"std::testing::NullResource[agent1,name=test1],v={version1}",
512+
"val": "val1",
513+
"requires": [],
514+
},
515+
{
516+
"id": f"std::testing::NullResource[agent1,name=test2],v={version1}",
517+
"val": "val2",
518+
"requires": [],
519+
},
520+
{
521+
"id": f"std::testing::NullResource[agent1,name=test3],v={version1}",
522+
"val": "val3",
523+
"requires": [],
524+
},
525+
{
526+
"id": f"std::testing::NullResource[agent1,name=test4],v={version1}",
527+
"val": "val4",
528+
"requires": [],
529+
},
530+
{
531+
"id": f"std::testing::NullResource[agent1,name=test5],v={version1}",
532+
"val": "val5",
533+
"requires": [f"std::testing::NullResource[agent1,name=test4],v={version1}"],
534+
},
535+
],
536+
resource_state={"std::testing::NullResource[agent1,name=test4]": const.ResourceState.undefined},
537+
compiler_version=util.get_compiler_version(),
538+
)
539+
assert result.code == 200, result.result
540+
541+
# Release version
542+
result = await client.release_version(tid=environment, id=version1)
543+
assert result.code == 200
544+
545+
# Move resources
546+
# * std::testing::NullResource[agent1,name=test1]
547+
# * std::testing::NullResource[agent1,name=test2]
548+
# to deployed state
549+
aclient = agent._client
550+
for i in range(1, 3):
551+
action_id = uuid.uuid4()
552+
result = await aclient.resource_deploy_start(
553+
tid=environment,
554+
rvid=f"std::testing::NullResource[agent1,name=test{i}],v={version1}",
555+
action_id=action_id,
556+
)
557+
assert result.code == 200
558+
result = await aclient.resource_deploy_done(
559+
tid=environment,
560+
rvid=f"std::testing::NullResource[agent1,name=test{i}],v={version1}",
561+
action_id=action_id,
562+
status=const.ResourceState.deployed,
563+
)
564+
assert result.code == 200, result.result
565+
566+
# Create a new version containing:
567+
# * an updated desired state for resource std::testing::NullResource[agent1,name=test1]
568+
# * A version of the std::testing::NullResource[agent1,name=test4] that is no longer undefined but available.
569+
version2 = await clienthelper.get_version()
570+
result = await client.put_version(
571+
tid=environment,
572+
version=version2,
573+
resources=[
574+
{
575+
"id": f"std::testing::NullResource[agent1,name=test1],v={version2}",
576+
"val": "val1_updated",
577+
"requires": [],
578+
},
579+
{
580+
"id": f"std::testing::NullResource[agent1,name=test2],v={version2}",
581+
"val": "val2",
582+
"requires": [],
583+
},
584+
{
585+
"id": f"std::testing::NullResource[agent1,name=test3],v={version2}",
586+
"val": "val3",
587+
"requires": [],
588+
},
589+
{
590+
"id": f"std::testing::NullResource[agent1,name=test4],v={version2}",
591+
"val": "val4",
592+
"requires": [],
593+
},
594+
{
595+
"id": f"std::testing::NullResource[agent1,name=test5],v={version2}",
596+
"val": "val5",
597+
"requires": [f"std::testing::NullResource[agent1,name=test4],v={version2}"],
598+
},
599+
],
600+
resource_state={},
601+
compiler_version=util.get_compiler_version(),
602+
)
603+
assert result.code == 200, result.result
604+
605+
async def assert_states(expected_states: dict[ResourceIdStr, const.ResourceState]) -> None:
606+
# Verify behavior of resource_details() endpoint.
607+
for rid, state in expected_states.items():
608+
result = await client.resource_details(tid=environment, rid=rid)
609+
assert result.code == 200
610+
assert (
611+
result.result["data"]["status"] == state.value
612+
), f"Got state {result.result['data']['status']} for resource {rid}, expected {state.value}"
613+
614+
# Verify behavior of get_current_resource_state() endpoint
615+
resource_state: Optional[ResourceState]
616+
for rid, state in expected_states.items():
617+
resource_state = await data.Resource.get_current_resource_state(env=uuid.UUID(environment), rid=rid)
618+
assert resource_state == state
619+
620+
# Verify behavior of resource_list() endpoint
621+
result = await client.resource_list(tid=environment)
622+
assert result.code == 200
623+
actual_states = {r["resource_id"]: const.ResourceState(r["status"]) for r in result.result["data"]}
624+
assert expected_states == actual_states
625+
626+
await assert_states(
627+
{
628+
ResourceIdStr("std::testing::NullResource[agent1,name=test1]"): const.ResourceState.deployed,
629+
ResourceIdStr("std::testing::NullResource[agent1,name=test2]"): const.ResourceState.deployed,
630+
ResourceIdStr("std::testing::NullResource[agent1,name=test3]"): const.ResourceState.available,
631+
ResourceIdStr("std::testing::NullResource[agent1,name=test4]"): const.ResourceState.undefined,
632+
ResourceIdStr("std::testing::NullResource[agent1,name=test5]"): const.ResourceState.skipped_for_undefined,
633+
}
634+
)
635+
636+
# Release version2
637+
result = await client.release_version(tid=environment, id=version2)
638+
assert result.code == 200
639+
640+
await assert_states(
641+
{
642+
ResourceIdStr("std::testing::NullResource[agent1,name=test1]"): const.ResourceState.available,
643+
ResourceIdStr("std::testing::NullResource[agent1,name=test2]"): const.ResourceState.deployed,
644+
ResourceIdStr("std::testing::NullResource[agent1,name=test3]"): const.ResourceState.available,
645+
ResourceIdStr("std::testing::NullResource[agent1,name=test4]"): const.ResourceState.available,
646+
ResourceIdStr("std::testing::NullResource[agent1,name=test5]"): const.ResourceState.available,
647+
}
648+
)

tests/server/test_resource_list.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,17 @@ async def create_resource(
183183
await res.insert()
184184
await res.update_persistent_state(
185185
last_deploy=datetime.now(tz=UTC),
186-
last_non_deploying_status=status if status not in [ResourceState.available, ResourceState.deploying] else None,
186+
last_non_deploying_status=(
187+
status
188+
if status
189+
not in [
190+
ResourceState.available,
191+
ResourceState.deploying,
192+
ResourceState.undefined,
193+
ResourceState.skipped_for_undefined,
194+
]
195+
else None
196+
),
187197
)
188198

189199
await create_resource("agent1", "/etc/file1", "std::File", ResourceState.available, [1, 2, 3])

0 commit comments

Comments
 (0)