qontract-reconcile 0.10.1rc537__py3-none-any.whl → 0.10.1rc539__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {qontract_reconcile-0.10.1rc537.dist-info → qontract_reconcile-0.10.1rc539.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc537.dist-info → qontract_reconcile-0.10.1rc539.dist-info}/RECORD +19 -18
- reconcile/cli.py +33 -0
- reconcile/glitchtip/integration.py +1 -0
- reconcile/gql_definitions/glitchtip/glitchtip_project.py +2 -0
- reconcile/terraform_resources.py +182 -111
- reconcile/test/test_terraform_resources.py +242 -12
- reconcile/utils/early_exit_cache.py +5 -5
- reconcile/utils/extended_early_exit.py +177 -0
- reconcile/utils/external_resources.py +2 -1
- reconcile/utils/glitchtip/models.py +9 -1
- reconcile/utils/metrics.py +7 -0
- reconcile/utils/terraform_client.py +10 -4
- reconcile/utils/terrascript_aws_client.py +11 -3
- tools/qontract_cli.py +17 -17
- tools/test/test_qontract_cli.py +1 -1
- {qontract_reconcile-0.10.1rc537.dist-info → qontract_reconcile-0.10.1rc539.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc537.dist-info → qontract_reconcile-0.10.1rc539.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc537.dist-info → qontract_reconcile-0.10.1rc539.dist-info}/top_level.txt +0 -0
@@ -4,6 +4,7 @@ from collections.abc import (
|
|
4
4
|
Mapping,
|
5
5
|
)
|
6
6
|
from typing import Any
|
7
|
+
from unittest.mock import MagicMock, create_autospec
|
7
8
|
|
8
9
|
import pytest
|
9
10
|
from pytest_mock import MockerFixture
|
@@ -12,6 +13,7 @@ import reconcile.terraform_resources as integ
|
|
12
13
|
from reconcile.gql_definitions.terraform_resources.terraform_resources_namespaces import (
|
13
14
|
NamespaceV1,
|
14
15
|
)
|
16
|
+
from reconcile.utils.secret_reader import SecretReaderBase
|
15
17
|
|
16
18
|
|
17
19
|
def test_cannot_use_exclude_accounts_if_not_dry_run():
|
@@ -71,7 +73,7 @@ def test_cannot_pass_two_aws_account_if_not_dry_run():
|
|
71
73
|
def test_filter_accounts_by_name():
|
72
74
|
accounts = [{"name": "a"}, {"name": "b"}, {"name": "c"}]
|
73
75
|
|
74
|
-
filtered = integ.filter_accounts_by_name(accounts,
|
76
|
+
filtered = integ.filter_accounts_by_name(accounts, names=("a", "b"))
|
75
77
|
|
76
78
|
assert filtered == [{"name": "a"}, {"name": "b"}]
|
77
79
|
|
@@ -79,7 +81,7 @@ def test_filter_accounts_by_name():
|
|
79
81
|
def test_exclude_accounts_by_name():
|
80
82
|
accounts = [{"name": "a"}, {"name": "b"}, {"name": "c"}]
|
81
83
|
|
82
|
-
filtered = integ.exclude_accounts_by_name(accounts,
|
84
|
+
filtered = integ.exclude_accounts_by_name(accounts, names=("a", "b"))
|
83
85
|
|
84
86
|
assert filtered == [{"name": "c"}]
|
85
87
|
|
@@ -258,27 +260,255 @@ def test_filter_tf_namespaces_namespace_deleted(gql_class_factory: Callable):
|
|
258
260
|
assert filtered == [ns2]
|
259
261
|
|
260
262
|
|
261
|
-
def
|
263
|
+
def setup_mocks(
|
264
|
+
mocker: MockerFixture,
|
265
|
+
secret_reader: SecretReaderBase,
|
266
|
+
aws_accounts: list[dict[str, Any]],
|
267
|
+
tf_namespaces: list[NamespaceV1],
|
268
|
+
feature_toggle_state: bool = True,
|
269
|
+
) -> dict[str, Any]:
|
262
270
|
mocked_queries = mocker.patch("reconcile.terraform_resources.queries")
|
263
|
-
mocked_queries.get_aws_accounts.return_value =
|
271
|
+
mocked_queries.get_aws_accounts.return_value = aws_accounts
|
264
272
|
mocked_queries.get_app_interface_settings.return_value = []
|
265
273
|
|
266
|
-
mocker.patch(
|
274
|
+
mocker.patch(
|
275
|
+
"reconcile.terraform_resources.get_namespaces"
|
276
|
+
).return_value = tf_namespaces
|
267
277
|
|
268
|
-
mocked_ts = mocker.patch(
|
269
|
-
|
278
|
+
mocked_ts = mocker.patch(
|
279
|
+
"reconcile.terraform_resources.Terrascript", autospec=True
|
280
|
+
).return_value
|
281
|
+
mocked_ts.resource_spec_inventory = {}
|
270
282
|
|
271
|
-
mocked_tf = mocker.patch(
|
272
|
-
|
273
|
-
|
283
|
+
mocked_tf = mocker.patch(
|
284
|
+
"reconcile.terraform_resources.Terraform", autospec=True
|
285
|
+
).return_value
|
286
|
+
mocked_tf.plan.return_value = (False, None)
|
287
|
+
mocked_tf.should_apply.return_value = False
|
274
288
|
|
275
289
|
mocker.patch("reconcile.terraform_resources.AWSApi", autospec=True)
|
276
|
-
mocker.patch("reconcile.terraform_resources.sys")
|
277
290
|
|
278
291
|
mocked_logging = mocker.patch("reconcile.terraform_resources.logging")
|
279
292
|
|
293
|
+
mocker.patch("reconcile.terraform_resources.get_app_interface_vault_settings")
|
294
|
+
|
295
|
+
mocker.patch(
|
296
|
+
"reconcile.terraform_resources.create_secret_reader",
|
297
|
+
return_value=secret_reader,
|
298
|
+
)
|
299
|
+
|
300
|
+
mock_extended_early_exit_run = mocker.patch(
|
301
|
+
"reconcile.terraform_resources.extended_early_exit_run"
|
302
|
+
)
|
303
|
+
|
304
|
+
get_feature_toggle_state = mocker.patch(
|
305
|
+
"reconcile.terraform_resources.get_feature_toggle_state",
|
306
|
+
return_value=feature_toggle_state,
|
307
|
+
)
|
308
|
+
|
309
|
+
return {
|
310
|
+
"queries": mocked_queries,
|
311
|
+
"ts": mocked_ts,
|
312
|
+
"tf": mocked_tf,
|
313
|
+
"logging": mocked_logging,
|
314
|
+
"extended_early_exit_run": mock_extended_early_exit_run,
|
315
|
+
"get_feature_toggle_state": get_feature_toggle_state,
|
316
|
+
}
|
317
|
+
|
318
|
+
|
319
|
+
def test_empty_run(
|
320
|
+
mocker: MockerFixture,
|
321
|
+
secret_reader: SecretReaderBase,
|
322
|
+
) -> None:
|
323
|
+
mocks = setup_mocks(
|
324
|
+
mocker,
|
325
|
+
secret_reader,
|
326
|
+
aws_accounts=[{"name": "a"}],
|
327
|
+
tf_namespaces=[],
|
328
|
+
)
|
329
|
+
|
280
330
|
integ.run(True, account_name="a")
|
281
331
|
|
282
|
-
|
332
|
+
mocks["logging"].warning.assert_called_once_with(
|
283
333
|
"No terraform namespaces found, consider disabling this integration, account names: a"
|
284
334
|
)
|
335
|
+
|
336
|
+
|
337
|
+
def test_run_with_extended_early_exit_run_enabled(
|
338
|
+
mocker: MockerFixture,
|
339
|
+
secret_reader: SecretReaderBase,
|
340
|
+
) -> None:
|
341
|
+
mocks = setup_mocks(
|
342
|
+
mocker,
|
343
|
+
secret_reader,
|
344
|
+
aws_accounts=[{"name": "a"}],
|
345
|
+
tf_namespaces=[],
|
346
|
+
)
|
347
|
+
defer = MagicMock()
|
348
|
+
expected_runner_params = integ.RunnerParams(
|
349
|
+
accounts=[{"name": "a"}],
|
350
|
+
account_names={"a"},
|
351
|
+
tf_namespaces=[],
|
352
|
+
tf=mocks["tf"],
|
353
|
+
ts=mocks["ts"],
|
354
|
+
secret_reader=secret_reader,
|
355
|
+
dry_run=True,
|
356
|
+
enable_deletion=False,
|
357
|
+
thread_pool_size=10,
|
358
|
+
internal=None,
|
359
|
+
use_jump_host=True,
|
360
|
+
light=False,
|
361
|
+
vault_output_path="",
|
362
|
+
defer=defer,
|
363
|
+
)
|
364
|
+
|
365
|
+
integ.run.__wrapped__(
|
366
|
+
True,
|
367
|
+
account_name="a",
|
368
|
+
enable_extended_early_exit=True,
|
369
|
+
extended_early_exit_cache_ttl_seconds=60,
|
370
|
+
log_cached_log_output=True,
|
371
|
+
defer=defer,
|
372
|
+
)
|
373
|
+
|
374
|
+
mocks["extended_early_exit_run"].assert_called_once_with(
|
375
|
+
integration=integ.QONTRACT_INTEGRATION,
|
376
|
+
integration_version=integ.QONTRACT_INTEGRATION_VERSION,
|
377
|
+
dry_run=True,
|
378
|
+
cache_source=mocks["ts"].terraform_configurations.return_value,
|
379
|
+
ttl_seconds=60,
|
380
|
+
logger=mocks["logging"].getLogger.return_value,
|
381
|
+
runner=integ.runner,
|
382
|
+
runner_params=expected_runner_params,
|
383
|
+
secret_reader=secret_reader,
|
384
|
+
log_cached_log_output=True,
|
385
|
+
)
|
386
|
+
|
387
|
+
|
388
|
+
def test_run_with_extended_early_exit_run_disabled(
|
389
|
+
mocker: MockerFixture,
|
390
|
+
secret_reader: SecretReaderBase,
|
391
|
+
) -> None:
|
392
|
+
mocks = setup_mocks(
|
393
|
+
mocker,
|
394
|
+
secret_reader,
|
395
|
+
aws_accounts=[{"name": "a"}],
|
396
|
+
tf_namespaces=[],
|
397
|
+
)
|
398
|
+
|
399
|
+
integ.run(
|
400
|
+
True,
|
401
|
+
account_name="a",
|
402
|
+
enable_extended_early_exit=False,
|
403
|
+
)
|
404
|
+
|
405
|
+
mocks["extended_early_exit_run"].assert_not_called()
|
406
|
+
mocks["tf"].plan.assert_called_once_with(False)
|
407
|
+
|
408
|
+
|
409
|
+
def test_run_with_extended_early_exit_run_feature_disabled(
|
410
|
+
mocker: MockerFixture,
|
411
|
+
secret_reader: SecretReaderBase,
|
412
|
+
) -> None:
|
413
|
+
mocks = setup_mocks(
|
414
|
+
mocker,
|
415
|
+
secret_reader,
|
416
|
+
aws_accounts=[{"name": "a"}],
|
417
|
+
tf_namespaces=[],
|
418
|
+
feature_toggle_state=False,
|
419
|
+
)
|
420
|
+
|
421
|
+
integ.run(
|
422
|
+
True,
|
423
|
+
account_name="a",
|
424
|
+
enable_extended_early_exit=True,
|
425
|
+
)
|
426
|
+
|
427
|
+
mocks["extended_early_exit_run"].assert_not_called()
|
428
|
+
mocks["tf"].plan.assert_called_once_with(False)
|
429
|
+
mocks["get_feature_toggle_state"].assert_called_once_with(
|
430
|
+
"terraform-resources-extended-early-exit",
|
431
|
+
default=False,
|
432
|
+
)
|
433
|
+
|
434
|
+
|
435
|
+
def test_terraform_resources_runner_dry_run(
|
436
|
+
secret_reader: SecretReaderBase,
|
437
|
+
) -> None:
|
438
|
+
tf = create_autospec(integ.Terraform)
|
439
|
+
tf.plan.return_value = (False, None)
|
440
|
+
|
441
|
+
ts = create_autospec(integ.Terrascript)
|
442
|
+
terraform_configurations = {"a": "b"}
|
443
|
+
ts.terraform_configurations.return_value = terraform_configurations
|
444
|
+
|
445
|
+
defer = MagicMock()
|
446
|
+
|
447
|
+
runner_params = dict(
|
448
|
+
accounts=[{"name": "a"}],
|
449
|
+
account_names={"a"},
|
450
|
+
tf_namespaces=[],
|
451
|
+
tf=tf,
|
452
|
+
ts=ts,
|
453
|
+
secret_reader=secret_reader,
|
454
|
+
dry_run=True,
|
455
|
+
enable_deletion=False,
|
456
|
+
thread_pool_size=10,
|
457
|
+
internal=None,
|
458
|
+
use_jump_host=True,
|
459
|
+
light=False,
|
460
|
+
vault_output_path="",
|
461
|
+
defer=defer,
|
462
|
+
)
|
463
|
+
|
464
|
+
result = integ.runner(**runner_params)
|
465
|
+
|
466
|
+
assert result == integ.ExtendedEarlyExitRunnerResult(
|
467
|
+
payload=terraform_configurations,
|
468
|
+
applied_count=0,
|
469
|
+
)
|
470
|
+
|
471
|
+
|
472
|
+
def test_terraform_resources_runner_no_dry_run(
|
473
|
+
mocker: MockerFixture,
|
474
|
+
secret_reader: SecretReaderBase,
|
475
|
+
) -> None:
|
476
|
+
tf = create_autospec(integ.Terraform)
|
477
|
+
tf.plan.return_value = (False, None)
|
478
|
+
tf.apply_count = 1
|
479
|
+
tf.should_apply.return_value = True
|
480
|
+
tf.apply.return_value = False
|
481
|
+
|
482
|
+
ts = create_autospec(integ.Terrascript)
|
483
|
+
terraform_configurations = {"a": "b"}
|
484
|
+
ts.terraform_configurations.return_value = terraform_configurations
|
485
|
+
ts.resource_spec_inventory = {}
|
486
|
+
|
487
|
+
defer = MagicMock()
|
488
|
+
|
489
|
+
mocked_ob = mocker.patch("reconcile.terraform_resources.ob")
|
490
|
+
mocked_ob.realize_data.return_value = [{"action": "applied"}]
|
491
|
+
|
492
|
+
runner_params = dict(
|
493
|
+
accounts=[{"name": "a"}],
|
494
|
+
account_names={"a"},
|
495
|
+
tf_namespaces=[],
|
496
|
+
tf=tf,
|
497
|
+
ts=ts,
|
498
|
+
secret_reader=secret_reader,
|
499
|
+
dry_run=False,
|
500
|
+
enable_deletion=False,
|
501
|
+
thread_pool_size=10,
|
502
|
+
internal=None,
|
503
|
+
use_jump_host=True,
|
504
|
+
light=False,
|
505
|
+
vault_output_path="",
|
506
|
+
defer=defer,
|
507
|
+
)
|
508
|
+
|
509
|
+
result = integ.runner(**runner_params)
|
510
|
+
|
511
|
+
assert result == integ.ExtendedEarlyExitRunnerResult(
|
512
|
+
payload=terraform_configurations,
|
513
|
+
applied_count=2,
|
514
|
+
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
from datetime import UTC, datetime, timedelta
|
2
2
|
from enum import Enum
|
3
|
-
from typing import Any,
|
3
|
+
from typing import Any, Self
|
4
4
|
|
5
5
|
from deepdiff import DeepHash
|
6
6
|
from pydantic import BaseModel
|
@@ -16,19 +16,19 @@ class CacheKey(BaseModel):
|
|
16
16
|
integration: str
|
17
17
|
integration_version: str
|
18
18
|
dry_run: bool
|
19
|
-
|
19
|
+
cache_source: object
|
20
20
|
|
21
21
|
def __str__(self) -> str:
|
22
22
|
return "/".join([
|
23
23
|
self.integration,
|
24
24
|
self.integration_version,
|
25
25
|
"dry-run" if self.dry_run else "no-dry-run",
|
26
|
-
DeepHash(self.
|
26
|
+
DeepHash(self.cache_source)[self.cache_source],
|
27
27
|
])
|
28
28
|
|
29
29
|
|
30
30
|
class CacheValue(BaseModel):
|
31
|
-
|
31
|
+
payload: object
|
32
32
|
log_output: str
|
33
33
|
applied_count: int
|
34
34
|
|
@@ -46,7 +46,7 @@ class EarlyExitCache:
|
|
46
46
|
@classmethod
|
47
47
|
def build(
|
48
48
|
cls,
|
49
|
-
secret_reader:
|
49
|
+
secret_reader: SecretReaderBase | None = None,
|
50
50
|
) -> Self:
|
51
51
|
state = init_state(STATE_INTEGRATION, secret_reader)
|
52
52
|
return cls(state)
|
@@ -0,0 +1,177 @@
|
|
1
|
+
import logging
|
2
|
+
from collections.abc import Callable, Generator, Mapping
|
3
|
+
from contextlib import contextmanager
|
4
|
+
from io import StringIO
|
5
|
+
from logging import Logger
|
6
|
+
|
7
|
+
from pydantic import BaseModel
|
8
|
+
|
9
|
+
from reconcile.utils.early_exit_cache import (
|
10
|
+
CacheKey,
|
11
|
+
CacheStatus,
|
12
|
+
CacheValue,
|
13
|
+
EarlyExitCache,
|
14
|
+
)
|
15
|
+
from reconcile.utils.metrics import (
|
16
|
+
CounterMetric,
|
17
|
+
GaugeMetric,
|
18
|
+
inc_counter,
|
19
|
+
normalize_integration_name,
|
20
|
+
set_gauge,
|
21
|
+
)
|
22
|
+
from reconcile.utils.secret_reader import SecretReaderBase
|
23
|
+
|
24
|
+
|
25
|
+
class ExtendedEarlyExitRunnerResult(BaseModel):
|
26
|
+
payload: object
|
27
|
+
applied_count: int
|
28
|
+
|
29
|
+
|
30
|
+
class ExtendedEarlyExitBaseMetric(BaseModel):
|
31
|
+
integration: str
|
32
|
+
integration_version: str
|
33
|
+
dry_run: bool
|
34
|
+
cache_status: str
|
35
|
+
|
36
|
+
|
37
|
+
class ExtendedEarlyExitCounter(ExtendedEarlyExitBaseMetric, CounterMetric):
|
38
|
+
@classmethod
|
39
|
+
def name(cls) -> str:
|
40
|
+
return "qontract_reconcile_extended_early_exit"
|
41
|
+
|
42
|
+
|
43
|
+
class ExtendedEarlyExitAppliedCountGauge(ExtendedEarlyExitBaseMetric, GaugeMetric):
|
44
|
+
@classmethod
|
45
|
+
def name(cls) -> str:
|
46
|
+
return "qontract_reconcile_extended_early_exit_applied_count"
|
47
|
+
|
48
|
+
|
49
|
+
def _publish_metrics(
|
50
|
+
cache_key: CacheKey,
|
51
|
+
cache_status: CacheStatus,
|
52
|
+
applied_count: int,
|
53
|
+
) -> None:
|
54
|
+
inc_counter(
|
55
|
+
ExtendedEarlyExitCounter(
|
56
|
+
integration=cache_key.integration,
|
57
|
+
integration_version=cache_key.integration_version,
|
58
|
+
dry_run=cache_key.dry_run,
|
59
|
+
cache_status=cache_status.value,
|
60
|
+
),
|
61
|
+
)
|
62
|
+
set_gauge(
|
63
|
+
ExtendedEarlyExitAppliedCountGauge(
|
64
|
+
integration=cache_key.integration,
|
65
|
+
integration_version=cache_key.integration_version,
|
66
|
+
dry_run=cache_key.dry_run,
|
67
|
+
cache_status=cache_status.value,
|
68
|
+
),
|
69
|
+
applied_count,
|
70
|
+
)
|
71
|
+
|
72
|
+
|
73
|
+
def _ttl_seconds(
|
74
|
+
applied_count: int,
|
75
|
+
ttl_seconds: int,
|
76
|
+
) -> int:
|
77
|
+
"""
|
78
|
+
Pick the ttl based on the applied count.
|
79
|
+
If the applied count is greater than 0, then we want to set ttl to 0 so that the next run will not hit the cache,
|
80
|
+
this will allow us to easy debug reconcile loops, as we will be able to see the logs of the next run,
|
81
|
+
and check cached value for more details.
|
82
|
+
|
83
|
+
:param applied_count: The number of resources that were applied
|
84
|
+
:param ttl_seconds: A ttl in seconds
|
85
|
+
:return: The ttl in seconds
|
86
|
+
"""
|
87
|
+
return 0 if applied_count > 0 else ttl_seconds
|
88
|
+
|
89
|
+
|
90
|
+
@contextmanager
|
91
|
+
def log_stream_handler(
|
92
|
+
logger: Logger,
|
93
|
+
) -> Generator[StringIO, None, None]:
|
94
|
+
"""
|
95
|
+
Add a stream handler to the logger, and return the stream generator, automatically remove the handler when done.
|
96
|
+
|
97
|
+
:param logger: A logger
|
98
|
+
:return: A stream generator
|
99
|
+
"""
|
100
|
+
log_stream = StringIO()
|
101
|
+
log_handler = logging.StreamHandler(log_stream)
|
102
|
+
logger.addHandler(log_handler)
|
103
|
+
try:
|
104
|
+
yield log_stream
|
105
|
+
finally:
|
106
|
+
logger.removeHandler(log_handler)
|
107
|
+
|
108
|
+
|
109
|
+
def extended_early_exit_run(
|
110
|
+
integration: str,
|
111
|
+
integration_version: str,
|
112
|
+
dry_run: bool,
|
113
|
+
cache_source: object,
|
114
|
+
ttl_seconds: int,
|
115
|
+
logger: Logger,
|
116
|
+
runner: Callable[..., ExtendedEarlyExitRunnerResult],
|
117
|
+
runner_params: Mapping | None = None,
|
118
|
+
secret_reader: SecretReaderBase | None = None,
|
119
|
+
log_cached_log_output: bool = False,
|
120
|
+
) -> None:
|
121
|
+
"""
|
122
|
+
Run the runner based on the cache status. Early exit when cache hit.
|
123
|
+
Runner log output will be extracted and stored in cache value,
|
124
|
+
and will be logged when hit if log_cached_log_output is True,
|
125
|
+
this is mainly used to show all log output from different integrations in one place (CI).
|
126
|
+
When runner returns no applies (applied_count is 0), the ttl will be set to ttl_seconds,
|
127
|
+
otherwise it will be set to 0.
|
128
|
+
|
129
|
+
:param integration: The integration name
|
130
|
+
:param integration_version: The integration version
|
131
|
+
:param dry_run: True if the run is in dry run mode, False otherwise
|
132
|
+
:param cache_source: The cache source, usually the static desired state
|
133
|
+
:param ttl_seconds: A ttl in seconds
|
134
|
+
:param logger: A Logger
|
135
|
+
:param runner: A runner can return ExtendedEarlyExitRunnerResult when called
|
136
|
+
:param runner_params: Runner params, will be spread into kwargs when calling runner
|
137
|
+
:param secret_reader: A secret reader
|
138
|
+
:param log_cached_log_output: Whether to log the cached log output when there is a cache hit
|
139
|
+
:return: None
|
140
|
+
"""
|
141
|
+
with EarlyExitCache.build(secret_reader) as cache:
|
142
|
+
key = CacheKey(
|
143
|
+
integration=normalize_integration_name(integration),
|
144
|
+
integration_version=integration_version,
|
145
|
+
dry_run=dry_run,
|
146
|
+
cache_source=cache_source,
|
147
|
+
)
|
148
|
+
cache_status = cache.head(key)
|
149
|
+
logger.debug("Early exit cache status for key=%s: %s", key, cache_status)
|
150
|
+
|
151
|
+
if cache_status == CacheStatus.HIT:
|
152
|
+
if log_cached_log_output:
|
153
|
+
logger.info(cache.get(key).log_output)
|
154
|
+
_publish_metrics(
|
155
|
+
cache_key=key,
|
156
|
+
cache_status=cache_status,
|
157
|
+
applied_count=0,
|
158
|
+
)
|
159
|
+
return
|
160
|
+
|
161
|
+
with log_stream_handler(logger) as log_stream:
|
162
|
+
result = runner(**(runner_params or {}))
|
163
|
+
log_output = log_stream.getvalue()
|
164
|
+
|
165
|
+
value = CacheValue(
|
166
|
+
payload=result.payload,
|
167
|
+
log_output=log_output,
|
168
|
+
applied_count=result.applied_count,
|
169
|
+
)
|
170
|
+
ttl = _ttl_seconds(result.applied_count, ttl_seconds)
|
171
|
+
logger.debug("Set early exit cache for key=%s with ttl=%d", key, ttl)
|
172
|
+
cache.set(key, value, ttl)
|
173
|
+
_publish_metrics(
|
174
|
+
cache_key=key,
|
175
|
+
cache_status=cache_status,
|
176
|
+
applied_count=result.applied_count,
|
177
|
+
)
|
@@ -79,11 +79,12 @@ def get_inventory_count_combinations(
|
|
79
79
|
|
80
80
|
def publish_metrics(inventory: ExternalResourceSpecInventory, integration: str) -> None:
|
81
81
|
count_combinations = get_inventory_count_combinations(inventory)
|
82
|
+
integration_name = metrics.normalize_integration_name(integration)
|
82
83
|
for combination, count in count_combinations.items():
|
83
84
|
provision_provider, provisioner_name, provider = combination
|
84
85
|
metrics.set_gauge(
|
85
86
|
ExternalResourceInventoryGauge(
|
86
|
-
integration=
|
87
|
+
integration=integration_name,
|
87
88
|
provision_provider=provision_provider,
|
88
89
|
provisioner_name=provisioner_name,
|
89
90
|
provider=provider,
|
@@ -154,6 +154,10 @@ class Project(BaseModel):
|
|
154
154
|
platform: Optional[str]
|
155
155
|
teams: list[Team] = []
|
156
156
|
alerts: list[ProjectAlert] = []
|
157
|
+
event_throttle_rate: int = Field(0, alias="eventThrottleRate")
|
158
|
+
|
159
|
+
class Config:
|
160
|
+
allow_population_by_field_name = True
|
157
161
|
|
158
162
|
@root_validator
|
159
163
|
def slugify( # pylint: disable=no-self-argument
|
@@ -174,7 +178,11 @@ class Project(BaseModel):
|
|
174
178
|
return self.slug == other.slug
|
175
179
|
|
176
180
|
def diff(self, other: Project) -> bool:
|
177
|
-
return
|
181
|
+
return (
|
182
|
+
self.name != other.name
|
183
|
+
or self.platform != other.platform
|
184
|
+
or self.event_throttle_rate != other.event_throttle_rate
|
185
|
+
)
|
178
186
|
|
179
187
|
def __hash__(self) -> int:
|
180
188
|
return hash(self.slug)
|
reconcile/utils/metrics.py
CHANGED
@@ -563,3 +563,10 @@ class ErrorRateMetricSet:
|
|
563
563
|
if exc_value:
|
564
564
|
self.fail(exc_value)
|
565
565
|
inc_counter(self._error_counter, by=(1 if self._errors else 0))
|
566
|
+
|
567
|
+
|
568
|
+
def normalize_integration_name(integration: str) -> str:
|
569
|
+
"""
|
570
|
+
Normalize the integration name to be used in prometheus.
|
571
|
+
"""
|
572
|
+
return integration.replace("_", "-")
|
@@ -90,7 +90,7 @@ class TerraformClient: # pylint: disable=too-many-public-methods
|
|
90
90
|
self.thread_pool_size = thread_pool_size
|
91
91
|
self._aws_api = aws_api
|
92
92
|
self._log_lock = Lock()
|
93
|
-
self.
|
93
|
+
self.apply_count = 0
|
94
94
|
|
95
95
|
self.specs: list[TerraformSpec] = []
|
96
96
|
self.init_specs()
|
@@ -111,6 +111,12 @@ class TerraformClient: # pylint: disable=too-many-public-methods
|
|
111
111
|
for account, output in self.outputs.items()
|
112
112
|
}
|
113
113
|
|
114
|
+
def increment_apply_count(self):
|
115
|
+
self.apply_count += 1
|
116
|
+
|
117
|
+
def should_apply(self) -> bool:
|
118
|
+
return self.apply_count > 0
|
119
|
+
|
114
120
|
def get_new_users(self):
|
115
121
|
new_users = []
|
116
122
|
self.init_outputs() # get updated output
|
@@ -282,7 +288,7 @@ class TerraformClient: # pylint: disable=too-many-public-methods
|
|
282
288
|
after = output_change.get("after")
|
283
289
|
if before != after:
|
284
290
|
logging.info(["update", name, "output", output_name])
|
285
|
-
self.
|
291
|
+
self.increment_apply_count()
|
286
292
|
|
287
293
|
# A way to detect deleted outputs is by comparing
|
288
294
|
# the prior state with the output changes.
|
@@ -295,7 +301,7 @@ class TerraformClient: # pylint: disable=too-many-public-methods
|
|
295
301
|
deleted_outputs = [po for po in prior_outputs if po not in output_changes]
|
296
302
|
for output_name in deleted_outputs:
|
297
303
|
logging.info(["delete", name, "output", output_name])
|
298
|
-
self.
|
304
|
+
self.increment_apply_count()
|
299
305
|
|
300
306
|
resource_changes = output.get("resource_changes")
|
301
307
|
if resource_changes is None:
|
@@ -339,7 +345,7 @@ class TerraformClient: # pylint: disable=too-many-public-methods
|
|
339
345
|
resource_name,
|
340
346
|
self._resource_diff_changed_fields(action, resource_change),
|
341
347
|
])
|
342
|
-
self.
|
348
|
+
self.increment_apply_count()
|
343
349
|
if action == "create":
|
344
350
|
if resource_type == "aws_iam_user_login_profile":
|
345
351
|
created_users.append(AccountUser(name, resource_name))
|
@@ -3890,22 +3890,30 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
|
|
3890
3890
|
if os.path.isfile(print_to_file):
|
3891
3891
|
os.remove(print_to_file)
|
3892
3892
|
|
3893
|
-
for name, ts in self.
|
3893
|
+
for name, ts in self.terraform_configurations().items():
|
3894
3894
|
if print_to_file:
|
3895
3895
|
with open(print_to_file, "a", encoding="locale") as f:
|
3896
3896
|
f.write(f"##### {name} #####\n")
|
3897
|
-
f.write(
|
3897
|
+
f.write(ts)
|
3898
3898
|
f.write("\n")
|
3899
3899
|
if existing_dirs is None:
|
3900
3900
|
wd = tempfile.mkdtemp(prefix=TMP_DIR_PREFIX)
|
3901
3901
|
else:
|
3902
3902
|
wd = working_dirs[name]
|
3903
3903
|
with open(wd + "/config.tf.json", "w", encoding="locale") as f:
|
3904
|
-
f.write(
|
3904
|
+
f.write(ts)
|
3905
3905
|
working_dirs[name] = wd
|
3906
3906
|
|
3907
3907
|
return working_dirs
|
3908
3908
|
|
3909
|
+
def terraform_configurations(self) -> dict[str, str]:
|
3910
|
+
"""
|
3911
|
+
Return the Terraform configurations (in JSON format) for each AWS account.
|
3912
|
+
|
3913
|
+
:return: key is AWS account name and value is terraform configuration
|
3914
|
+
"""
|
3915
|
+
return {name: str(ts) for name, ts in self.tss.items()}
|
3916
|
+
|
3909
3917
|
def init_values(self, spec: ExternalResourceSpec, init_tags: bool = True) -> dict:
|
3910
3918
|
"""
|
3911
3919
|
Initialize the values of the terraform resource and merge the defaults and
|