cs-image-system-gcloud-runtime-plugin 0.1.4__tar.gz → 0.1.6__tar.gz
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.
- {cs_image_system_gcloud_runtime_plugin-0.1.4 → cs_image_system_gcloud_runtime_plugin-0.1.6}/.gitignore +2 -0
- {cs_image_system_gcloud_runtime_plugin-0.1.4 → cs_image_system_gcloud_runtime_plugin-0.1.6}/PKG-INFO +74 -32
- {cs_image_system_gcloud_runtime_plugin-0.1.4 → cs_image_system_gcloud_runtime_plugin-0.1.6}/README.md +70 -28
- {cs_image_system_gcloud_runtime_plugin-0.1.4 → cs_image_system_gcloud_runtime_plugin-0.1.6}/pyproject.toml +4 -4
- {cs_image_system_gcloud_runtime_plugin-0.1.4 → cs_image_system_gcloud_runtime_plugin-0.1.6}/src/cs_image_system/gcloud_runtime/bootstrap.py +74 -8
- {cs_image_system_gcloud_runtime_plugin-0.1.4 → cs_image_system_gcloud_runtime_plugin-0.1.6}/src/cs_image_system/gcloud_runtime/gcp_provider_specific_image.py +7 -8
- {cs_image_system_gcloud_runtime_plugin-0.1.4 → cs_image_system_gcloud_runtime_plugin-0.1.6}/src/cs_image_system/gcloud_runtime/gcp_runtime_builders.py +33 -6
- {cs_image_system_gcloud_runtime_plugin-0.1.4 → cs_image_system_gcloud_runtime_plugin-0.1.6}/src/cs_image_system/gcloud_runtime/gcp_utils.py +40 -59
- {cs_image_system_gcloud_runtime_plugin-0.1.4 → cs_image_system_gcloud_runtime_plugin-0.1.6}/tests/test_gcp_utils.py +29 -37
- {cs_image_system_gcloud_runtime_plugin-0.1.4 → cs_image_system_gcloud_runtime_plugin-0.1.6}/LICENSE +0 -0
- {cs_image_system_gcloud_runtime_plugin-0.1.4 → cs_image_system_gcloud_runtime_plugin-0.1.6}/src/cs_image_system/gcloud_runtime/__init__.py +0 -0
- {cs_image_system_gcloud_runtime_plugin-0.1.4 → cs_image_system_gcloud_runtime_plugin-0.1.6}/src/cs_image_system/gcloud_runtime/gcp_packer_source.py +0 -0
- {cs_image_system_gcloud_runtime_plugin-0.1.4 → cs_image_system_gcloud_runtime_plugin-0.1.6}/src/cs_image_system/gcloud_runtime/gcp_runtime_models.py +0 -0
- {cs_image_system_gcloud_runtime_plugin-0.1.4 → cs_image_system_gcloud_runtime_plugin-0.1.6}/src/cs_image_system/gcloud_runtime/main.py +0 -0
- {cs_image_system_gcloud_runtime_plugin-0.1.4 → cs_image_system_gcloud_runtime_plugin-0.1.6}/src/cs_image_system/py.typed +0 -0
- {cs_image_system_gcloud_runtime_plugin-0.1.4 → cs_image_system_gcloud_runtime_plugin-0.1.6}/tests/test_gcp_provider_specific_image.py +0 -0
{cs_image_system_gcloud_runtime_plugin-0.1.4 → cs_image_system_gcloud_runtime_plugin-0.1.6}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cs-image-system-gcloud-runtime-plugin
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: The gcloud runtime type: Google Compute Engine as a place where images bake and instances run
|
|
5
5
|
Project-URL: Repository, https://github.com/infrastructurebuilder/cs-image-system-3
|
|
6
6
|
Project-URL: Documentation, https://github.com/infrastructurebuilder/cs-image-system-3/blob/develop/docs/OPERATIONS.md
|
|
@@ -9,9 +9,9 @@ Author-email: Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
|
9
9
|
License-Expression: Apache-2.0
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Python: >=3.13
|
|
12
|
-
Requires-Dist: cs-image-system-base==0.1.
|
|
13
|
-
Requires-Dist: cs-image-system-contract==0.1.
|
|
14
|
-
Requires-Dist: cs-image-system-hashicorp-utils==0.1.
|
|
12
|
+
Requires-Dist: cs-image-system-base==0.1.6
|
|
13
|
+
Requires-Dist: cs-image-system-contract==0.1.6
|
|
14
|
+
Requires-Dist: cs-image-system-hashicorp-utils==0.1.6
|
|
15
15
|
Requires-Dist: google-api-core>=2.31
|
|
16
16
|
Requires-Dist: google-auth>=2.55
|
|
17
17
|
Requires-Dist: google-cloud-compute>=1.49
|
|
@@ -86,7 +86,12 @@ cs_image_system.gcloud_runtime.bootstrap:gcp_section`. It owns the
|
|
|
86
86
|
questions and the terraform for this cloud's one-time initialisation --
|
|
87
87
|
the workload identity pool and GitHub provider of CI_SETUP.md 3.4, the
|
|
88
88
|
READ-ONLY service account and the optional WRITE one, their project
|
|
89
|
-
roles and `workloadIdentityUser` bindings
|
|
89
|
+
roles and `workloadIdentityUser` bindings, and since hygiene XX the GCS
|
|
90
|
+
state bucket when the tree keeps its state in GCS and the project lacks
|
|
91
|
+
it (versioned, uniform access, no public access, the CI identities'
|
|
92
|
+
object access; managed from then on, with the AWS section's fork)
|
|
93
|
+
(`tfmodules/bootstrap_gcp`). The section renders its coverage audit for
|
|
94
|
+
the generated README: what it made, read, or leaves by hand and why.
|
|
90
95
|
The section is wanted by default where the tree declares a GCE runtime
|
|
91
96
|
or a `gcs` state backend. Its defaults come from the raw
|
|
92
97
|
`cfg/runtime-builders.yml` and `cfg/state-backends.yml` (a starter's
|
|
@@ -291,46 +296,83 @@ firewall rule targets logs a warning.
|
|
|
291
296
|
|
|
292
297
|
`query_provider_image()` takes one OS-builder runtime entry
|
|
293
298
|
(`OSBuilderBaseImageBuilderSubconfig` in
|
|
294
|
-
[os_builder_runtime_config.py](../base/src/cs_image_system/base/models/os_builder_runtime_config.py))
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
`
|
|
304
|
-
|
|
299
|
+
[os_builder_runtime_config.py](../base/src/cs_image_system/base/models/os_builder_runtime_config.py))
|
|
300
|
+
and asks it for its effective query, `entry.image_query()`: an
|
|
301
|
+
`ImageQuery` in the SYSTEM's vocabulary
|
|
302
|
+
([contract/image.py](../contract/src/cs_image_system/contract/image.py);
|
|
303
|
+
stage 106). That query is the image-source catalog's `gcloud` block for
|
|
304
|
+
the OS builder's family (the release's layer under the tree's
|
|
305
|
+
`cfg/image-sources*.yml`), with the OS builder's own `query` landing on it
|
|
306
|
+
key by key and the runtime entry's landing on that; the publishers
|
|
307
|
+
declared anywhere (the OS builder's `owners`, this runtime's
|
|
308
|
+
`default_owners`, the entry's `owners`) replace the catalog's. The builder
|
|
309
|
+
translates it with `translate_image_query()` (the table below) into
|
|
310
|
+
`projects` and a Compute `list()` filter expression; `query_image()` in
|
|
311
|
+
[gcp_utils.py](src/cs_image_system/gcloud_runtime/gcp_utils.py) resolves
|
|
312
|
+
the projects (`resolve_projects()`: `self` becomes the session project;
|
|
313
|
+
the aliases `debian`, `ubuntu`, `rhel`, `centos`, `rocky`, `fedora`,
|
|
314
|
+
`suse`, `windows`, `cos` and `google` become the public image projects;
|
|
315
|
+
AWS-only aliases and anything that is not a valid project id are dropped;
|
|
316
|
+
a valid project id such as `almalinux-cloud` is kept as is), lists every
|
|
317
|
+
candidate project, and returns the newest `creation_timestamp` with the
|
|
318
|
+
owning `project` added; with `newest: false` more than one match raises
|
|
319
|
+
`ImageQueryError` as ambiguous. The builder returns `(name, project,
|
|
320
|
+
raw)`; the base registers a `GcpProviderSpecificImage` in the resolved
|
|
321
|
+
state. `validate` has already refused an entry whose query uses a key this
|
|
322
|
+
runtime cannot translate (the EC2 particulars, which this runtime used to
|
|
323
|
+
drop with a debug line) or has no source at all, and has printed every
|
|
324
|
+
entry's query in both vocabularies.
|
|
325
|
+
|
|
326
|
+
### Image query translation
|
|
327
|
+
|
|
328
|
+
What `translate_image_query()` does with each key of the system's
|
|
329
|
+
vocabulary (`supported_query_keys()` is the set of rows not marked
|
|
330
|
+
refused; [tests/test_docs_image_sources.py](../../tests/test_docs_image_sources.py)
|
|
331
|
+
holds this table to it). The answer is `{"projects": [...], "terms":
|
|
332
|
+
{<image field>: value}, "filter": "<the list() expression>",
|
|
333
|
+
"most_recent": bool}`.
|
|
334
|
+
|
|
335
|
+
| System key | Compute Engine field | Note |
|
|
336
|
+
| --- | --- | --- |
|
|
337
|
+
| `name` | `name` | a glob over the image name |
|
|
338
|
+
| `family` | `family` | the vendor's image family; the latest READY image of it is the match |
|
|
339
|
+
| `version` | refused | the catalog fills `{version}` into `name` or `family` |
|
|
340
|
+
| `architecture` | `architecture` | `x86_64` becomes `X86_64`, `arm64` becomes `ARM64` |
|
|
341
|
+
| `publisher` | the projects | project ids and the aliases above |
|
|
342
|
+
| `state` | `status` | `available` becomes `READY`, forced when unset |
|
|
343
|
+
| `virtualization` | refused | an EC2 particular |
|
|
344
|
+
| `root_device` | refused | an EC2 particular |
|
|
345
|
+
| `tags` | `labels.<key>` | one term per tag |
|
|
346
|
+
| `newest` | `most_recent` | true when unset; false refuses more than one match |
|
|
347
|
+
| `native` | verbatim | each key an image field, passed as written into the expression |
|
|
305
348
|
|
|
306
349
|
When the entry declares `image_id` (stage 63), the base's resolve step
|
|
307
350
|
calls `query_provider_image_by_id(entry, image_id)` instead. A GCE image
|
|
308
351
|
is named uniquely within its project, so the id is an image name and is
|
|
309
|
-
searched where the vendor query would search:
|
|
310
|
-
|
|
311
|
-
`
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
base bake reproducible: the same vendor image every time, where the query
|
|
320
|
-
takes the newest match (a vendor family publishes a new image often).
|
|
352
|
+
searched where the vendor query would search: the effective query's
|
|
353
|
+
publishers as the projects, and the filter `name = "<id>"`; a match whose
|
|
354
|
+
status is not `READY` is refused by name (stage 67: `GCP runtime <rt>:
|
|
355
|
+
pinned image '<id>' is <status>, not READY; a bake from it would fail`).
|
|
356
|
+
The owning project is read from the result as for the query (else
|
|
357
|
+
`self`). No match returns `None`, and the base stops resolution with `OS
|
|
358
|
+
builder <name>: image_id '<id>' on runtime <rt> is not known to the
|
|
359
|
+
provider`. Pinning makes the base bake reproducible: the same vendor image
|
|
360
|
+
every time, where the query takes the newest match (a vendor family
|
|
361
|
+
publishes a new image often).
|
|
321
362
|
|
|
322
363
|
`GcpProviderSpecificImage.get_query_assets()` describes the image as a
|
|
323
364
|
generic query (`filters = { name = "<name>" }` plus `owners` when resolved;
|
|
324
|
-
|
|
325
|
-
|
|
365
|
+
when deferred, an `ImageQuery` for `<pattern>*` from `self`, translated
|
|
366
|
+
and quoted: `owners = ["self"]`, `filters = { name = "<pattern>*" }`,
|
|
367
|
+
`most_recent`). The packer source does not use these assets: GCE's image
|
|
326
368
|
families are the series, so `googlecompute_source()` writes `source_image`
|
|
327
369
|
(a resolved or pinned parent) or `source_image_family` (a deferred parent)
|
|
328
370
|
directly.
|
|
329
371
|
|
|
330
372
|
[tests/test_gcp_utils.py](tests/test_gcp_utils.py) and
|
|
331
373
|
[tests/test_gcp_provider_specific_image.py](tests/test_gcp_provider_specific_image.py)
|
|
332
|
-
pin the
|
|
333
|
-
shapes without network access.
|
|
374
|
+
pin the translation, the project resolution, the newest-wins rule and the
|
|
375
|
+
asset shapes without network access.
|
|
334
376
|
|
|
335
377
|
### Image families
|
|
336
378
|
|
|
@@ -65,7 +65,12 @@ cs_image_system.gcloud_runtime.bootstrap:gcp_section`. It owns the
|
|
|
65
65
|
questions and the terraform for this cloud's one-time initialisation --
|
|
66
66
|
the workload identity pool and GitHub provider of CI_SETUP.md 3.4, the
|
|
67
67
|
READ-ONLY service account and the optional WRITE one, their project
|
|
68
|
-
roles and `workloadIdentityUser` bindings
|
|
68
|
+
roles and `workloadIdentityUser` bindings, and since hygiene XX the GCS
|
|
69
|
+
state bucket when the tree keeps its state in GCS and the project lacks
|
|
70
|
+
it (versioned, uniform access, no public access, the CI identities'
|
|
71
|
+
object access; managed from then on, with the AWS section's fork)
|
|
72
|
+
(`tfmodules/bootstrap_gcp`). The section renders its coverage audit for
|
|
73
|
+
the generated README: what it made, read, or leaves by hand and why.
|
|
69
74
|
The section is wanted by default where the tree declares a GCE runtime
|
|
70
75
|
or a `gcs` state backend. Its defaults come from the raw
|
|
71
76
|
`cfg/runtime-builders.yml` and `cfg/state-backends.yml` (a starter's
|
|
@@ -270,46 +275,83 @@ firewall rule targets logs a warning.
|
|
|
270
275
|
|
|
271
276
|
`query_provider_image()` takes one OS-builder runtime entry
|
|
272
277
|
(`OSBuilderBaseImageBuilderSubconfig` in
|
|
273
|
-
[os_builder_runtime_config.py](../base/src/cs_image_system/base/models/os_builder_runtime_config.py))
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
`
|
|
283
|
-
|
|
278
|
+
[os_builder_runtime_config.py](../base/src/cs_image_system/base/models/os_builder_runtime_config.py))
|
|
279
|
+
and asks it for its effective query, `entry.image_query()`: an
|
|
280
|
+
`ImageQuery` in the SYSTEM's vocabulary
|
|
281
|
+
([contract/image.py](../contract/src/cs_image_system/contract/image.py);
|
|
282
|
+
stage 106). That query is the image-source catalog's `gcloud` block for
|
|
283
|
+
the OS builder's family (the release's layer under the tree's
|
|
284
|
+
`cfg/image-sources*.yml`), with the OS builder's own `query` landing on it
|
|
285
|
+
key by key and the runtime entry's landing on that; the publishers
|
|
286
|
+
declared anywhere (the OS builder's `owners`, this runtime's
|
|
287
|
+
`default_owners`, the entry's `owners`) replace the catalog's. The builder
|
|
288
|
+
translates it with `translate_image_query()` (the table below) into
|
|
289
|
+
`projects` and a Compute `list()` filter expression; `query_image()` in
|
|
290
|
+
[gcp_utils.py](src/cs_image_system/gcloud_runtime/gcp_utils.py) resolves
|
|
291
|
+
the projects (`resolve_projects()`: `self` becomes the session project;
|
|
292
|
+
the aliases `debian`, `ubuntu`, `rhel`, `centos`, `rocky`, `fedora`,
|
|
293
|
+
`suse`, `windows`, `cos` and `google` become the public image projects;
|
|
294
|
+
AWS-only aliases and anything that is not a valid project id are dropped;
|
|
295
|
+
a valid project id such as `almalinux-cloud` is kept as is), lists every
|
|
296
|
+
candidate project, and returns the newest `creation_timestamp` with the
|
|
297
|
+
owning `project` added; with `newest: false` more than one match raises
|
|
298
|
+
`ImageQueryError` as ambiguous. The builder returns `(name, project,
|
|
299
|
+
raw)`; the base registers a `GcpProviderSpecificImage` in the resolved
|
|
300
|
+
state. `validate` has already refused an entry whose query uses a key this
|
|
301
|
+
runtime cannot translate (the EC2 particulars, which this runtime used to
|
|
302
|
+
drop with a debug line) or has no source at all, and has printed every
|
|
303
|
+
entry's query in both vocabularies.
|
|
304
|
+
|
|
305
|
+
### Image query translation
|
|
306
|
+
|
|
307
|
+
What `translate_image_query()` does with each key of the system's
|
|
308
|
+
vocabulary (`supported_query_keys()` is the set of rows not marked
|
|
309
|
+
refused; [tests/test_docs_image_sources.py](../../tests/test_docs_image_sources.py)
|
|
310
|
+
holds this table to it). The answer is `{"projects": [...], "terms":
|
|
311
|
+
{<image field>: value}, "filter": "<the list() expression>",
|
|
312
|
+
"most_recent": bool}`.
|
|
313
|
+
|
|
314
|
+
| System key | Compute Engine field | Note |
|
|
315
|
+
| --- | --- | --- |
|
|
316
|
+
| `name` | `name` | a glob over the image name |
|
|
317
|
+
| `family` | `family` | the vendor's image family; the latest READY image of it is the match |
|
|
318
|
+
| `version` | refused | the catalog fills `{version}` into `name` or `family` |
|
|
319
|
+
| `architecture` | `architecture` | `x86_64` becomes `X86_64`, `arm64` becomes `ARM64` |
|
|
320
|
+
| `publisher` | the projects | project ids and the aliases above |
|
|
321
|
+
| `state` | `status` | `available` becomes `READY`, forced when unset |
|
|
322
|
+
| `virtualization` | refused | an EC2 particular |
|
|
323
|
+
| `root_device` | refused | an EC2 particular |
|
|
324
|
+
| `tags` | `labels.<key>` | one term per tag |
|
|
325
|
+
| `newest` | `most_recent` | true when unset; false refuses more than one match |
|
|
326
|
+
| `native` | verbatim | each key an image field, passed as written into the expression |
|
|
284
327
|
|
|
285
328
|
When the entry declares `image_id` (stage 63), the base's resolve step
|
|
286
329
|
calls `query_provider_image_by_id(entry, image_id)` instead. A GCE image
|
|
287
330
|
is named uniquely within its project, so the id is an image name and is
|
|
288
|
-
searched where the vendor query would search:
|
|
289
|
-
|
|
290
|
-
`
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
base bake reproducible: the same vendor image every time, where the query
|
|
299
|
-
takes the newest match (a vendor family publishes a new image often).
|
|
331
|
+
searched where the vendor query would search: the effective query's
|
|
332
|
+
publishers as the projects, and the filter `name = "<id>"`; a match whose
|
|
333
|
+
status is not `READY` is refused by name (stage 67: `GCP runtime <rt>:
|
|
334
|
+
pinned image '<id>' is <status>, not READY; a bake from it would fail`).
|
|
335
|
+
The owning project is read from the result as for the query (else
|
|
336
|
+
`self`). No match returns `None`, and the base stops resolution with `OS
|
|
337
|
+
builder <name>: image_id '<id>' on runtime <rt> is not known to the
|
|
338
|
+
provider`. Pinning makes the base bake reproducible: the same vendor image
|
|
339
|
+
every time, where the query takes the newest match (a vendor family
|
|
340
|
+
publishes a new image often).
|
|
300
341
|
|
|
301
342
|
`GcpProviderSpecificImage.get_query_assets()` describes the image as a
|
|
302
343
|
generic query (`filters = { name = "<name>" }` plus `owners` when resolved;
|
|
303
|
-
|
|
304
|
-
|
|
344
|
+
when deferred, an `ImageQuery` for `<pattern>*` from `self`, translated
|
|
345
|
+
and quoted: `owners = ["self"]`, `filters = { name = "<pattern>*" }`,
|
|
346
|
+
`most_recent`). The packer source does not use these assets: GCE's image
|
|
305
347
|
families are the series, so `googlecompute_source()` writes `source_image`
|
|
306
348
|
(a resolved or pinned parent) or `source_image_family` (a deferred parent)
|
|
307
349
|
directly.
|
|
308
350
|
|
|
309
351
|
[tests/test_gcp_utils.py](tests/test_gcp_utils.py) and
|
|
310
352
|
[tests/test_gcp_provider_specific_image.py](tests/test_gcp_provider_specific_image.py)
|
|
311
|
-
pin the
|
|
312
|
-
shapes without network access.
|
|
353
|
+
pin the translation, the project resolution, the newest-wins rule and the
|
|
354
|
+
asset shapes without network access.
|
|
313
355
|
|
|
314
356
|
### Image families
|
|
315
357
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cs-image-system-gcloud-runtime-plugin"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.6"
|
|
8
8
|
description = "The gcloud runtime type: Google Compute Engine as a place where images bake and instances run"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Mykel Alvis", email = "mykelalvis@infrastructurebuilder.org" }]
|
|
@@ -12,9 +12,9 @@ license = "Apache-2.0"
|
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
requires-python = ">=3.13"
|
|
14
14
|
dependencies = [
|
|
15
|
-
"cs-image-system-contract==0.1.
|
|
16
|
-
"cs-image-system-base==0.1.
|
|
17
|
-
"cs-image-system-hashicorp-utils==0.1.
|
|
15
|
+
"cs-image-system-contract==0.1.6",
|
|
16
|
+
"cs-image-system-base==0.1.6",
|
|
17
|
+
"cs-image-system-hashicorp-utils==0.1.6",
|
|
18
18
|
"google-cloud-compute>=1.49",
|
|
19
19
|
"google-api-core>=2.31",
|
|
20
20
|
"google-auth>=2.55",
|
|
@@ -32,7 +32,7 @@ import json
|
|
|
32
32
|
from typing import Any
|
|
33
33
|
|
|
34
34
|
from cs_image_system.base.bootstrap.facts import Facts, default_state_backend, first_runtime_name, real, runtime_of_type
|
|
35
|
-
from cs_image_system.base.bootstrap.questions import Answers, Question, Raw, Rendered, Secret, Section
|
|
35
|
+
from cs_image_system.base.bootstrap.questions import BY_HAND, MADE, READ, Answers, Question, Raw, Rendered, Secret, Section
|
|
36
36
|
|
|
37
37
|
MODULE = "bootstrap_gcp"
|
|
38
38
|
ISSUER = "https://token.actions.githubusercontent.com"
|
|
@@ -143,6 +143,13 @@ def _bucket_exists(facts: Facts, answers: Answers) -> bool | None:
|
|
|
143
143
|
return _describe(facts, "storage", "buckets", "describe", f"gs://{answers.get('state_bucket')}")[0]
|
|
144
144
|
|
|
145
145
|
|
|
146
|
+
def _bucket_location_default(facts: Facts, answers: Answers) -> str:
|
|
147
|
+
"""The region of the GCE runtime's zone (``us-east1-b`` -> ``us-east1``),
|
|
148
|
+
else the multi-region ``US``."""
|
|
149
|
+
zone = real(_runtime(facts).get("zone")) or ""
|
|
150
|
+
return zone.rsplit("-", 1)[0] if zone.count("-") >= 2 else "US"
|
|
151
|
+
|
|
152
|
+
|
|
146
153
|
def _roles_default(exists_key: str, roles: str):
|
|
147
154
|
"""The roles a NEW account is granted; none for one that exists."""
|
|
148
155
|
def default(facts: Facts, answers: Answers) -> str:
|
|
@@ -213,7 +220,15 @@ QUESTIONS: tuple[Question, ...] = (
|
|
|
213
220
|
default=lambda f, a: _gcs(f, "prefix").strip("/") or None,
|
|
214
221
|
help="the default GCS backend's `prefix` in cfg/state-backends.yml"),
|
|
215
222
|
Question("state_bucket_exists", "Does that bucket already exist?", kind="bool",
|
|
216
|
-
when=lambda a: bool(a.get("state_bucket")), default=_bucket_exists, help=UNASKABLE),
|
|
223
|
+
when=lambda a: bool(a.get("state_bucket")), default=_bucket_exists, help=UNASKABLE, observed=True),
|
|
224
|
+
# hygiene XX item 2: the bucket is MADE when absent, as the AWS section
|
|
225
|
+
# makes its own; whether this root manages it is a choice recorded once
|
|
226
|
+
Question("state_bucket_managed", "Does this root MANAGE the state bucket: made by an apply of this root and kept "
|
|
227
|
+
"here, so a destroy of the root removes it? (a bucket that stood before this root is read, never "
|
|
228
|
+
"managed; one this root makes is managed from then on)", kind="bool",
|
|
229
|
+
when=lambda a: bool(a.get("state_bucket")), default=lambda f, a: not a.get("state_bucket_exists")),
|
|
230
|
+
Question("state_bucket_location", "The bucket's location when this root makes it (a region, or a multi-region "
|
|
231
|
+
"such as US)", when=lambda a: bool(a.get("state_bucket")), default=_bucket_location_default),
|
|
217
232
|
Question("labels", "Labels for what this root makes, as k=v,k=v", default=_labels_default, may_be_empty=True),
|
|
218
233
|
)
|
|
219
234
|
|
|
@@ -257,10 +272,30 @@ def render(answers: Answers) -> Rendered:
|
|
|
257
272
|
"gcp_write_account_exists": bool(answers.get("write_account_exists")) if want_write else False,
|
|
258
273
|
"gcp_write_roles": _split(answers.get("write_roles")) if want_write else [],
|
|
259
274
|
})
|
|
275
|
+
bucket = str(answers.get("state_bucket") or "")
|
|
276
|
+
bucket_exists = bool(answers.get("state_bucket_exists")) if bucket else False
|
|
277
|
+
bucket_managed = (bool(answers.get("state_bucket_managed")) or not bucket_exists) if bucket else False
|
|
278
|
+
bucket_prefix = str(answers.get("state_prefix") or "").strip("/") if bucket else ""
|
|
279
|
+
labels = {k: v for k, v in (p.split("=", 1) for p in _split(answers.get("labels")) if "=" in p)}
|
|
280
|
+
tfvars.update({
|
|
281
|
+
"gcp_state_bucket": bucket,
|
|
282
|
+
"gcp_state_prefix": bucket_prefix,
|
|
283
|
+
"gcp_state_bucket_exists": bucket_exists,
|
|
284
|
+
"gcp_state_bucket_managed": bucket_managed,
|
|
285
|
+
"gcp_state_bucket_location": str(answers.get("state_bucket_location") or "US") if bucket else "US",
|
|
286
|
+
"gcp_labels": labels,
|
|
287
|
+
})
|
|
288
|
+
variables = variables + (
|
|
289
|
+
("gcp_state_bucket", "string", "the GCS state bucket (empty when the tree keeps no state in GCS)"),
|
|
290
|
+
("gcp_state_prefix", "string", "the state prefix within that bucket"),
|
|
291
|
+
("gcp_state_bucket_exists", "bool", "the bucket already stands (observed at every interview; decides the backend)"),
|
|
292
|
+
("gcp_state_bucket_managed", "bool", "this root manages the state bucket: made by an apply of it and kept here"),
|
|
293
|
+
("gcp_state_bucket_location", "string", "the bucket's location when this root makes it"),
|
|
294
|
+
("gcp_labels", "map(string)", "labels on what this root makes that carries labels"),
|
|
295
|
+
)
|
|
260
296
|
backend = None
|
|
261
|
-
if
|
|
262
|
-
backend = ("gcs", {"bucket":
|
|
263
|
-
"prefix": f"{str(answers['state_prefix']).strip('/')}/bootstrap"})
|
|
297
|
+
if bucket and bucket_exists:
|
|
298
|
+
backend = ("gcs", {"bucket": bucket, "prefix": f"{bucket_prefix}/bootstrap"})
|
|
264
299
|
by_hand = ["The network and the firewall rules: the team's, never modified by the system."]
|
|
265
300
|
if provider_exists and not names_repository(condition, answers):
|
|
266
301
|
by_hand.insert(0, f"The existing provider `{answers['provider']}` has the attribute condition `{condition or '(none)'}`, "
|
|
@@ -272,7 +307,6 @@ def render(answers: Answers) -> Rendered:
|
|
|
272
307
|
by_hand.append("No WRITE service account: `GCP_APPLY_SERVICE_ACCOUNT` is set to the READ-ONLY account's "
|
|
273
308
|
"address (CI never writes to a runtime it does not perform on), or delete those lines from "
|
|
274
309
|
"the `perform` job (CI_SETUP.md 3.4 step 4).")
|
|
275
|
-
labels = {k: v for k, v in (p.split("=", 1) for p in _split(answers.get("labels")) if "=" in p)}
|
|
276
310
|
destroy_takes: list[str] = []
|
|
277
311
|
for key, roles in (("read", _split(answers.get("read_roles"))),
|
|
278
312
|
("write", _split(answers.get("write_roles")) if want_write else [])):
|
|
@@ -287,7 +321,9 @@ def render(answers: Answers) -> Rendered:
|
|
|
287
321
|
module_args={**{{"provider": "provider_id"}.get(n, n): Raw(f"var.gcp_{n}") for n in names},
|
|
288
322
|
**{k: Raw(f"var.gcp_{k}") for k in ("principal_value", "attribute_condition", "read_roles",
|
|
289
323
|
"want_write", "write_account", "write_account_exists",
|
|
290
|
-
"write_roles"
|
|
324
|
+
"write_roles", "state_bucket", "state_prefix",
|
|
325
|
+
"state_bucket_exists", "state_bucket_managed",
|
|
326
|
+
"state_bucket_location", "labels")}},
|
|
291
327
|
variables=variables,
|
|
292
328
|
tfvars=tfvars,
|
|
293
329
|
outputs=(
|
|
@@ -297,6 +333,7 @@ def render(answers: Answers) -> Rendered:
|
|
|
297
333
|
"the READ-ONLY service account's address: the GCP_SERVICE_ACCOUNT secret"),
|
|
298
334
|
("gcp_write_service_account", f"module.{MODULE}.write_service_account",
|
|
299
335
|
"the address CI may write as (the READ-ONLY one when there is no WRITE account): the GCP_APPLY_SERVICE_ACCOUNT secret"),
|
|
336
|
+
("gcp_state_bucket", f"module.{MODULE}.state_bucket", "the GCS state bucket, when the tree keeps its state in GCS"),
|
|
300
337
|
),
|
|
301
338
|
required_providers={"google": {"source": "hashicorp/google", "version": ">= 5.0"}},
|
|
302
339
|
provider_blocks=('provider "google" {\n project = var.gcp_project # the credentials: GOOGLE_OAUTH_ACCESS_TOKEN '
|
|
@@ -334,10 +371,39 @@ def render(answers: Answers) -> Rendered:
|
|
|
334
371
|
"every role and binding here is one member added; nothing existing is replaced"
|
|
335
372
|
+ ("; an account that already exists is granted nothing unless you named the roles" if any(
|
|
336
373
|
answers.get(f"{k}_account_exists") for k in ("read", "write")) else ""),
|
|
374
|
+
*((f"the GCS state bucket `{bucket}` "
|
|
375
|
+
+ ("exists and is MANAGED here (made by an earlier apply of this root; a destroy of the root removes it): "
|
|
376
|
+
"this root's state is bound to it (decision D8)" if bucket_exists and bucket_managed else
|
|
377
|
+
"exists: this root's state is bound to it (decision D8); the bucket stood before this root and is not managed here"
|
|
378
|
+
if bucket_exists else
|
|
379
|
+
f"is CREATED here in {answers.get('state_bucket_location') or 'US'} (versioned, uniform access, no public "
|
|
380
|
+
"access; the CI identities get object access on it) and managed from then on: local state for this first apply"),)
|
|
381
|
+
if bucket else ("no GCS state bucket: the tree keeps its state elsewhere",)),
|
|
337
382
|
*((f"labels: {', '.join(f'{k}={v}' for k, v in sorted(labels.items()))} (service accounts carry no labels; "
|
|
338
|
-
"
|
|
383
|
+
"the bucket does, when made here)",) if labels else ()),
|
|
339
384
|
),
|
|
340
385
|
backend=backend,
|
|
386
|
+
coverage=(
|
|
387
|
+
(f"the workload identity pool `{answers['pool']}`", READ if answers["pool_exists"] else MADE,
|
|
388
|
+
"read as it stands" if answers["pool_exists"] else "the project lacked it"),
|
|
389
|
+
(f"the pool's GitHub provider `{answers['provider']}`", READ if provider_exists else MADE,
|
|
390
|
+
"read as it stands, never rewritten: other repositories may share its condition" if provider_exists
|
|
391
|
+
else "the project lacked it: made with a condition admitting this repository alone"),
|
|
392
|
+
(f"the READ-ONLY service account `{answers['read_account']}`",
|
|
393
|
+
READ if answers["read_account_exists"] else MADE,
|
|
394
|
+
"read as it stands; granted only the roles you named" if answers["read_account_exists"] else "the project lacked it"),
|
|
395
|
+
((f"the WRITE service account `{answers.get('write_account')}`",
|
|
396
|
+
READ if answers.get("write_account_exists") else MADE,
|
|
397
|
+
"read as it stands; granted only the roles you named" if answers.get("write_account_exists")
|
|
398
|
+
else "the project lacked it") if want_write
|
|
399
|
+
else ("a WRITE service account", BY_HAND, "not wanted: CI performs on no GCE runtime, so none is made or read")),
|
|
400
|
+
((f"the GCS state bucket `{bucket}`", MADE if bucket_managed else READ,
|
|
401
|
+
"made by an apply of this root and managed from then on, the CI identities' object access included"
|
|
402
|
+
if bucket_managed else "stood before this root: read, never managed")
|
|
403
|
+
if bucket else ("a GCS state bucket", BY_HAND, "the tree declares no gcs backend: nothing to make")),
|
|
404
|
+
("the network and the firewall rules", BY_HAND,
|
|
405
|
+
"the team's, by decision: the system never creates or modifies network configuration"),
|
|
406
|
+
),
|
|
341
407
|
)
|
|
342
408
|
|
|
343
409
|
|
|
@@ -28,11 +28,10 @@ class GcpProviderSpecificImage(ProviderSpecificImage):
|
|
|
28
28
|
ret["owners"] = [f'"{self.owner}"']
|
|
29
29
|
return ret
|
|
30
30
|
# Deferred: find the most recent image matching the output-name pattern
|
|
31
|
-
# produced by a build in this project.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
31
|
+
# produced by a build in this project. stage 106: an ImageQuery in the
|
|
32
|
+
# system's vocabulary, translated like a vendor query, then quoted.
|
|
33
|
+
from cs_image_system.contract.image import ImageQuery
|
|
34
|
+
from .gcp_utils import translate_image_query
|
|
35
|
+
native = translate_image_query(ImageQuery(name=f"{self.deferred_name_pattern}*", publisher=("self",), newest=True))
|
|
36
|
+
filters = {k: f'"{v}"' for k, v in native["terms"].items() if k != "status"}
|
|
37
|
+
return {"owners": [f'"{p}"' for p in native["projects"]], "filters": filters, "most_recent": True}
|
|
@@ -20,7 +20,8 @@ from cs_image_system.contract.core import PluginArtifactProtocol
|
|
|
20
20
|
from .gcp_provider_specific_image import GcpProviderSpecificImage
|
|
21
21
|
from .gcp_runtime_models import GCPCloudBuilderModel
|
|
22
22
|
from cs_image_system.gcloud_runtime import gcp_utils
|
|
23
|
-
from .gcp_utils import
|
|
23
|
+
from .gcp_utils import GCP_QUERY_KEYS, query_image, translate_image_query
|
|
24
|
+
from cs_image_system.contract.image import ImageQuery
|
|
24
25
|
|
|
25
26
|
GCP_RUNTIME: str = "gcloud"
|
|
26
27
|
|
|
@@ -41,12 +42,18 @@ class GCPCloudBuilder(CloudBuilderBase[GCPCloudBuilderModel], PluginArtifactProt
|
|
|
41
42
|
def provider_specific_image_class(self) -> type[ProviderSpecificImage]:
|
|
42
43
|
return GcpProviderSpecificImage
|
|
43
44
|
|
|
45
|
+
# --- the image query (stage 106)
|
|
46
|
+
def supported_query_keys(self) -> frozenset[str]:
|
|
47
|
+
return GCP_QUERY_KEYS
|
|
48
|
+
|
|
49
|
+
def translate_image_query(self, query: ImageQuery) -> dict[str, Any]:
|
|
50
|
+
return translate_image_query(query)
|
|
51
|
+
|
|
44
52
|
def query_provider_image(self, os_builder: OSBuilderBaseImageBuilderSubconfig) -> tuple[str, str, Mapping[str,Any]] | None:
|
|
45
53
|
retval: str | None = None
|
|
46
|
-
q
|
|
54
|
+
q = self.translate_image_query(os_builder.image_query()) if os_builder else {}
|
|
47
55
|
kv = query_image(
|
|
48
56
|
query=q,
|
|
49
|
-
post_query_filter=missed,
|
|
50
57
|
session_config=self.model.self_to_gcp_client_config(),
|
|
51
58
|
)
|
|
52
59
|
if kv:
|
|
@@ -71,9 +78,7 @@ class GCPCloudBuilder(CloudBuilderBase[GCPCloudBuilderModel], PluginArtifactProt
|
|
|
71
78
|
"""The image an entry pins by ``image_id`` (stage 63): a GCE image is
|
|
72
79
|
named uniquely within its project, so the name is searched in the
|
|
73
80
|
entry's owner projects, as the vendor query is."""
|
|
74
|
-
q
|
|
75
|
-
q = {k: v for k, v in q.items() if k not in ("family", "filter")}
|
|
76
|
-
q["filter"] = f'name = "{image_id}"'
|
|
81
|
+
q = {"projects": list(os_builder.image_query().publisher), "filter": f'name = "{image_id}"'}
|
|
77
82
|
kv = query_image(query=q, session_config=self.model.self_to_gcp_client_config())
|
|
78
83
|
if not kv:
|
|
79
84
|
return None
|
|
@@ -155,6 +160,28 @@ class GCPCloudBuilder(CloudBuilderBase[GCPCloudBuilderModel], PluginArtifactProt
|
|
|
155
160
|
return f"GCE label keys must start with a lowercase letter; {key!r} becomes {label!r}"
|
|
156
161
|
return None
|
|
157
162
|
|
|
163
|
+
def image_name_as_made(self, name: str) -> str:
|
|
164
|
+
"""What GCE makes of a build name (stage 105): ``gce_name`` --
|
|
165
|
+
lowercase, dashes for what it cannot keep, a letter first, at most
|
|
166
|
+
63 characters -- the name the packer source writes."""
|
|
167
|
+
from .gcp_packer_source import gce_name
|
|
168
|
+
return gce_name(name)
|
|
169
|
+
|
|
170
|
+
def image_name_problem(self, name: str) -> str | None:
|
|
171
|
+
"""Why a build's full name cannot be a GCE image name (stage 105):
|
|
172
|
+
only its length. ``gce_name`` can lowercase and rewrite, but it
|
|
173
|
+
shortens by cutting the end, where the timestamp is, so a name
|
|
174
|
+
longer than 63 characters once rewritten would lose what keeps
|
|
175
|
+
builds apart."""
|
|
176
|
+
import re
|
|
177
|
+
made = re.sub(r"[^a-z0-9-]+", "-", str(name).lower()).strip("-")
|
|
178
|
+
if made and not made[0].isalpha():
|
|
179
|
+
made = f"i-{made}"
|
|
180
|
+
if len(made) > 63:
|
|
181
|
+
return (f"a GCE image name is at most 63 characters; {name!r} becomes {made!r} "
|
|
182
|
+
f"({len(made)}) and would be cut where the timestamp is")
|
|
183
|
+
return None
|
|
184
|
+
|
|
158
185
|
def one_bake_user_per_chain(self) -> bool:
|
|
159
186
|
return True # finding 43
|
|
160
187
|
|
|
@@ -8,6 +8,8 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
10
|
import logging
|
|
11
|
+
|
|
12
|
+
from cs_image_system.contract.image import ImageQuery
|
|
11
13
|
import re
|
|
12
14
|
from collections.abc import Mapping
|
|
13
15
|
from typing import Any, Sequence, cast
|
|
@@ -16,7 +18,6 @@ from google.api_core import exceptions as gcp_exceptions
|
|
|
16
18
|
from google.cloud import compute_v1
|
|
17
19
|
|
|
18
20
|
from cs_image_system.contract.constants import SELF
|
|
19
|
-
from cs_image_system.base.models.os_builder_runtime_config import OSBuilderBaseImageBuilderSubconfig
|
|
20
21
|
|
|
21
22
|
log = logging.getLogger(__name__)
|
|
22
23
|
|
|
@@ -204,65 +205,42 @@ def build_filter_expression(terms: Mapping[str, Any]) -> str:
|
|
|
204
205
|
return " AND ".join(parts)
|
|
205
206
|
|
|
206
207
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
"""
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
208
|
+
#: stage 106: the keys of the system's image-query vocabulary Compute
|
|
209
|
+
#: Engine's images.list can answer, and the field each becomes.
|
|
210
|
+
GCP_QUERY_KEYS: frozenset[str] = frozenset({"name", "family", "architecture", "publisher", "state", "tags",
|
|
211
|
+
"newest", "native"})
|
|
212
|
+
GCP_FIELD_NAMES: dict[str, str] = {"name": "name", "family": "family", "architecture": "architecture", "state": "status"}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def translate_image_query(query: ImageQuery) -> dict[str, Any]:
|
|
216
|
+
"""The system's query in Compute Engine's words (stage 106):
|
|
217
|
+
``{"projects": [...], "terms": {<image field>: value}, "filter": <the
|
|
218
|
+
list() expression>, "most_recent": bool}``; ``query_image`` takes it.
|
|
219
|
+
``state`` defaults to ``available`` (READY); ``architecture`` and
|
|
220
|
+
``state`` values are mapped; ``tags`` become ``labels.<k>``. A key the
|
|
221
|
+
API cannot answer (the EC2 particulars) raises ValueError naming it
|
|
222
|
+
(``validate`` refuses it first); ``native`` keys are image fields,
|
|
223
|
+
passed verbatim."""
|
|
224
|
+
unsupported = sorted(k for k in query.keys() if k not in GCP_QUERY_KEYS)
|
|
225
|
+
if unsupported:
|
|
226
|
+
raise ValueError(f"GCE cannot translate image query key(s) {', '.join(unsupported)}")
|
|
227
|
+
terms: dict[str, Any] = {}
|
|
228
|
+
for key, name in GCP_FIELD_NAMES.items():
|
|
229
|
+
value = getattr(query, key)
|
|
230
|
+
if value is None:
|
|
225
231
|
continue
|
|
226
|
-
if
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
continue
|
|
239
|
-
field_name = GCP_DI_MAP.get(_k)
|
|
240
|
-
if not field_name:
|
|
241
|
-
missed[_k] = _v
|
|
242
|
-
continue
|
|
243
|
-
if field_name == "status":
|
|
244
|
-
_v = _gcp_status(_v)
|
|
245
|
-
elif field_name == "architecture":
|
|
246
|
-
_v = _gcp_architecture(_v)
|
|
247
|
-
filter_terms[field_name] = _v
|
|
248
|
-
continue
|
|
249
|
-
field_name = GCP_DI_MAP.get(topk)
|
|
250
|
-
if not field_name:
|
|
251
|
-
if topk in AWS_ONLY_QUERY_KEYS:
|
|
252
|
-
log.debug(f"Dropping AWS-only image query key {topk!r} for GCP query.")
|
|
253
|
-
else:
|
|
254
|
-
missed[topk] = topv
|
|
255
|
-
continue
|
|
256
|
-
if field_name == "status":
|
|
257
|
-
topv = _gcp_status(topv)
|
|
258
|
-
elif field_name == "architecture":
|
|
259
|
-
topv = _gcp_architecture(topv)
|
|
260
|
-
filter_terms[field_name] = topv
|
|
261
|
-
|
|
262
|
-
if "status" not in filter_terms:
|
|
263
|
-
filter_terms["status"] = "READY"
|
|
264
|
-
ret["filter"] = build_filter_expression(filter_terms)
|
|
265
|
-
return (ret, missed)
|
|
232
|
+
if name == "status":
|
|
233
|
+
value = _gcp_status(value)
|
|
234
|
+
elif name == "architecture":
|
|
235
|
+
value = _gcp_architecture(value)
|
|
236
|
+
terms[name] = value
|
|
237
|
+
terms.setdefault("status", "READY")
|
|
238
|
+
for k, v in sorted(query.tags.items()):
|
|
239
|
+
terms[f"labels.{k}"] = v
|
|
240
|
+
for k, v in query.native.items():
|
|
241
|
+
terms[str(k)] = v
|
|
242
|
+
return {"projects": list(query.publisher), "terms": terms, "filter": build_filter_expression(terms),
|
|
243
|
+
"most_recent": True if query.newest is None else bool(query.newest)}
|
|
266
244
|
|
|
267
245
|
|
|
268
246
|
def _image_to_dict(image: compute_v1.Image, project: str) -> dict[str, Any]:
|
|
@@ -339,6 +317,7 @@ def query_image(
|
|
|
339
317
|
results: list[dict[str, Any]] = []
|
|
340
318
|
filter_expr = query.get("filter", "")
|
|
341
319
|
family = query.get("family")
|
|
320
|
+
most_recent = query.get("most_recent", True)
|
|
342
321
|
for project in projects:
|
|
343
322
|
if filter_expr:
|
|
344
323
|
results.extend(_query_by_filters(client, project, filter_expr))
|
|
@@ -365,6 +344,8 @@ def query_image(
|
|
|
365
344
|
if not ilist:
|
|
366
345
|
return None
|
|
367
346
|
if len(ilist) > 1:
|
|
347
|
+
if not most_recent:
|
|
348
|
+
raise ImageQueryError(f"{len(ilist)} images match and the query says newest: false; narrow it")
|
|
368
349
|
ilist.sort(key=lambda x: x.get("creation_timestamp", ""), reverse=True)
|
|
369
350
|
|
|
370
351
|
return ilist[0]
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
5
|
"""Unit tests for the GCP-native image query utilities (no network access)."""
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
from google.cloud import compute_v1
|
|
@@ -13,9 +13,10 @@ from cs_image_system.gcloud_runtime.gcp_utils import (
|
|
|
13
13
|
build_filter_expression,
|
|
14
14
|
get_image_owner,
|
|
15
15
|
query_image,
|
|
16
|
-
remap_for_image_query,
|
|
17
16
|
resolve_projects,
|
|
17
|
+
translate_image_query,
|
|
18
18
|
)
|
|
19
|
+
from cs_image_system.contract.image import ImageQuery
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class FakeSubconfig:
|
|
@@ -51,41 +52,32 @@ def _img(name: str, ts: str, **kw: Any) -> compute_v1.Image:
|
|
|
51
52
|
return compute_v1.Image(name=name, creation_timestamp=ts, **kw)
|
|
52
53
|
|
|
53
54
|
|
|
54
|
-
def
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
q,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
q, _ = remap_for_image_query(cast(Any, rc))
|
|
81
|
-
assert '(labels.Environment = "prod")' in q["filter"]
|
|
82
|
-
assert '(status = "READY")' in q["filter"]
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def test_remap_unknown_keys_go_to_missed():
|
|
86
|
-
rc = FakeSubconfig(owners=[], query={"filters": {"totally_custom": "x"}})
|
|
87
|
-
_, missed = remap_for_image_query(cast(Any, rc))
|
|
88
|
-
assert missed == {"totally_custom": "x"}
|
|
55
|
+
def test_translate_puts_the_systems_query_in_compute_engines_words():
|
|
56
|
+
# stage 106: the vocabulary is the system's; the values are mapped
|
|
57
|
+
q = translate_image_query(ImageQuery.from_mapping({
|
|
58
|
+
"publisher": ["self", "rhel-cloud"], "name": "rhel-9-v*", "architecture": "x86_64", "state": "available"}))
|
|
59
|
+
assert q["projects"] == ["self", "rhel-cloud"]
|
|
60
|
+
assert q["terms"] == {"name": "rhel-9-v*", "architecture": "X86_64", "status": "READY"}
|
|
61
|
+
assert '(name = "rhel-9-v*")' in q["filter"] and '(status = "READY")' in q["filter"]
|
|
62
|
+
assert '(architecture = "X86_64")' in q["filter"] and q["most_recent"] is True
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_translate_defaults_status_ready_maps_tags_to_labels_and_passes_native():
|
|
66
|
+
q = translate_image_query(ImageQuery.from_mapping({"tag:Environment": "prod", "family": "debian-12",
|
|
67
|
+
"native": {"deprecated.state": "ACTIVE"}}))
|
|
68
|
+
assert q["terms"] == {"family": "debian-12", "status": "READY", "labels.Environment": "prod",
|
|
69
|
+
"deprecated.state": "ACTIVE"}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_translate_refuses_the_ec2_particulars_by_name():
|
|
73
|
+
# the keys GCE used to drop with a debug line are refused, named (validate refuses them first)
|
|
74
|
+
with pytest.raises(ValueError, match="root_device, virtualization"):
|
|
75
|
+
translate_image_query(ImageQuery.from_mapping({"filters": {"root_device_type": "ebs", "virtualization_type": "hvm"}}))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_an_unknown_key_is_refused_by_the_vocabulary_not_buried():
|
|
79
|
+
with pytest.raises(ValueError, match="totally_custom"):
|
|
80
|
+
ImageQuery.from_mapping({"filters": {"totally_custom": "x"}})
|
|
89
81
|
|
|
90
82
|
|
|
91
83
|
def test_build_filter_expression_quotes_strings_and_lowers_bools():
|
{cs_image_system_gcloud_runtime_plugin-0.1.4 → cs_image_system_gcloud_runtime_plugin-0.1.6}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|