qontract-reconcile 0.10.2.dev159__py3-none-any.whl → 0.10.2.dev161__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.2.dev159.dist-info → qontract_reconcile-0.10.2.dev161.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.2.dev159.dist-info → qontract_reconcile-0.10.2.dev161.dist-info}/RECORD +15 -8
- reconcile/aws_cloudwatch_log_retention/integration.py +39 -25
- reconcile/cli.py +4 -6
- reconcile/gcp_image_mirror.py +276 -0
- reconcile/gql_definitions/aws_cloudwatch_log_retention/__init__.py +0 -0
- reconcile/gql_definitions/aws_cloudwatch_log_retention/aws_accounts.py +158 -0
- reconcile/gql_definitions/fragments/container_image_mirror.py +33 -0
- reconcile/gql_definitions/gcp/__init__.py +0 -0
- reconcile/gql_definitions/gcp/gcp_docker_repos.py +128 -0
- reconcile/gql_definitions/gcp/gcp_projects.py +77 -0
- reconcile/gql_definitions/introspection.json +131 -1
- reconcile/typed_queries/aws_cloudwatch_log_retention/aws_accounts.py +12 -0
- reconcile/gcr_mirror.py +0 -278
- {qontract_reconcile-0.10.2.dev159.dist-info → qontract_reconcile-0.10.2.dev161.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.2.dev159.dist-info → qontract_reconcile-0.10.2.dev161.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
"""
|
2
|
+
Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
|
3
|
+
"""
|
4
|
+
from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
|
5
|
+
from datetime import datetime # noqa: F401 # pylint: disable=W0611
|
6
|
+
from enum import Enum # noqa: F401 # pylint: disable=W0611
|
7
|
+
from typing import ( # noqa: F401 # pylint: disable=W0611
|
8
|
+
Any,
|
9
|
+
Optional,
|
10
|
+
Union,
|
11
|
+
)
|
12
|
+
|
13
|
+
from pydantic import ( # noqa: F401 # pylint: disable=W0611
|
14
|
+
BaseModel,
|
15
|
+
Extra,
|
16
|
+
Field,
|
17
|
+
Json,
|
18
|
+
)
|
19
|
+
|
20
|
+
from reconcile.gql_definitions.fragments.vault_secret import VaultSecret
|
21
|
+
|
22
|
+
|
23
|
+
class ConfiguredBaseModel(BaseModel):
|
24
|
+
class Config:
|
25
|
+
smart_union=True
|
26
|
+
extra=Extra.forbid
|
27
|
+
|
28
|
+
|
29
|
+
class ContainerImageMirror(ConfiguredBaseModel):
|
30
|
+
url: str = Field(..., alias="url")
|
31
|
+
pull_credentials: Optional[VaultSecret] = Field(..., alias="pullCredentials")
|
32
|
+
tags: Optional[list[str]] = Field(..., alias="tags")
|
33
|
+
tags_exclude: Optional[list[str]] = Field(..., alias="tagsExclude")
|
File without changes
|
@@ -0,0 +1,128 @@
|
|
1
|
+
"""
|
2
|
+
Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
|
3
|
+
"""
|
4
|
+
from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
|
5
|
+
from datetime import datetime # noqa: F401 # pylint: disable=W0611
|
6
|
+
from enum import Enum # noqa: F401 # pylint: disable=W0611
|
7
|
+
from typing import ( # noqa: F401 # pylint: disable=W0611
|
8
|
+
Any,
|
9
|
+
Optional,
|
10
|
+
Union,
|
11
|
+
)
|
12
|
+
|
13
|
+
from pydantic import ( # noqa: F401 # pylint: disable=W0611
|
14
|
+
BaseModel,
|
15
|
+
Extra,
|
16
|
+
Field,
|
17
|
+
Json,
|
18
|
+
)
|
19
|
+
|
20
|
+
from reconcile.gql_definitions.fragments.container_image_mirror import ContainerImageMirror
|
21
|
+
|
22
|
+
|
23
|
+
DEFINITION = """
|
24
|
+
fragment ContainerImageMirror on ContainerImageMirror_v1 {
|
25
|
+
url
|
26
|
+
pullCredentials {
|
27
|
+
...VaultSecret
|
28
|
+
}
|
29
|
+
tags
|
30
|
+
tagsExclude
|
31
|
+
}
|
32
|
+
|
33
|
+
fragment VaultSecret on VaultSecret_v1 {
|
34
|
+
path
|
35
|
+
field
|
36
|
+
version
|
37
|
+
format
|
38
|
+
}
|
39
|
+
|
40
|
+
query GcpDockerRepos {
|
41
|
+
apps: apps_v1 {
|
42
|
+
gcrRepos {
|
43
|
+
project {
|
44
|
+
name
|
45
|
+
}
|
46
|
+
items {
|
47
|
+
name
|
48
|
+
mirror {
|
49
|
+
...ContainerImageMirror
|
50
|
+
}
|
51
|
+
}
|
52
|
+
}
|
53
|
+
artifactRegistryMirrors {
|
54
|
+
project {
|
55
|
+
name
|
56
|
+
}
|
57
|
+
items {
|
58
|
+
imageURL
|
59
|
+
mirror {
|
60
|
+
...ContainerImageMirror
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|
64
|
+
}
|
65
|
+
}
|
66
|
+
"""
|
67
|
+
|
68
|
+
|
69
|
+
class ConfiguredBaseModel(BaseModel):
|
70
|
+
class Config:
|
71
|
+
smart_union=True
|
72
|
+
extra=Extra.forbid
|
73
|
+
|
74
|
+
|
75
|
+
class GcpProjectV1(ConfiguredBaseModel):
|
76
|
+
name: str = Field(..., alias="name")
|
77
|
+
|
78
|
+
|
79
|
+
class AppGcrReposItemsV1(ConfiguredBaseModel):
|
80
|
+
name: str = Field(..., alias="name")
|
81
|
+
mirror: Optional[ContainerImageMirror] = Field(..., alias="mirror")
|
82
|
+
|
83
|
+
|
84
|
+
class AppGcrReposV1(ConfiguredBaseModel):
|
85
|
+
project: GcpProjectV1 = Field(..., alias="project")
|
86
|
+
items: list[AppGcrReposItemsV1] = Field(..., alias="items")
|
87
|
+
|
88
|
+
|
89
|
+
class AppArtifactRegistryMirrorsV1_GcpProjectV1(ConfiguredBaseModel):
|
90
|
+
name: str = Field(..., alias="name")
|
91
|
+
|
92
|
+
|
93
|
+
class AppArtifactRegistryMirrorsItemsV1(ConfiguredBaseModel):
|
94
|
+
image_url: str = Field(..., alias="imageURL")
|
95
|
+
mirror: ContainerImageMirror = Field(..., alias="mirror")
|
96
|
+
|
97
|
+
|
98
|
+
class AppArtifactRegistryMirrorsV1(ConfiguredBaseModel):
|
99
|
+
project: AppArtifactRegistryMirrorsV1_GcpProjectV1 = Field(..., alias="project")
|
100
|
+
items: list[AppArtifactRegistryMirrorsItemsV1] = Field(..., alias="items")
|
101
|
+
|
102
|
+
|
103
|
+
class AppV1(ConfiguredBaseModel):
|
104
|
+
gcr_repos: Optional[list[AppGcrReposV1]] = Field(..., alias="gcrRepos")
|
105
|
+
artifact_registry_mirrors: Optional[list[AppArtifactRegistryMirrorsV1]] = Field(..., alias="artifactRegistryMirrors")
|
106
|
+
|
107
|
+
|
108
|
+
class GcpDockerReposQueryData(ConfiguredBaseModel):
|
109
|
+
apps: Optional[list[AppV1]] = Field(..., alias="apps")
|
110
|
+
|
111
|
+
|
112
|
+
def query(query_func: Callable, **kwargs: Any) -> GcpDockerReposQueryData:
|
113
|
+
"""
|
114
|
+
This is a convenience function which queries and parses the data into
|
115
|
+
concrete types. It should be compatible with most GQL clients.
|
116
|
+
You do not have to use it to consume the generated data classes.
|
117
|
+
Alternatively, you can also mime and alternate the behavior
|
118
|
+
of this function in the caller.
|
119
|
+
|
120
|
+
Parameters:
|
121
|
+
query_func (Callable): Function which queries your GQL Server
|
122
|
+
kwargs: optional arguments that will be passed to the query function
|
123
|
+
|
124
|
+
Returns:
|
125
|
+
GcpDockerReposQueryData: queried data parsed into generated classes
|
126
|
+
"""
|
127
|
+
raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
|
128
|
+
return GcpDockerReposQueryData(**raw_data)
|
@@ -0,0 +1,77 @@
|
|
1
|
+
"""
|
2
|
+
Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
|
3
|
+
"""
|
4
|
+
from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
|
5
|
+
from datetime import datetime # noqa: F401 # pylint: disable=W0611
|
6
|
+
from enum import Enum # noqa: F401 # pylint: disable=W0611
|
7
|
+
from typing import ( # noqa: F401 # pylint: disable=W0611
|
8
|
+
Any,
|
9
|
+
Optional,
|
10
|
+
Union,
|
11
|
+
)
|
12
|
+
|
13
|
+
from pydantic import ( # noqa: F401 # pylint: disable=W0611
|
14
|
+
BaseModel,
|
15
|
+
Extra,
|
16
|
+
Field,
|
17
|
+
Json,
|
18
|
+
)
|
19
|
+
|
20
|
+
from reconcile.gql_definitions.fragments.vault_secret import VaultSecret
|
21
|
+
|
22
|
+
|
23
|
+
DEFINITION = """
|
24
|
+
fragment VaultSecret on VaultSecret_v1 {
|
25
|
+
path
|
26
|
+
field
|
27
|
+
version
|
28
|
+
format
|
29
|
+
}
|
30
|
+
|
31
|
+
query GcpProjects {
|
32
|
+
gcp_projects: gcp_projects_v1 {
|
33
|
+
name
|
34
|
+
gcrPushCredentials {
|
35
|
+
...VaultSecret
|
36
|
+
}
|
37
|
+
artifactPushCredentials {
|
38
|
+
...VaultSecret
|
39
|
+
}
|
40
|
+
}
|
41
|
+
}
|
42
|
+
"""
|
43
|
+
|
44
|
+
|
45
|
+
class ConfiguredBaseModel(BaseModel):
|
46
|
+
class Config:
|
47
|
+
smart_union=True
|
48
|
+
extra=Extra.forbid
|
49
|
+
|
50
|
+
|
51
|
+
class GcpProjectV1(ConfiguredBaseModel):
|
52
|
+
name: str = Field(..., alias="name")
|
53
|
+
gcr_push_credentials: Optional[VaultSecret] = Field(..., alias="gcrPushCredentials")
|
54
|
+
artifact_push_credentials: VaultSecret = Field(..., alias="artifactPushCredentials")
|
55
|
+
|
56
|
+
|
57
|
+
class GcpProjectsQueryData(ConfiguredBaseModel):
|
58
|
+
gcp_projects: Optional[list[GcpProjectV1]] = Field(..., alias="gcp_projects")
|
59
|
+
|
60
|
+
|
61
|
+
def query(query_func: Callable, **kwargs: Any) -> GcpProjectsQueryData:
|
62
|
+
"""
|
63
|
+
This is a convenience function which queries and parses the data into
|
64
|
+
concrete types. It should be compatible with most GQL clients.
|
65
|
+
You do not have to use it to consume the generated data classes.
|
66
|
+
Alternatively, you can also mime and alternate the behavior
|
67
|
+
of this function in the caller.
|
68
|
+
|
69
|
+
Parameters:
|
70
|
+
query_func (Callable): Function which queries your GQL Server
|
71
|
+
kwargs: optional arguments that will be passed to the query function
|
72
|
+
|
73
|
+
Returns:
|
74
|
+
GcpProjectsQueryData: queried data parsed into generated classes
|
75
|
+
"""
|
76
|
+
raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
|
77
|
+
return GcpProjectsQueryData(**raw_data)
|
@@ -15959,6 +15959,26 @@
|
|
15959
15959
|
"isDeprecated": false,
|
15960
15960
|
"deprecationReason": null
|
15961
15961
|
},
|
15962
|
+
{
|
15963
|
+
"name": "artifactRegistryMirrors",
|
15964
|
+
"description": null,
|
15965
|
+
"args": [],
|
15966
|
+
"type": {
|
15967
|
+
"kind": "LIST",
|
15968
|
+
"name": null,
|
15969
|
+
"ofType": {
|
15970
|
+
"kind": "NON_NULL",
|
15971
|
+
"name": null,
|
15972
|
+
"ofType": {
|
15973
|
+
"kind": "OBJECT",
|
15974
|
+
"name": "AppArtifactRegistryMirrors_v1",
|
15975
|
+
"ofType": null
|
15976
|
+
}
|
15977
|
+
}
|
15978
|
+
},
|
15979
|
+
"isDeprecated": false,
|
15980
|
+
"deprecationReason": null
|
15981
|
+
},
|
15962
15982
|
{
|
15963
15983
|
"name": "quayRepos",
|
15964
15984
|
"description": null,
|
@@ -16816,7 +16836,7 @@
|
|
16816
16836
|
"deprecationReason": null
|
16817
16837
|
},
|
16818
16838
|
{
|
16819
|
-
"name": "
|
16839
|
+
"name": "gcrPushCredentials",
|
16820
16840
|
"description": null,
|
16821
16841
|
"args": [],
|
16822
16842
|
"type": {
|
@@ -16826,6 +16846,22 @@
|
|
16826
16846
|
},
|
16827
16847
|
"isDeprecated": false,
|
16828
16848
|
"deprecationReason": null
|
16849
|
+
},
|
16850
|
+
{
|
16851
|
+
"name": "artifactPushCredentials",
|
16852
|
+
"description": null,
|
16853
|
+
"args": [],
|
16854
|
+
"type": {
|
16855
|
+
"kind": "NON_NULL",
|
16856
|
+
"name": null,
|
16857
|
+
"ofType": {
|
16858
|
+
"kind": "OBJECT",
|
16859
|
+
"name": "VaultSecret_v1",
|
16860
|
+
"ofType": null
|
16861
|
+
}
|
16862
|
+
},
|
16863
|
+
"isDeprecated": false,
|
16864
|
+
"deprecationReason": null
|
16829
16865
|
}
|
16830
16866
|
],
|
16831
16867
|
"inputFields": null,
|
@@ -17027,6 +17063,100 @@
|
|
17027
17063
|
"enumValues": null,
|
17028
17064
|
"possibleTypes": null
|
17029
17065
|
},
|
17066
|
+
{
|
17067
|
+
"kind": "OBJECT",
|
17068
|
+
"name": "AppArtifactRegistryMirrors_v1",
|
17069
|
+
"description": null,
|
17070
|
+
"fields": [
|
17071
|
+
{
|
17072
|
+
"name": "project",
|
17073
|
+
"description": null,
|
17074
|
+
"args": [],
|
17075
|
+
"type": {
|
17076
|
+
"kind": "NON_NULL",
|
17077
|
+
"name": null,
|
17078
|
+
"ofType": {
|
17079
|
+
"kind": "OBJECT",
|
17080
|
+
"name": "GcpProject_v1",
|
17081
|
+
"ofType": null
|
17082
|
+
}
|
17083
|
+
},
|
17084
|
+
"isDeprecated": false,
|
17085
|
+
"deprecationReason": null
|
17086
|
+
},
|
17087
|
+
{
|
17088
|
+
"name": "items",
|
17089
|
+
"description": null,
|
17090
|
+
"args": [],
|
17091
|
+
"type": {
|
17092
|
+
"kind": "NON_NULL",
|
17093
|
+
"name": null,
|
17094
|
+
"ofType": {
|
17095
|
+
"kind": "LIST",
|
17096
|
+
"name": null,
|
17097
|
+
"ofType": {
|
17098
|
+
"kind": "NON_NULL",
|
17099
|
+
"name": null,
|
17100
|
+
"ofType": {
|
17101
|
+
"kind": "OBJECT",
|
17102
|
+
"name": "AppArtifactRegistryMirrorsItems_v1",
|
17103
|
+
"ofType": null
|
17104
|
+
}
|
17105
|
+
}
|
17106
|
+
}
|
17107
|
+
},
|
17108
|
+
"isDeprecated": false,
|
17109
|
+
"deprecationReason": null
|
17110
|
+
}
|
17111
|
+
],
|
17112
|
+
"inputFields": null,
|
17113
|
+
"interfaces": [],
|
17114
|
+
"enumValues": null,
|
17115
|
+
"possibleTypes": null
|
17116
|
+
},
|
17117
|
+
{
|
17118
|
+
"kind": "OBJECT",
|
17119
|
+
"name": "AppArtifactRegistryMirrorsItems_v1",
|
17120
|
+
"description": null,
|
17121
|
+
"fields": [
|
17122
|
+
{
|
17123
|
+
"name": "imageURL",
|
17124
|
+
"description": null,
|
17125
|
+
"args": [],
|
17126
|
+
"type": {
|
17127
|
+
"kind": "NON_NULL",
|
17128
|
+
"name": null,
|
17129
|
+
"ofType": {
|
17130
|
+
"kind": "SCALAR",
|
17131
|
+
"name": "String",
|
17132
|
+
"ofType": null
|
17133
|
+
}
|
17134
|
+
},
|
17135
|
+
"isDeprecated": false,
|
17136
|
+
"deprecationReason": null
|
17137
|
+
},
|
17138
|
+
{
|
17139
|
+
"name": "mirror",
|
17140
|
+
"description": null,
|
17141
|
+
"args": [],
|
17142
|
+
"type": {
|
17143
|
+
"kind": "NON_NULL",
|
17144
|
+
"name": null,
|
17145
|
+
"ofType": {
|
17146
|
+
"kind": "OBJECT",
|
17147
|
+
"name": "ContainerImageMirror_v1",
|
17148
|
+
"ofType": null
|
17149
|
+
}
|
17150
|
+
},
|
17151
|
+
"isDeprecated": false,
|
17152
|
+
"deprecationReason": null
|
17153
|
+
}
|
17154
|
+
],
|
17155
|
+
"inputFields": null,
|
17156
|
+
"interfaces": [],
|
17157
|
+
"enumValues": null,
|
17158
|
+
"possibleTypes": null
|
17159
|
+
},
|
17030
17160
|
{
|
17031
17161
|
"kind": "OBJECT",
|
17032
17162
|
"name": "AppQuayRepos_v1",
|
@@ -0,0 +1,12 @@
|
|
1
|
+
from reconcile.gql_definitions.aws_cloudwatch_log_retention.aws_accounts import (
|
2
|
+
AWSAccountV1,
|
3
|
+
query,
|
4
|
+
)
|
5
|
+
from reconcile.utils.gql import GqlApi
|
6
|
+
|
7
|
+
|
8
|
+
def get_aws_accounts(
|
9
|
+
gql_api: GqlApi,
|
10
|
+
) -> list[AWSAccountV1]:
|
11
|
+
data = query(query_func=gql_api.query)
|
12
|
+
return data.accounts or []
|
reconcile/gcr_mirror.py
DELETED
@@ -1,278 +0,0 @@
|
|
1
|
-
import base64
|
2
|
-
import logging
|
3
|
-
import os
|
4
|
-
import re
|
5
|
-
import tempfile
|
6
|
-
import time
|
7
|
-
from collections import defaultdict
|
8
|
-
from typing import Any, Self
|
9
|
-
|
10
|
-
import requests
|
11
|
-
from sretoolbox.container import (
|
12
|
-
Image,
|
13
|
-
Skopeo,
|
14
|
-
)
|
15
|
-
from sretoolbox.container.image import ImageComparisonError
|
16
|
-
from sretoolbox.container.skopeo import SkopeoCmdError
|
17
|
-
|
18
|
-
from reconcile import queries
|
19
|
-
from reconcile.utils import gql
|
20
|
-
from reconcile.utils.secret_reader import SecretReader
|
21
|
-
|
22
|
-
_LOG = logging.getLogger(__name__)
|
23
|
-
|
24
|
-
QONTRACT_INTEGRATION = "gcr-mirror"
|
25
|
-
REQUEST_TIMEOUT = 60
|
26
|
-
|
27
|
-
|
28
|
-
class QuayMirror:
|
29
|
-
GCR_PROJECT_CATALOG_QUERY = """
|
30
|
-
{
|
31
|
-
projects: gcp_projects_v1 {
|
32
|
-
name
|
33
|
-
pushCredentials {
|
34
|
-
path
|
35
|
-
field
|
36
|
-
version
|
37
|
-
format
|
38
|
-
}
|
39
|
-
}
|
40
|
-
}
|
41
|
-
"""
|
42
|
-
|
43
|
-
GCR_REPOS_QUERY = """
|
44
|
-
{
|
45
|
-
apps: apps_v1 {
|
46
|
-
gcrRepos {
|
47
|
-
project {
|
48
|
-
name
|
49
|
-
}
|
50
|
-
items {
|
51
|
-
name
|
52
|
-
mirror {
|
53
|
-
url
|
54
|
-
pullCredentials {
|
55
|
-
path
|
56
|
-
field
|
57
|
-
version
|
58
|
-
format
|
59
|
-
}
|
60
|
-
tags
|
61
|
-
tagsExclude
|
62
|
-
}
|
63
|
-
}
|
64
|
-
}
|
65
|
-
}
|
66
|
-
}
|
67
|
-
"""
|
68
|
-
|
69
|
-
def __init__(self, dry_run: bool = False) -> None:
|
70
|
-
self.dry_run = dry_run
|
71
|
-
self.gqlapi = gql.get_api()
|
72
|
-
settings = queries.get_app_interface_settings()
|
73
|
-
self.secret_reader = SecretReader(settings=settings)
|
74
|
-
self.skopeo_cli = Skopeo(dry_run)
|
75
|
-
self.push_creds = self._get_push_creds()
|
76
|
-
self.session = requests.Session()
|
77
|
-
|
78
|
-
def __enter__(self) -> Self:
|
79
|
-
return self
|
80
|
-
|
81
|
-
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
|
82
|
-
self.session.close()
|
83
|
-
|
84
|
-
def run(self) -> None:
|
85
|
-
sync_tasks = self.process_sync_tasks()
|
86
|
-
for org, data in sync_tasks.items():
|
87
|
-
for item in data:
|
88
|
-
try:
|
89
|
-
self.skopeo_cli.copy(
|
90
|
-
src_image=item["mirror_url"],
|
91
|
-
src_creds=item["mirror_creds"],
|
92
|
-
dst_image=item["image_url"],
|
93
|
-
dest_creds=self.push_creds[org],
|
94
|
-
)
|
95
|
-
except SkopeoCmdError as details:
|
96
|
-
_LOG.error("[%s]", details)
|
97
|
-
|
98
|
-
def process_repos_query(self) -> dict[str, list[dict[str, Any]]]:
|
99
|
-
result = self.gqlapi.query(self.GCR_REPOS_QUERY)
|
100
|
-
|
101
|
-
summary = defaultdict(list)
|
102
|
-
|
103
|
-
for app in result["apps"]:
|
104
|
-
gcr_repos = app.get("gcrRepos")
|
105
|
-
|
106
|
-
if gcr_repos is None:
|
107
|
-
continue
|
108
|
-
|
109
|
-
for gcr_repo in gcr_repos:
|
110
|
-
project = gcr_repo["project"]["name"]
|
111
|
-
server_url = gcr_repo["project"].get("serverUrl") or "gcr.io"
|
112
|
-
for item in gcr_repo["items"]:
|
113
|
-
if item["mirror"] is None:
|
114
|
-
continue
|
115
|
-
|
116
|
-
summary[project].append({
|
117
|
-
"name": item["name"],
|
118
|
-
"mirror": item["mirror"],
|
119
|
-
"server_url": server_url,
|
120
|
-
})
|
121
|
-
|
122
|
-
return summary
|
123
|
-
|
124
|
-
@staticmethod
|
125
|
-
def sync_tag(
|
126
|
-
tags: list[str] | None, tags_exclude: list[str] | None, candidate: str
|
127
|
-
) -> bool:
|
128
|
-
if tags is not None:
|
129
|
-
# When tags is defined, we don't look at tags_exclude
|
130
|
-
return any(re.match(tag, candidate) for tag in tags)
|
131
|
-
|
132
|
-
if tags_exclude is not None:
|
133
|
-
for tag_exclude in tags_exclude:
|
134
|
-
if re.match(tag_exclude, candidate):
|
135
|
-
return False
|
136
|
-
return True
|
137
|
-
|
138
|
-
# Both tags and tags_exclude are None, so
|
139
|
-
# tag must be synced
|
140
|
-
return True
|
141
|
-
|
142
|
-
def process_sync_tasks(self) -> dict[str, list[dict[str, Any]]]:
|
143
|
-
eight_hours = 28800 # 60 * 60 * 8
|
144
|
-
is_deep_sync = self._is_deep_sync(interval=eight_hours)
|
145
|
-
|
146
|
-
summary = self.process_repos_query()
|
147
|
-
|
148
|
-
sync_tasks = defaultdict(list)
|
149
|
-
for org, data in summary.items():
|
150
|
-
for item in data:
|
151
|
-
image = Image(
|
152
|
-
f"{item['server_url']}/{org}/{item['name']}",
|
153
|
-
session=self.session,
|
154
|
-
timeout=REQUEST_TIMEOUT,
|
155
|
-
)
|
156
|
-
|
157
|
-
mirror_url = item["mirror"]["url"]
|
158
|
-
|
159
|
-
username = None
|
160
|
-
password = None
|
161
|
-
mirror_creds = None
|
162
|
-
if item["mirror"]["pullCredentials"] is not None:
|
163
|
-
pull_credentials = item["mirror"]["pullCredentials"]
|
164
|
-
raw_data = self.secret_reader.read_all(pull_credentials)
|
165
|
-
username = raw_data["user"]
|
166
|
-
password = raw_data["token"]
|
167
|
-
mirror_creds = f"{username}:{password}"
|
168
|
-
|
169
|
-
image_mirror = Image(
|
170
|
-
mirror_url,
|
171
|
-
username=username,
|
172
|
-
password=password,
|
173
|
-
session=self.session,
|
174
|
-
timeout=REQUEST_TIMEOUT,
|
175
|
-
)
|
176
|
-
|
177
|
-
tags = item["mirror"].get("tags")
|
178
|
-
tags_exclude = item["mirror"].get("tagsExclude")
|
179
|
-
|
180
|
-
for tag in image_mirror:
|
181
|
-
if not self.sync_tag(
|
182
|
-
tags=tags, tags_exclude=tags_exclude, candidate=tag
|
183
|
-
):
|
184
|
-
continue
|
185
|
-
|
186
|
-
upstream = image_mirror[tag]
|
187
|
-
downstream = image[tag]
|
188
|
-
if tag not in image:
|
189
|
-
_LOG.debug(
|
190
|
-
"Image %s and mirror %s are out off sync",
|
191
|
-
downstream,
|
192
|
-
upstream,
|
193
|
-
)
|
194
|
-
sync_tasks[org].append({
|
195
|
-
"mirror_url": str(upstream),
|
196
|
-
"mirror_creds": mirror_creds,
|
197
|
-
"image_url": str(downstream),
|
198
|
-
})
|
199
|
-
continue
|
200
|
-
|
201
|
-
# Deep (slow) check only in non dry-run mode
|
202
|
-
if self.dry_run:
|
203
|
-
_LOG.debug(
|
204
|
-
"Image %s and mirror %s are in sync", downstream, upstream
|
205
|
-
)
|
206
|
-
continue
|
207
|
-
|
208
|
-
# Deep (slow) check only from time to time
|
209
|
-
if not is_deep_sync:
|
210
|
-
_LOG.debug(
|
211
|
-
"Image %s and mirror %s are in sync", downstream, upstream
|
212
|
-
)
|
213
|
-
continue
|
214
|
-
|
215
|
-
try:
|
216
|
-
if downstream == upstream:
|
217
|
-
_LOG.debug(
|
218
|
-
"Image %s and mirror %s are in sync",
|
219
|
-
downstream,
|
220
|
-
upstream,
|
221
|
-
)
|
222
|
-
continue
|
223
|
-
except ImageComparisonError as details:
|
224
|
-
_LOG.error("[%s]", details)
|
225
|
-
continue
|
226
|
-
|
227
|
-
_LOG.debug(
|
228
|
-
"Image %s and mirror %s are out of sync", downstream, upstream
|
229
|
-
)
|
230
|
-
sync_tasks[org].append({
|
231
|
-
"mirror_url": str(upstream),
|
232
|
-
"mirror_creds": mirror_creds,
|
233
|
-
"image_url": str(downstream),
|
234
|
-
})
|
235
|
-
|
236
|
-
return sync_tasks
|
237
|
-
|
238
|
-
def _is_deep_sync(self, interval: int) -> bool:
|
239
|
-
control_file_name = "qontract-reconcile-gcr-mirror.timestamp"
|
240
|
-
control_file_path = os.path.join(tempfile.gettempdir(), control_file_name)
|
241
|
-
try:
|
242
|
-
with open(control_file_path, encoding="locale") as file_obj:
|
243
|
-
last_deep_sync = float(file_obj.read())
|
244
|
-
except FileNotFoundError:
|
245
|
-
self._record_timestamp(control_file_path)
|
246
|
-
return True
|
247
|
-
|
248
|
-
next_deep_sync = last_deep_sync + interval
|
249
|
-
if time.time() >= next_deep_sync:
|
250
|
-
self._record_timestamp(control_file_path)
|
251
|
-
return True
|
252
|
-
|
253
|
-
return False
|
254
|
-
|
255
|
-
@staticmethod
|
256
|
-
def _record_timestamp(path: str) -> None:
|
257
|
-
with open(path, "w", encoding="locale") as file_object:
|
258
|
-
file_object.write(str(time.time()))
|
259
|
-
|
260
|
-
def _get_push_creds(self) -> dict[str, str]:
|
261
|
-
result = self.gqlapi.query(self.GCR_PROJECT_CATALOG_QUERY)
|
262
|
-
|
263
|
-
creds = {}
|
264
|
-
for project_data in result["projects"]:
|
265
|
-
push_secret = project_data["pushCredentials"]
|
266
|
-
if push_secret is None:
|
267
|
-
continue
|
268
|
-
|
269
|
-
raw_data = self.secret_reader.read_all(push_secret)
|
270
|
-
project = project_data["name"]
|
271
|
-
token = base64.b64decode(raw_data["token"]).decode()
|
272
|
-
creds[project] = f"{raw_data['user']}:{token}"
|
273
|
-
return creds
|
274
|
-
|
275
|
-
|
276
|
-
def run(dry_run: bool) -> None:
|
277
|
-
with QuayMirror(dry_run) as gcr_mirror:
|
278
|
-
gcr_mirror.run()
|
{qontract_reconcile-0.10.2.dev159.dist-info → qontract_reconcile-0.10.2.dev161.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|