-
Notifications
You must be signed in to change notification settings - Fork 29
/
ctx.py
283 lines (208 loc) · 7.4 KB
/
ctx.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
#
# SPDX-License-Identifier: Apache-2.0
import dataclasses
import functools
import os
import typing
import dacite
import ci.util
'''
Execution context. Filled upon invocation of cli.py, read by submodules
'''
args = None # the parsed command line arguments
cfg: 'GlobalConfig' = None # initialised upon importing this module
@dataclasses.dataclass
class TerminalCfg:
output_columns: typing.Optional[int] = None
terminal_type: typing.Optional[str] = None
@dataclasses.dataclass
class GithubRepoMapping:
repo_url: str
path: str
@dataclasses.dataclass
class CtxCfg:
config_dir: typing.Optional[str] = None # points to "root" cfg-repo dir
delivery_cfg_name: str | None = None
github_repo_mappings: tuple[GithubRepoMapping, ...] = ()
cache_dir: str | None = None # used (e.g.) for caching component-descriptors
ocm_repo_base_url: str | None = None
ocm_repository_mappings: list | None = None
@property
def ocm_repository_lookup(self) -> 'cnudie.retrieve.OcmRepositoryLookup | None':
if not self.ocm_repository_mappings:
return None
import cnudie.util
import ocm
def iter_ocm_repositories(component: ocm.ComponentIdentity, /):
for entry in self.ocm_repository_mappings:
if not entry.prefix:
yield entry.repository
continue
component_name = cnudie.util.to_component_name(component)
if component_name.startswith(entry.prefix):
yield entry.repository
return iter_ocm_repositories
@property
def ocm_lookup(self) -> 'cnudie.retrieve.ComponentDescriptorLookupById | None':
if not self.ocm_repository_lookup:
return None
import cnudie.retrieve
import ccc.oci
import ccc.delivery
return cnudie.retrieve.create_default_component_descriptor_lookup(
ocm_repository_lookup=self.ocm_repository_lookup,
oci_client=ccc.oci.oci_client(),
delivery_client=ccc.delivery.default_client_if_available(),
)
@property
def component_descriptor_cache_dir(self) -> str | None:
if not self.cache_dir:
return None
else:
return os.path.join(self.cache_dir, 'component-descriptors')
def __post_init__(self):
if not self.ocm_repository_mappings:
return
# late import to avoid cyclic imports
import cnudie.retrieve
OcmRepositoryMappingEntry = cnudie.retrieve.OcmRepositoryMappingEntry
self.ocm_repository_mappings = [
dacite.from_dict(
data_class=OcmRepositoryMappingEntry,
data=mapping
) for mapping in self.ocm_repository_mappings
]
@dataclasses.dataclass
class GlobalConfig:
ctx: CtxCfg = dataclasses.field(default_factory=CtxCfg)
terminal: typing.Optional[TerminalCfg] = None
def merge_cfgs(ctor, left, right):
if not left or not right:
return left or right # nothing to merge
left_dict = dataclasses.asdict(left)
# do not overwrite existing values w/ None
def none_or_empty(v):
if v is None or v == () or v == []:
return True
return False
right_dict = {k: v for k,v in dataclasses.asdict(right).items() if not none_or_empty(v)}
merged = ci.util.merge_dicts(left_dict, right_dict)
return dacite.from_dict(
data_class=ctor,
data=merged,
config=dacite.Config(cast=[tuple]),
)
def merge_global_cfg(left: GlobalConfig, right: GlobalConfig):
merged_cfg = GlobalConfig(
ctx=merge_cfgs(CtxCfg, left.ctx, right.ctx),
terminal=merge_cfgs(TerminalCfg, left.terminal, right.terminal),
)
return merged_cfg
def _config_from_env():
env = os.environ
terminal_config = TerminalCfg(
output_columns=env.get('COLUMNS'),
terminal_type=env.get('TERM'),
)
if cfg_dir := env.get('CC_CONFIG_DIR'):
ctx_cfg = CtxCfg(
config_dir=cfg_dir,
)
else:
ctx_cfg = None
return GlobalConfig(
ctx=ctx_cfg,
terminal=terminal_config,
)
def _config_from_fs():
if os.path.isdir('/cc-config'):
return GlobalConfig(ctx=CtxCfg(config_dir='/cc-config'))
return None
def _config_from_user_home():
cfg_file_path = os.path.join(os.path.expanduser('~'), '.cc-utils.cfg')
if not os.path.isfile(cfg_file_path):
return None
raw = ci.util.parse_yaml_file(cfg_file_path) or {}
return dacite.from_dict(
data_class=GlobalConfig,
data=raw,
config=dacite.Config(cast=[tuple]),
)
def _config_from_parsed_argv():
if not args or args.cfg_dir is None:
return None
return GlobalConfig(ctx=CtxCfg(config_dir=args.cfg_dir))
def load_config():
global cfg
cfg = GlobalConfig()
additional_cfgs = (
_config_from_user_home(),
_config_from_env(),
_config_from_fs(),
_config_from_parsed_argv(),
)
for additional_cfg in additional_cfgs:
if not additional_cfg:
continue
cfg = merge_global_cfg(cfg, additional_cfg)
load_config()
def _cfg_factory_from_dir():
if not cfg or not cfg.ctx or not (cfg_dir := cfg.ctx.config_dir):
return None
from ci.util import existing_dir
cfg_dir = existing_dir(cfg_dir)
from model import ConfigFactory
factory = ConfigFactory.from_cfg_dir(cfg_dir=cfg_dir)
return factory
def _secrets_server_client():
import ccc.secrets_server
try:
if bool(args.server_endpoint) ^ bool(args.concourse_cfg_name):
raise ValueError(
'either all or none of server-endpoint and concourse-cfg-name must be set'
)
if args.server_endpoint or args.cache_file:
return ccc.secrets_server.SecretsServerClient(
endpoint_url=args.server_endpoint,
concourse_secret_name=args.concourse_cfg_name,
cache_file=args.cache_file
)
except AttributeError:
pass # ignore
# fall-back to environment variables
exception = None
try:
return ccc.secrets_server.SecretsServerClient.from_env()
except ValueError as ve:
exception = ve
# one last try: use hardcoded default client (will only work if running in
# CI-cluster)
try:
return ccc.secrets_server.SecretsServerClient.default()
except ValueError:
pass
# raise original exception stating missing env-vars
raise exception
def _cfg_factory_from_secrets_server():
import model
raw_dict = _secrets_server_client().retrieve_secrets()
factory = model.ConfigFactory.from_dict(raw_dict)
return factory
@functools.lru_cache()
def cfg_factory():
from ci.util import fail
factory = _cfg_factory_from_dir()
# fallback to secrets-server
if not factory:
factory = _cfg_factory_from_secrets_server()
if not factory:
fail('cfg_factory is required. configure using the global --cfg-dir option or via env')
return factory
@functools.lru_cache()
def cfg_set(name: str=None):
if not name:
if not ci.util._running_on_ci():
raise RuntimeError('current cfg set only available for "central builds"')
name = ci.util.current_config_set_name()
return cfg_factory().cfg_set(name)