-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathloader.py
More file actions
271 lines (227 loc) · 9.73 KB
/
loader.py
File metadata and controls
271 lines (227 loc) · 9.73 KB
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
#
# Copyright (C) 2023 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import pprint
import yaml
from collections.abc import Sequence
from collections import OrderedDict
try:
from importlib_resources import files # type: ignore
except ImportError:
from importlib.resources import files # type: ignore
from pathlib import Path
from typing import (
Any,
Dict,
Iterator,
List,
Optional,
Union,
)
from charmed_openstack_info.exceptions import CategoryFileNotFound
logger = logging.getLogger(__name__)
class CharmProjectInfo:
"""Represents a CharmProjectInfo.
The CharmProjectInfo is defined in a yaml file and has the following form:
name: the human friendly name of the project
charmhub: the charmhub store name
launchpad: the launchpad project name
team: the team who should own the branches and charm recipes
repo: a URL to the upstream repository to be mirrored in
launchpad
branches: a list of branch -> recipe_info mappings for charm recipes on
launchpad.
The branch_info dictionary consists of the following keys:
* channels (optional) - a list of fully qualified channel names to
publish the charm to after building.
* build-path (optional) - subdirectory within the branch containing
metadata.yaml
* recipe-name (optional) - A string used to format the name of the
recipe. The project name will be passed as 'project', the branch
name will be passed as 'branch', and the track name will be passed
as 'track'. The default recipe-name is '{project}.{branch}.{track}'.
* auto-build (optional) - a boolean indicating whether to automatically
build the charm when the branch changes. Default value is True.
* upload (optional) - a boolean indicating whether to upload to the store
after a charm is built. Default value is True.
* build-channels (optional) - a dictionary indicating which channels
should be used by the launchpad builder for building charms. The
key is the name of the snap or base and the value is the full
channel identifier (e.g. latest/edge). Currently, Launchpad accepts
the following keys: charmcraft, core, core18, core20 and core22.
The following examples provide information for various scenarios.
The following example uses all launchpad builder charm_recipe defaults
publishes the main branch to the latest/edge channel and the stable
branch to the latest/stable channel:
name: Awesome Charm
charmhub: awesome
launchpad: charm-awesome
team: awesome-charmers
repo: https://github.com/canonical/charm-awesome-operator
branches:
main:
channels: latest/edge
stable:
channels: latest/stable
The following example builds a charm using the latest/edge channel of
charmcraft, and does not upload the results to the store
name: Awesome Charm
charmhub: awesome
launchpad: charm-awesome
team: awesome-charmers
repo: https://github.com/canonical/charm-awesome-operator
branches:
main:
store-upload: False
build-channels:
charmcraft: latest/edge
The following example builds a charm on the main branch of the git
repository and publishes the results to the yoga/edge and latest/edge
channels and builds a charm on the stable/xena branch of the git
repository and publishes the results to xena/edge.
name: Awesome Charm
charmhub: awesome
launchpad: charm-awesome
team: awesome-charmers
repo: https://github.com/canonical/charm-awesome-operator
branches:
main:
channels:
- yoga/edge
- latest/edge
stable/xena:
channels:
- xena/edge
"""
def __init__(self, config: Dict[str, Any]):
self.name: str = config.get('name') # type: ignore
self.team: str = config.get('team') # type: ignore
self.charmhub_name: str = config.get('charmhub') # type: ignore
self.launchpad_project: str = config.get('launchpad') # type: ignore
self.repository: str = config.get('repository') # type: ignore
self.branches: Dict[str, Dict[str, Any]] = {}
self._add_branches(config.get('branches', {}))
def _add_branches(self, branches_spec: Dict[str, Dict]) -> None:
default_branch_info = {
'auto-build': True,
'upload': True,
'recipe-name': '{project}.{branch}.{track}'
}
for branch, branch_info in branches_spec.items():
ref = f'refs/heads/{branch}'
if ref not in self.branches:
self.branches[ref] = dict(default_branch_info)
if not isinstance(branch_info, dict):
raise ValueError('Expected a dict for key branches, '
f' instead got {type(branch_info)}')
self.branches[ref].update(branch_info)
def merge(self, config: Dict[str, Any]) -> None:
"""Merge config, by overwriting."""
self.name = config.get('name', self.name)
self.team = config.get('team', self.team)
self.charmhub_name = config.get('charmhub', self.charmhub_name)
self.launchpad_project = config.get('launchpad',
self.launchpad_project)
self.repository = config.get('repository', self.repository)
self._add_branches(config.get('branches', {}))
def is_supported(self, channel="latest/stable"):
"""Is the charm available on the channel supported?"""
class CharmsGroupConfig:
"""Group all the config files and build CharmProjectInfo objects.
This collects together the files passed (which define a charm projects
config and creates CharmProject objects to ensure git repositories and
ensure that the charm builder recipes in launchpad exist with the correct
settings.
"""
def __init__(self,
files: Optional[List[Union[str, Path]]] = None) -> None:
"""Configure the GroupConfig object.
:param files: the list of files to load config from.
"""
self.charm_projects: Dict[str, CharmProjectInfo] = OrderedDict()
if files is not None:
self.load_files(files)
def load_files(
self,
files: Optional[List[Union[str, Path]]] = None,
) -> None:
"""Load the files into the object.
This loads the files, and configures the projects and then creates
CharmProjectInfo objects.
:param files: the list of files to load config from.
"""
assert not isinstance(files, str), "param files must not be str"
assert isinstance(files, Sequence), "Must pass a list or tuple."
for file in files:
with open(file, 'r') as f:
group_config = yaml.safe_load(f)
logger.debug('group_config is: \n%s', pprint.pformat(group_config))
project_defaults = group_config.get('defaults', {})
for project in group_config.get('projects', []):
for key, value in project_defaults.items():
project.setdefault(key, value)
logger.debug('Loaded project %s', project.get('name'))
self.add_charm_project(project)
def add_charm_project(self,
project_config: Dict[str, Any],
merge: bool = False,
) -> None:
"""Add a CharmProjectInfo object from the project specification dict.
:param project: the project to add.
:param merge: if merge is True, merge/overwrite the existing object.
:raises: ValueError if merge is false and the charm project already
exists.
"""
name: str = project_config.get('name') # type: ignore
if name in self.charm_projects:
if merge:
self.charm_projects[name].merge(project_config)
else:
raise ValueError(
f"Project config for '{name}' already exists.")
else:
self.charm_projects[name] = CharmProjectInfo(project_config)
def projects(self, select: Optional[List[str]] = None,
) -> Iterator[CharmProjectInfo]:
"""Generator returns a list of projects."""
if not (select):
select = None
for project in self.charm_projects.values():
if (select is None or
project.launchpad_project in select or
project.charmhub_name in select):
yield project
def find_file(category: str) -> Path:
"""Find the configuration file associated to a category"""
fpath = files(
'charmed_openstack_info.data.lp-builder-config'
).joinpath('%s.yaml' % category)
if not fpath.is_file():
raise CategoryFileNotFound(fpath)
return Path(str(fpath))
def load_file(
fpath: Union[str, Path],
gc: Optional[CharmsGroupConfig] = None,
) -> CharmsGroupConfig:
"""Get a list of CharmProjectInfo objects.
Parses the contents of the file.
:param fpath: path to the configuration file to parse
:returns: list of CharmProjectInfo objects
"""
if not gc:
gc = CharmsGroupConfig()
gc.load_files([fpath])
return gc