cs-image-system-gcs-state-plugin 0.1.1__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.
- cs_image_system/gcs_state_plugin/__init__.py +3 -0
- cs_image_system/gcs_state_plugin/gcs_state_builder.py +20 -0
- cs_image_system/gcs_state_plugin/gcs_state_models.py +116 -0
- cs_image_system/gcs_state_plugin/main.py +25 -0
- cs_image_system_gcs_state_plugin-0.1.1.dist-info/METADATA +456 -0
- cs_image_system_gcs_state_plugin-0.1.1.dist-info/RECORD +9 -0
- cs_image_system_gcs_state_plugin-0.1.1.dist-info/WHEEL +4 -0
- cs_image_system_gcs_state_plugin-0.1.1.dist-info/entry_points.txt +2 -0
- cs_image_system_gcs_state_plugin-0.1.1.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from typing import TypeVar
|
|
6
|
+
|
|
7
|
+
from cs_image_system.base.basic.builder_base_state import StateBuilderBase
|
|
8
|
+
|
|
9
|
+
from .gcs_state_models import GCS_STATE, GcsStateBuilderModel
|
|
10
|
+
|
|
11
|
+
Q = TypeVar("Q", bound=GcsStateBuilderModel)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GcsStateBuilder(StateBuilderBase[Q]):
|
|
15
|
+
"""The gcs backend's builder: it emits nothing and runs nothing; the
|
|
16
|
+
consumers do, through the collector (like the S3 and local builders)."""
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def csis_name(cls) -> str:
|
|
20
|
+
return GCS_STATE
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""The ``gcs`` state backend type (stage 47.4): a Google Cloud Storage bucket.
|
|
6
|
+
|
|
7
|
+
OpenTofu's ``gcs`` backend stores a workspace's state as
|
|
8
|
+
``<prefix>/<terraform workspace>.tfstate``, the terraform workspace being
|
|
9
|
+
``default`` for every root this system emits, so a root's own prefix is the
|
|
10
|
+
declared prefix with the root's safe name appended: two roots never share an
|
|
11
|
+
object. Credentials are a PATH (``credentials``) or an impersonated service
|
|
12
|
+
account, never a value. Declaring the type binds nothing: the standing
|
|
13
|
+
decision keeps every live root, the GCE roots included, on the S3 backend.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any, Mapping
|
|
18
|
+
|
|
19
|
+
from pydantic.dataclasses import dataclass # stage 23: validation at construction
|
|
20
|
+
|
|
21
|
+
from cs_image_system.base.constants import VCT
|
|
22
|
+
from cs_image_system.base.models.model_config import CSIS_MODEL_CONFIG
|
|
23
|
+
from cs_image_system.base.models.state_builder import StateBuilderModel
|
|
24
|
+
from cs_image_system.base.utils import super_safe_name
|
|
25
|
+
from cs_image_system.hashicorp_utils.collector import BackendRegistration, StateLocation, TerraformCollector
|
|
26
|
+
|
|
27
|
+
GCS_STATE: str = "gcs"
|
|
28
|
+
DEFAULT_TERRAFORM_WORKSPACE = "default"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class GcsBackendKind:
|
|
32
|
+
"""The gcs type's renderings: the location is ``gcs://<bucket>/<prefix>/
|
|
33
|
+
<root>/default.tfstate``; the backend file and a consumer's data source
|
|
34
|
+
carry ``bucket`` and the root's ``prefix``, plus the credential path and
|
|
35
|
+
the impersonated account when declared (the data source reads with the
|
|
36
|
+
same identity as the producer)."""
|
|
37
|
+
type = GCS_STATE
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def root_prefix(settings: Mapping[str, Any], workspace: str) -> str:
|
|
41
|
+
return StateLocation.normalise_key(f"{settings.get('prefix') or ''}/{super_safe_name(workspace)}")
|
|
42
|
+
|
|
43
|
+
def location(self, settings: Mapping[str, Any], workspace: str) -> StateLocation:
|
|
44
|
+
return StateLocation(type=self.type, container=str(settings["bucket"]),
|
|
45
|
+
key=f"{self.root_prefix(settings, workspace)}/{DEFAULT_TERRAFORM_WORKSPACE}.tfstate")
|
|
46
|
+
|
|
47
|
+
def _identity(self, settings: Mapping[str, Any]) -> dict[str, Any]:
|
|
48
|
+
out: dict[str, Any] = {}
|
|
49
|
+
if settings.get("credentials"):
|
|
50
|
+
out["credentials"] = settings["credentials"]
|
|
51
|
+
if settings.get("impersonate_service_account"):
|
|
52
|
+
out["impersonate_service_account"] = settings["impersonate_service_account"]
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
def backend_settings(self, settings: Mapping[str, Any], workspace: str) -> dict[str, Any]:
|
|
56
|
+
out: dict[str, Any] = {"bucket": settings["bucket"], "prefix": self.root_prefix(settings, workspace)}
|
|
57
|
+
out.update(self._identity(settings))
|
|
58
|
+
for key in ("encryption_key", "kms_encryption_key"):
|
|
59
|
+
if settings.get(key):
|
|
60
|
+
out[key] = settings[key]
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
def remote_state_settings(self, settings: Mapping[str, Any], workspace: str) -> dict[str, Any]:
|
|
64
|
+
out: dict[str, Any] = {"bucket": settings["bucket"], "prefix": self.root_prefix(settings, workspace)}
|
|
65
|
+
out.update(self._identity(settings))
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
GCS_KIND = GcsBackendKind()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(kw_only=True, config=CSIS_MODEL_CONFIG)
|
|
73
|
+
class GcsStateBuilderModel(StateBuilderModel):
|
|
74
|
+
"""A ``gcs`` entry under ``state_backends:``."""
|
|
75
|
+
bucket: str
|
|
76
|
+
prefix: str = "statefiles"
|
|
77
|
+
credentials: str | None = None # a PATH to a credentials file, never a value
|
|
78
|
+
impersonate_service_account: str | None = None
|
|
79
|
+
encryption_key: str | None = None # a customer-supplied key, from the environment in practice
|
|
80
|
+
kms_encryption_key: str | None = None # a Cloud KMS key name
|
|
81
|
+
type = GCS_STATE
|
|
82
|
+
# the executables entry these roots run; None = unspecified (no version
|
|
83
|
+
# check), a declared name is checked like every builder's (stage 63)
|
|
84
|
+
executable: str | None = None
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def csis_name(cls) -> str:
|
|
88
|
+
return GCS_STATE
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def csis_classifier(cls) -> VCT:
|
|
92
|
+
return VCT.STATE_BACKEND_MODEL
|
|
93
|
+
|
|
94
|
+
def __post_init__(self) -> None:
|
|
95
|
+
if not self.bucket or not self.bucket.strip():
|
|
96
|
+
raise ValueError("Terraform state backend 'bucket' cannot be empty for 'gcs' type.")
|
|
97
|
+
self.bucket = self.bucket.strip()
|
|
98
|
+
self.prefix = self.prefix.strip().strip("/")
|
|
99
|
+
super().__post_init__()
|
|
100
|
+
|
|
101
|
+
def to_backend_registration(self) -> BackendRegistration:
|
|
102
|
+
return BackendRegistration(
|
|
103
|
+
name=self.name, type=self.type_, kind=GCS_KIND, is_default=self.get_is_default(),
|
|
104
|
+
aliases=tuple(sorted(self.aliases or ())),
|
|
105
|
+
settings={"bucket": self.bucket, "prefix": self.prefix, "credentials": self.credentials,
|
|
106
|
+
"impersonate_service_account": self.impersonate_service_account,
|
|
107
|
+
"encryption_key": self.encryption_key, "kms_encryption_key": self.kms_encryption_key})
|
|
108
|
+
|
|
109
|
+
def get_state_file_path(self, builder_name: str) -> str:
|
|
110
|
+
return self.to_backend_registration().state_file_path(builder_name)
|
|
111
|
+
|
|
112
|
+
def finalize(self) -> None:
|
|
113
|
+
if self._finalized:
|
|
114
|
+
return
|
|
115
|
+
super().finalize()
|
|
116
|
+
TerraformCollector().register_backend(self.to_backend_registration())
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from cs_image_system.base.basic.abstract_plugin_metadata import AbstractPluginMetadata
|
|
8
|
+
from cs_image_system.base.protocols.plugin_metadata import PluginMetadataProtocol
|
|
9
|
+
|
|
10
|
+
from .gcs_state_builder import GcsStateBuilder
|
|
11
|
+
from .gcs_state_models import GCS_STATE, GcsStateBuilderModel
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GcsStateTypes(AbstractPluginMetadata, PluginMetadataProtocol):
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
super().__init__("1", "3.13",
|
|
19
|
+
{GCS_STATE: [GcsStateBuilderModel, GcsStateBuilder]},
|
|
20
|
+
{GcsStateBuilderModel: GcsStateBuilder})
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def initialize():
|
|
24
|
+
log.debug("Initializing GCS state provider")
|
|
25
|
+
return GcsStateTypes()
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cs-image-system-gcs-state-plugin
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: The GCS state backend plugin: a Google Cloud Storage bucket for every terraform root that names it
|
|
5
|
+
Project-URL: Repository, https://github.com/infrastructurebuilder/cs-image-system-3
|
|
6
|
+
Project-URL: Documentation, https://github.com/infrastructurebuilder/cs-image-system-3/blob/develop/docs/OPERATIONS.md
|
|
7
|
+
Project-URL: Package README, https://github.com/infrastructurebuilder/cs-image-system-3/blob/develop/packages/gcs-state-plugin/README.md
|
|
8
|
+
Author-email: Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Python: >=3.13
|
|
12
|
+
Requires-Dist: cs-image-system-hashicorp-utils==0.1.1
|
|
13
|
+
Requires-Dist: cs-image-system-system==0.1.1
|
|
14
|
+
Requires-Dist: pydantic>=2.13
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# cs-image-system-gcs-state-plugin
|
|
18
|
+
|
|
19
|
+
The `gcs` **state backend** plugin (stage 47): a Google Cloud Storage bucket
|
|
20
|
+
as a terraform state backend. It registers one model,
|
|
21
|
+
`GcsStateBuilderModel`, for entries with `type: gcs` under `state_backends:`
|
|
22
|
+
in `cfg/state-*.yml`. Like the S3 and local plugins it emits no files and
|
|
23
|
+
runs no commands of its own: a root that binds to it gets an empty
|
|
24
|
+
`backend "gcs" {}` block, a partial configuration file, and consumers reading
|
|
25
|
+
its state get a `terraform_remote_state` data source with the same settings.
|
|
26
|
+
|
|
27
|
+
Declaring the type binds nothing. The standing decision keeps every live
|
|
28
|
+
root -- the GCE roots included -- on the S3 backend; the fixture declares a
|
|
29
|
+
`gcs` backend that no root binds to, so the type loads and renders, and the
|
|
30
|
+
golden does not move.
|
|
31
|
+
|
|
32
|
+
## What it registers
|
|
33
|
+
|
|
34
|
+
```toml
|
|
35
|
+
[project.entry-points."cs_image_system.plugins.state"]
|
|
36
|
+
gcs_state_plugin = "cs_image_system.gcs_state_plugin.main:initialize"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`initialize()` in [main.py](src/cs_image_system/gcs_state_plugin/main.py)
|
|
40
|
+
returns a `GcsStateTypes` metadata object (plugin metadata version `1`,
|
|
41
|
+
Python `3.13`) that pairs the model with its builder. The package depends
|
|
42
|
+
on `cs-image-system-system` and `cs-image-system-hashicorp-utils` at its
|
|
43
|
+
own version ([pyproject.toml](pyproject.toml)); the collector the kind
|
|
44
|
+
renders for lives in the latter.
|
|
45
|
+
|
|
46
|
+
| Service name (`type:` of a backend entry) | Model class | Builder class | Classifications (VCT) |
|
|
47
|
+
|---|---|---|---|
|
|
48
|
+
| `gcs` | `GcsStateBuilderModel` | `GcsStateBuilder` | `STATE_BACKEND_MODEL`, `STATE_BACKEND` |
|
|
49
|
+
|
|
50
|
+
## The model
|
|
51
|
+
|
|
52
|
+
`GcsStateBuilderModel` in
|
|
53
|
+
[gcs_state_models.py](src/cs_image_system/gcs_state_plugin/gcs_state_models.py)
|
|
54
|
+
extends `StateBuilderModel`
|
|
55
|
+
([state_builder.py](../base/src/cs_image_system/base/models/state_builder.py)),
|
|
56
|
+
which adds nothing over `BuilderModel`
|
|
57
|
+
([builder_model.py](../base/src/cs_image_system/base/models/builder_model.py))
|
|
58
|
+
except the classification. A state backend has no `runtime` field. Every
|
|
59
|
+
field, with its type and default, is in the configuration reference below.
|
|
60
|
+
`GcsStateBuilder` in
|
|
61
|
+
[gcs_state_builder.py](src/cs_image_system/gcs_state_plugin/gcs_state_builder.py)
|
|
62
|
+
extends `StateBuilderBase` and implements no hooks: it exists so the
|
|
63
|
+
registry can pair the model with a builder class.
|
|
64
|
+
|
|
65
|
+
## The kind
|
|
66
|
+
|
|
67
|
+
`GcsBackendKind` in
|
|
68
|
+
[gcs_state_models.py](src/cs_image_system/gcs_state_plugin/gcs_state_models.py)
|
|
69
|
+
renders the type for the collector (the contract the S3 plugin's README
|
|
70
|
+
describes under "Adding a backend type"). OpenTofu's `gcs` backend stores a
|
|
71
|
+
root's state as `<prefix>/<terraform workspace>.tfstate`, and the terraform
|
|
72
|
+
workspace is `default` for every root this system emits, so the root's own
|
|
73
|
+
prefix is the declared prefix with the root's safe name appended -- two
|
|
74
|
+
roots never share an object:
|
|
75
|
+
|
|
76
|
+
| `location(settings, workspace)` | `backend_settings(...)` | `remote_state_settings(...)` |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `gcs://<bucket>/<prefix>/<super_safe_name(workspace)>/default.tfstate` | `bucket`, `prefix = "<prefix>/<root>"`, then `credentials`, `impersonate_service_account`, `encryption_key`, `kms_encryption_key` when set | `bucket`, `prefix`, then `credentials` and `impersonate_service_account` when set |
|
|
79
|
+
|
|
80
|
+
`super_safe_name` lowercases the root's name and turns `-`, `.`, `:`, `+`,
|
|
81
|
+
`/`, `\`, `@` and spaces into `_`, so the root `gcp-pd` under the declared
|
|
82
|
+
prefix `statefiles/csia` keeps its state at
|
|
83
|
+
`gcs://csis-sandbox-tfstate/statefiles/csia/gcp_pd/default.tfstate`, and
|
|
84
|
+
its partial configuration file reads:
|
|
85
|
+
|
|
86
|
+
```hcl
|
|
87
|
+
# Backend 'gcs-east1' (gcs) partial configuration for workspace gcp-pd
|
|
88
|
+
bucket = "csis-sandbox-tfstate"
|
|
89
|
+
prefix = "statefiles/csia/gcp_pd"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The root's prefix is normalised before it is rendered or compared
|
|
93
|
+
(repeated slashes collapsed, leading and trailing ones stripped, case
|
|
94
|
+
kept), so `//a//b/` and `a/b` are one place. An empty declared prefix puts
|
|
95
|
+
every root directly under the bucket (`gcs://<bucket>/<root>/default.tfstate`).
|
|
96
|
+
|
|
97
|
+
## Example configuration
|
|
98
|
+
|
|
99
|
+
[`cfg/state-gcm.yml`](../../tests/fixtures/config/cfg/state-gcm.yml) in the
|
|
100
|
+
fixture, declared and bound to nothing:
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
---
|
|
104
|
+
state_backends:
|
|
105
|
+
- name: gcs-east1
|
|
106
|
+
type: gcs
|
|
107
|
+
bucket: csis-sandbox-tfstate
|
|
108
|
+
prefix: statefiles/csia
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The live configuration carries the same file with every line commented out:
|
|
112
|
+
a placeholder for the day the GCE roots move, which is a decision, not a
|
|
113
|
+
declaration. A file of comments loads as no declaration at all
|
|
114
|
+
([tests/test_v2_state_locations.py](../../tests/test_v2_state_locations.py)
|
|
115
|
+
proves both shapes).
|
|
116
|
+
|
|
117
|
+
A root would bind with `state_configuration: gcs-east1` on the builder
|
|
118
|
+
(or on its runtime, so that everything on that runtime keeps its state
|
|
119
|
+
there); no fixture or live root does.
|
|
120
|
+
|
|
121
|
+
## Prerequisites and integration
|
|
122
|
+
|
|
123
|
+
Everything below is what a root BOUND to a `gcs` backend needs; a declared
|
|
124
|
+
and unbound backend (the fixture, the live tree) needs nothing beyond the
|
|
125
|
+
core, and a dry run never reaches the bucket.
|
|
126
|
+
|
|
127
|
+
- **The core, and the gate.** `cs-image-system-system` and
|
|
128
|
+
`cs-image-system-hashicorp-utils` at the same version (the package's
|
|
129
|
+
dependencies). The whole state-backend mechanism is gated by
|
|
130
|
+
`config.use_state_backends` in `cfg/_config.yml`: when it is false or
|
|
131
|
+
absent, nothing this plugin registers is rendered, recorded or checked
|
|
132
|
+
(the collector answers "no backend" for every workspace), and none of
|
|
133
|
+
what follows applies.
|
|
134
|
+
Off is itself refused by `validate` when any root would read another's
|
|
135
|
+
outputs through remote state (stage 63 item 19), so it is a shape only
|
|
136
|
+
for a tree where nothing reads anything.
|
|
137
|
+
- **A bucket that exists.** The system creates no state bucket: the
|
|
138
|
+
`bucket` field names one the operator made beforehand, in a project the
|
|
139
|
+
identity below can write to. Nothing in the system checks that it
|
|
140
|
+
exists; the first real `tofu init -backend-config=...` against it does,
|
|
141
|
+
and refuses with tofu's own message when it does not.
|
|
142
|
+
- **A Google identity for tofu.** This plugin reads no environment
|
|
143
|
+
variable and no credential file. It passes three declarations through
|
|
144
|
+
to tofu, in the root's `.tfbackend.hcl` and in every consumer's
|
|
145
|
+
`terraform_remote_state` data source: `credentials` (a PATH to a
|
|
146
|
+
credentials file -- the emission is committed, so never a value),
|
|
147
|
+
`impersonate_service_account` (the service account tofu impersonates),
|
|
148
|
+
or neither, in which case OpenTofu's `gcs` backend finds its identity
|
|
149
|
+
the way OpenTofu documents (Application Default Credentials, or its own
|
|
150
|
+
`GOOGLE_*` backend variables in the environment of whoever runs
|
|
151
|
+
`init`/`plan`/`apply`). The live convention for GCE is ADC obtained by
|
|
152
|
+
`gcloud auth application-default login --impersonate-service-account=csis-runner@<project>.iam.gserviceaccount.com`
|
|
153
|
+
([docs/OPERATIONS.md](../../docs/OPERATIONS.md), "Credentials
|
|
154
|
+
contract"). `preflight`, `state query` and every run report whether ADC
|
|
155
|
+
exist (`GOOGLE_APPLICATION_CREDENTIALS`, else
|
|
156
|
+
`~/.config/gcloud/application_default_credentials.json`) as one
|
|
157
|
+
`session:` line -- for the GCE runtimes that declare them; the line
|
|
158
|
+
knows nothing about a state backend, and no session check is made on a
|
|
159
|
+
backend's behalf.
|
|
160
|
+
- **Encryption keys, when declared.** `encryption_key` (a customer-supplied
|
|
161
|
+
key) and `kms_encryption_key` (a Cloud KMS key name) are written verbatim
|
|
162
|
+
into the root's `.tfbackend.hcl` when set. That file is part of the
|
|
163
|
+
committed emission, so a raw encryption key does not belong in the
|
|
164
|
+
field: leave it unset and supply it through tofu's environment, or name
|
|
165
|
+
a KMS key (a name, not a secret).
|
|
166
|
+
- **The tofu binary.** The consumer root's, not this plugin's: a root's
|
|
167
|
+
`executable` names an entry of `cfg/executables.yml` (the fixture's
|
|
168
|
+
`open-tofu-1`, type `tofu`, `>1,<2`), and that entry is what `validate`
|
|
169
|
+
and every run check. Since stage 63 the model's own `executable` field
|
|
170
|
+
is a checked declaration as well: unset by default, and when set it must
|
|
171
|
+
name an entry of `cfg/executables.yml`, which is then version-checked
|
|
172
|
+
like every entry. It does not choose the binary a root runs. Until
|
|
173
|
+
2026-09-25 it defaulted to `tofu` and was read by nothing; see the
|
|
174
|
+
reference below.
|
|
175
|
+
- **Network.** Whoever runs a real `init`, `plan` or `apply` must reach
|
|
176
|
+
Google Cloud Storage. A dry run initialises with `init -backend=false`
|
|
177
|
+
and never touches the bucket, so CI's dry records, an operator's preview
|
|
178
|
+
and `validate` need no reachability at all.
|
|
179
|
+
- **An AWS session, today.** Not for this plugin -- but because every
|
|
180
|
+
live root, the GCE roots included, keeps its state on the S3 backend,
|
|
181
|
+
any GCE lifecycle still needs a live AWS session until the move is
|
|
182
|
+
decided.
|
|
183
|
+
|
|
184
|
+
## Configuration reference
|
|
185
|
+
|
|
186
|
+
An entry under `state_backends:` in any `cfg/state-*.yml` (the fixture's
|
|
187
|
+
is `cfg/state-gcm.yml`) with `type: gcs`. Unknown keys are refused at load
|
|
188
|
+
by pydantic (naming the key); a `parameters:` key is refused with a message
|
|
189
|
+
naming `variables:`. Field names are exact: `type` in YAML is the model's
|
|
190
|
+
`type_`.
|
|
191
|
+
|
|
192
|
+
| Field | Type | Default | Meaning |
|
|
193
|
+
|---|---|---|---|
|
|
194
|
+
| `name` | str | required | what a root's or runtime's `state_configuration` names; normalised (lowercase, spaces to `_`); `/` and `\` are refused, and so are the reserved words `default`, `self`, `none`, the empty name and null, in any case |
|
|
195
|
+
| `type` | str | required | `gcs`; also the terraform backend type written into consumers' `backend "gcs" {}` block |
|
|
196
|
+
| `description` | str or null | null | free text; not read |
|
|
197
|
+
| `aliases` | set[str] | `{}` | other names the backend answers to (stage 63): they travel on the `BackendRegistration`, and `resolve_backend` tries the backend names first and then every backend's aliases, so a root's or runtime's `state_configuration` may name an alias and `validate` accepts it. Until 2026-09-25 they were registered and never read, and binding to an alias was refused as not declared |
|
|
198
|
+
| `is_default` | bool | `false` | the backend `default` resolves to (a root that names none, whose runtime names none); more than one default across all backend types is refused when anything resolves it |
|
|
199
|
+
| `bucket` | str | required, non-empty | the state bucket; whitespace is trimmed |
|
|
200
|
+
| `prefix` | str | `statefiles` | the prefix inside the bucket; leading and trailing `/` and whitespace are trimmed; each root gets `<prefix>/<root safe name>` beneath it; may be empty, must be a string (`null` is refused) |
|
|
201
|
+
| `credentials` | str or null | null | a PATH to a credentials file, never a value; written as-is into the backend file and every consumer's data source when set |
|
|
202
|
+
| `impersonate_service_account` | str or null | null | the service account tofu impersonates; written into the backend file and every consumer's data source when set |
|
|
203
|
+
| `encryption_key` | str or null | null | a customer-supplied encryption key; written into the backend file (never a data source) when set -- in clear when declared in clear, as its `ENC[age:...]` ciphertext when declared encrypted (stage 63; see Variations) |
|
|
204
|
+
| `kms_encryption_key` | str or null | null | a Cloud KMS key name; written into the backend file only, when set |
|
|
205
|
+
| `executable` | str or null | null | the `cfg/executables.yml` entry the tofu for the roots on this backend comes from (stage 63). Unset: nothing is checked on the backend's behalf. Declared: `validate` and every run refuse a name that is not an entry of `cfg/executables.yml` (`check_state_backend_executables` in [validate.py](../base/src/cs_image_system/base/commands/validate.py)), and the entry is version-checked like every executables entry. This plugin still runs no commands under it. Until 2026-09-25 it defaulted to `tofu`, named no entry, and nothing noticed |
|
|
206
|
+
| `config` | mapping | `{}` | **accepted, not read** |
|
|
207
|
+
| `gitignore` | list[str] | `[]` | **accepted, not read** |
|
|
208
|
+
| `tags` | mapping[str, str] | `{}` | **accepted, not read** |
|
|
209
|
+
|
|
210
|
+
There is no `required_plugins` field any more (stage 63): it was accepted
|
|
211
|
+
and read by nothing, and the `gcs` backend is built into OpenTofu, so the
|
|
212
|
+
field was removed on 2026-09-25. Declaring it is now an unknown-key
|
|
213
|
+
refusal at load; delete the key.
|
|
214
|
+
|
|
215
|
+
What reaches the emission is exactly what `to_backend_registration()`
|
|
216
|
+
registers: `bucket`, `prefix`, `credentials`, `impersonate_service_account`,
|
|
217
|
+
`encryption_key`, `kms_encryption_key`, plus `name`, `type`, `is_default`
|
|
218
|
+
and (since stage 63) `aliases` on the registration itself. `executable` is
|
|
219
|
+
read by `validate`, not by the plugin. Two other declarations, owned by
|
|
220
|
+
other plugins, decide whether any of it is used:
|
|
221
|
+
|
|
222
|
+
| Declared where | Field | Effect on this plugin |
|
|
223
|
+
|---|---|---|
|
|
224
|
+
| `cfg/_config.yml` | `config.use_state_backends` (bool, default false) | off: no backend block, no `.tfbackend.hcl`, no remote-state data source, no location record, no location check -- the declaration is validated at load and then ignored |
|
|
225
|
+
| an instance, storage or identity builder; a runtime builder | `state_configuration` (default `default`) | the binding: the root's own value when it names a backend, else its runtime's, else the single `is_default` backend; `gcs-east1` binds a root here |
|
|
226
|
+
|
|
227
|
+
### Variations
|
|
228
|
+
|
|
229
|
+
- **Bound or unbound.** When a root's `state_configuration` (or its
|
|
230
|
+
runtime's, or the default) resolves to a `gcs` entry, the root gets the
|
|
231
|
+
empty `backend "gcs" {}` block, the `.tfbackend.hcl` above, an
|
|
232
|
+
`init -reconfigure -backend-config=<file>` on real runs, a
|
|
233
|
+
`# state: workspace <root> -> gcs://...` line in its lifecycle's runner
|
|
234
|
+
header, and a record in `meta-state/state-locations.yaml`; every consumer
|
|
235
|
+
that reads the root's outputs gets a `data "terraform_remote_state"` with
|
|
236
|
+
`backend = "gcs"` and the `remote_state_settings` above. When no root
|
|
237
|
+
resolves to it (the fixture, the live tree) the entry is loaded,
|
|
238
|
+
validated and registered, and nothing else happens: the golden carries
|
|
239
|
+
no `gcs` file.
|
|
240
|
+
- **`use_state_backends` on or off.** Off, the plugin's registration is
|
|
241
|
+
inert: `validate` skips the location rules, no file or record names the
|
|
242
|
+
bucket, and `init` runs bare.
|
|
243
|
+
- **Dry run or real run.** A dry run's generation-time init is
|
|
244
|
+
`init -backend=false`: providers are installed and the emission is
|
|
245
|
+
validated, and the bucket is never contacted (there is no state to read,
|
|
246
|
+
and nothing after it needs one). A real run's generation-time init and
|
|
247
|
+
the committed runner script's own init are
|
|
248
|
+
`init -input=false -reconfigure -backend-config=<root>-<phase>.tfbackend.hcl`
|
|
249
|
+
(the bare file name; tofu runs inside the phase directory). Both forms
|
|
250
|
+
record the root's location in meta-state; the record is not a claim of
|
|
251
|
+
reality, only of resolution.
|
|
252
|
+
- **Identity declared or not.** With `credentials` and/or
|
|
253
|
+
`impersonate_service_account` set, both the backend file and every
|
|
254
|
+
consumer's data source carry them, so a consumer reads the producer's
|
|
255
|
+
state with the same identity the producer wrote it with. With neither,
|
|
256
|
+
the files carry `bucket` and `prefix` alone and tofu resolves the
|
|
257
|
+
identity from its own environment.
|
|
258
|
+
- **Encryption keys declared or not.** `encryption_key` and
|
|
259
|
+
`kms_encryption_key` go into the backend file only; a remote-state data
|
|
260
|
+
source never carries them (the test
|
|
261
|
+
[tests/test_gcs_backend.py](tests/test_gcs_backend.py) pins this). Set
|
|
262
|
+
neither and the objects are stored with the bucket's own settings.
|
|
263
|
+
- **Declared prefix empty or set.** Empty (`prefix: ""`), a root's object is
|
|
264
|
+
`gcs://<bucket>/<root>/default.tfstate`; set, it is
|
|
265
|
+
`<prefix>/<root>/default.tfstate`. Either way no two roots share an
|
|
266
|
+
object, because the root's own safe name is always a segment.
|
|
267
|
+
- **Encrypted or clear values.** Any value in the tree may be an
|
|
268
|
+
`ENC[age:...]` marker and decrypts at load. Since stage 63 the partial
|
|
269
|
+
configuration file (`.tfbackend.hcl`) writes such a setting as the
|
|
270
|
+
ciphertext it was read from: `render_backend_value` in
|
|
271
|
+
[collector.py](../hashicorp-utils/src/cs_image_system/hashicorp_utils/collector.py)
|
|
272
|
+
calls `emit`, and execution's materialise step puts the plaintext back
|
|
273
|
+
in the `_private/` copy tofu runs from. So an `encryption_key` declared
|
|
274
|
+
encrypted is committed as ciphertext in the backend file. Until
|
|
275
|
+
2026-09-25 the backend file carried the decrypted value in clear, and
|
|
276
|
+
only the plaintext guard caught it. That change covers the backend file
|
|
277
|
+
only. The kind registers nothing as sensitive, and the other places the
|
|
278
|
+
settings reach -- the consumers' data sources (`bucket`, `prefix`,
|
|
279
|
+
`credentials`, `impersonate_service_account`), the runner header's
|
|
280
|
+
location line, and the `state-locations.yaml` record (which repeats the
|
|
281
|
+
backend-file settings) -- were not changed by stage 63; wherever one of
|
|
282
|
+
them would carry a plaintext, the plaintext guard still refuses the
|
|
283
|
+
write or the commit, naming the file and line. So the safe practice is
|
|
284
|
+
unchanged: keep these values public (the bucket name is public by
|
|
285
|
+
decision) or out of the tree (a key in tofu's environment). `name` and
|
|
286
|
+
`type` may not be markers at all.
|
|
287
|
+
- **`--migrate-state <root>`.** A root moving onto or off a `gcs` backend
|
|
288
|
+
goes through the migration operation like any other: the record of the
|
|
289
|
+
previous location (`backend`, `type`, `location` and the settings above)
|
|
290
|
+
is what `state-migration begin` writes back as
|
|
291
|
+
`<root>.tfbackend.previous.hcl`; the kind renders it, the operation is
|
|
292
|
+
type-agnostic. No such move has been made.
|
|
293
|
+
- **Fixture or live tree.** The fixture declares `gcs-east1` (so the type
|
|
294
|
+
is loaded, registered and rendered in every test run); the live tree
|
|
295
|
+
carries the same file entirely commented out, which loads as nothing. CI
|
|
296
|
+
therefore never sees a `gcs` registration.
|
|
297
|
+
|
|
298
|
+
## What it tests and verifies
|
|
299
|
+
|
|
300
|
+
- **At load (pydantic, then `__post_init__`).** `bucket` present and
|
|
301
|
+
non-empty after trimming; `prefix` a string (trimmed of `/` and
|
|
302
|
+
whitespace); unknown keys and `parameters:` refused; `name` free of `/`
|
|
303
|
+
and `\` and not a reserved word (`default`, `self`, `none`, empty;
|
|
304
|
+
refused where the file is read, naming it). A failure is a
|
|
305
|
+
`ValidationError` naming the field, raised while the configuration
|
|
306
|
+
loads, so `validate` and every run stop with exit 1 before anything is
|
|
307
|
+
generated. Then `finalize()` -- called once per model at load, after
|
|
308
|
+
every plugin key has been structured -- registers a `BackendRegistration`
|
|
309
|
+
with the collector; the same name registered twice with different
|
|
310
|
+
settings is an `HclConfigConflictError` (`State backend '<name>'
|
|
311
|
+
registered twice with different configurations`).
|
|
312
|
+
- **At `validate`, and again at the start of every run** (gated by
|
|
313
|
+
`use_state_backends`; `check_state_locations` in
|
|
314
|
+
[validate.py](../base/src/cs_image_system/base/commands/validate.py)):
|
|
315
|
+
every terraform root's backend is resolved through the chain from the
|
|
316
|
+
declarations alone (by name, or by one of a backend's `aliases` since
|
|
317
|
+
stage 63); a root naming an undeclared backend, a prefix whose
|
|
318
|
+
rendered key carries a `.` or `..` segment, more than one `is_default`,
|
|
319
|
+
two roots that would share one object (same bucket and normalised
|
|
320
|
+
prefix under two names, or two root names that collapse under
|
|
321
|
+
`super_safe_name`), and a root whose resolved location differs from the
|
|
322
|
+
one recorded in `meta-state/state-locations.yaml` while the records show
|
|
323
|
+
live resources in it, are each one line in the validation errors; exit
|
|
324
|
+
1, nothing generated. A root with nothing deployed moves with one INFO
|
|
325
|
+
line. Note the timing of the `..` rule: the model accepts `prefix:
|
|
326
|
+
a/../b` at load, and the refusal comes when a location is asked for.
|
|
327
|
+
- **At generation.** The collector's `validate_all()` (provider
|
|
328
|
+
constraints plus the collision rule) runs before every terraform block,
|
|
329
|
+
provider block, variable block and remote-state data source is rendered;
|
|
330
|
+
a conflict is an `HclConfigConflictError` and the run fails. Each bound
|
|
331
|
+
root's location is written to `meta-state/state-locations.yaml`
|
|
332
|
+
(`backend`, `type`, `location`, the backend-file settings, `run`) and
|
|
333
|
+
named in the runner header. The GCS type's tests here are the four in
|
|
334
|
+
[tests/test_gcs_backend.py](tests/test_gcs_backend.py) (the per-root
|
|
335
|
+
prefix and its normalisation, identity as a path or an impersonation
|
|
336
|
+
never a value, the model's trimming and registration, and a `gcs`
|
|
337
|
+
location as a third kind of place beside `s3` and `local` in the
|
|
338
|
+
collision rule) and the fixture-level test in
|
|
339
|
+
[tests/test_v2_state_locations.py](../../tests/test_v2_state_locations.py)
|
|
340
|
+
(declared, bound to nothing, moves nothing in the golden; its commented
|
|
341
|
+
twin loads as nothing). [tests/test_v2_state_encryption.py](../../tests/test_v2_state_encryption.py)
|
|
342
|
+
requires every declared and emitted `s3` backend to encrypt and counts
|
|
343
|
+
`s3` and `local` files; it asks nothing of `gcs`, and no `gcs` file is
|
|
344
|
+
emitted.
|
|
345
|
+
- **At apply.** Nothing of its own. The plugin runs no command; whether
|
|
346
|
+
the bucket exists, is reachable and is writable by the identity is
|
|
347
|
+
established by tofu's `init` against the backend file, and its verdict
|
|
348
|
+
is tofu's output in the run log (and a non-zero exit for the lifecycle).
|
|
349
|
+
- **After apply (post-finalize hooks) and in the state query.** Nothing.
|
|
350
|
+
The state query asks runtimes and identity providers, never a state
|
|
351
|
+
backend; the location record is the only memory of where a root's state
|
|
352
|
+
was resolved to, and it is compared, not verified against the bucket.
|
|
353
|
+
- **Executable versions (stage 63).** When the entry declares
|
|
354
|
+
`executable`, `check_state_backend_executables` in
|
|
355
|
+
[validate.py](../base/src/cs_image_system/base/commands/validate.py)
|
|
356
|
+
requires it to name an entry of `cfg/executables.yml`, at `validate`
|
|
357
|
+
and at the head of every run, and the stage-48 checks version-check
|
|
358
|
+
that entry with the others. Unset, nothing is checked. Until 2026-09-25
|
|
359
|
+
a state backend's `executable` was not among the checked names.
|
|
360
|
+
|
|
361
|
+
## When it fails
|
|
362
|
+
|
|
363
|
+
No failure involving this plugin has happened live: since it landed on
|
|
364
|
+
2026-09-17 (stage 47.4) no root has been bound to a `gcs` backend, in the
|
|
365
|
+
fixture or in the live tree, so nothing below carries a date. What follows
|
|
366
|
+
is what the code raises, in the order an operator would meet it.
|
|
367
|
+
|
|
368
|
+
- `1 validation error for GcsStateBuilderModel / bucket / Field required`
|
|
369
|
+
or `Terraform state backend 'bucket' cannot be empty for 'gcs' type.` --
|
|
370
|
+
the entry lacks a bucket, or it is blank. Raised at load (the log names
|
|
371
|
+
the file's plugin key and the item that failed to structure); exit 1.
|
|
372
|
+
Add the bucket.
|
|
373
|
+
- `prefix / Input should be a valid string` -- `prefix: null` or a
|
|
374
|
+
non-string. The field is a string; omit it for the default `statefiles`
|
|
375
|
+
or write `""` for none.
|
|
376
|
+
- `<key> / Unexpected keyword argument` (or pydantic's extra-input
|
|
377
|
+
refusal naming the key) -- a field this model does not have, a typo
|
|
378
|
+
(`impersonate_service_acount`), an S3 field (`key`, `region`,
|
|
379
|
+
`profile`) on a `gcs` entry, or `required_plugins` (removed in stage 63;
|
|
380
|
+
until 2026-09-25 it was accepted and ignored). Remove or rename it; the
|
|
381
|
+
table above is the whole set.
|
|
382
|
+
- `<name>: `parameters` was retired (stage 26) ...` -- delete the key.
|
|
383
|
+
- `Name '<name>' contains invalid characters` / `Name cannot be in
|
|
384
|
+
'['default', None, '', 'self']'` -- rename the backend.
|
|
385
|
+
- `Unrecognized type/alias 'gcs' for VCT.STATE_BACKEND_MODEL` (a
|
|
386
|
+
`KeyError` at load) -- the plugin is not installed in the environment
|
|
387
|
+
that loaded the tree (it is a workspace member here, so this means a
|
|
388
|
+
foreign or partial install). Install `cs-image-system-gcs-state-plugin`
|
|
389
|
+
at the system's version.
|
|
390
|
+
- `State backend '<name>' registered twice with different
|
|
391
|
+
configurations` -- two files declare the same name with different
|
|
392
|
+
settings (an uncommented live `state-gcm.yml` beside a second copy, for
|
|
393
|
+
instance). Keep one.
|
|
394
|
+
- `workspace '<root>' names state backend '<name>', which is not declared`
|
|
395
|
+
-- a root's or runtime's `state_configuration` names a backend no file
|
|
396
|
+
declares, by name or by alias (an alias resolves since stage 63; until
|
|
397
|
+
2026-09-25 naming one landed here); the live tree's `gcs-east1` is commented out, so binding a
|
|
398
|
+
live root to it today fails exactly here. Declare it, or change the
|
|
399
|
+
binding. Exit 1 from `validate` or the run.
|
|
400
|
+
- `Executable <x> specified for state backend <name> not found in
|
|
401
|
+
executables list.` -- the entry's `executable` names nothing in
|
|
402
|
+
`cfg/executables.yml` (stage 63). Fix the spelling, declare the entry,
|
|
403
|
+
or remove the key (unset means unchecked). Exit 1 from `validate` or
|
|
404
|
+
the run.
|
|
405
|
+
- `workspace '<root>': state key '<prefix>/<root>' carries a '..' segment`
|
|
406
|
+
-- the declared prefix contains `.` or `..`. Write a plain prefix.
|
|
407
|
+
- `Multiple default state backends registered: [...]` -- two backends,
|
|
408
|
+
of any types, carry `is_default: true`. Keep one.
|
|
409
|
+
- `state location collision: workspaces <a>, <b> would share the state
|
|
410
|
+
object gcs://<bucket>/<prefix>/<name>/default.tfstate` -- two root names
|
|
411
|
+
that collapse under `super_safe_name` (`Gcp-PD` and `gcp_pd`) on one
|
|
412
|
+
`gcs` backend, or two `gcs` backends naming the same bucket and prefix
|
|
413
|
+
bound to same-named roots. The same bucket name under `s3` and under
|
|
414
|
+
`gcs` is two places, not a collision. Rename a root or separate the
|
|
415
|
+
prefixes.
|
|
416
|
+
- `workspace '<root>' would move its state from <old> to
|
|
417
|
+
gcs://... while <resources> stand(s) in it: the new location is empty,
|
|
418
|
+
so the next plan would create everything again and strand the old
|
|
419
|
+
state. To MOVE the state: run --no-dry-run ... --migrate-state <root>
|
|
420
|
+
...` -- the guard that makes moving the GCE roots onto a `gcs` backend an
|
|
421
|
+
operation. The record is `meta-state/state-locations.yaml`; the way
|
|
422
|
+
through is the migration operation ([docs/OPERATIONS.md](../../docs/OPERATIONS.md),
|
|
423
|
+
"Where state lives"), never a rebinding. A root with nothing deployed
|
|
424
|
+
logs `moves its state from ... nothing is deployed from it` and
|
|
425
|
+
proceeds.
|
|
426
|
+
- `State backend '<name>' (type 'gcs') was registered without a kind` --
|
|
427
|
+
cannot happen through this model (it always registers `GCS_KIND`); it
|
|
428
|
+
would mean a hand-built `BackendRegistration` in a test.
|
|
429
|
+
- **tofu refuses at `init`** -- the bucket does not exist, the identity
|
|
430
|
+
cannot read or write it, `credentials` names a file that is not there
|
|
431
|
+
where tofu runs (the real run's commands execute in the `_private/`
|
|
432
|
+
mirror beside `generated/`, so give an absolute path), or the network
|
|
433
|
+
is not there. The message is tofu's, in the run log under the root's
|
|
434
|
+
init, and the lifecycle exits non-zero; the plugin makes no claim of its
|
|
435
|
+
own. Only a real run gets here; a dry run's init is `-backend=false`.
|
|
436
|
+
- **`meta-state/<file>: <line>` from the plaintext guard, or a refused
|
|
437
|
+
commit** -- a backend field was declared as an `ENC[age:...]` value and
|
|
438
|
+
its plaintext now stands in a record or in a part of the emission that
|
|
439
|
+
does not write ciphertext. Since stage 63 the `.tfbackend.hcl` itself
|
|
440
|
+
carries the ciphertext, so the backend file is no longer where the
|
|
441
|
+
plaintext stands; the data sources, the runner header and the
|
|
442
|
+
location record were not changed (see Variations). Look at the file
|
|
443
|
+
and line the guard names, then write the value in clear or move it out
|
|
444
|
+
of the tree.
|
|
445
|
+
|
|
446
|
+
## Related
|
|
447
|
+
|
|
448
|
+
- [tf-s3-state-plugin](../tf-s3-state-plugin/README.md): the S3 type, and
|
|
449
|
+
the recipe for adding a type.
|
|
450
|
+
- [local-state-plugin](../local-state-plugin/README.md): the type that needs
|
|
451
|
+
nothing.
|
|
452
|
+
- [hashicorp-utils collector.py](../hashicorp-utils/src/cs_image_system/hashicorp_utils/collector.py):
|
|
453
|
+
`TerraformCollector`, `BackendRegistration`, `StateLocation` and the
|
|
454
|
+
`BackendKind` protocol this plugin's kind implements.
|
|
455
|
+
- [docs/CONFIGURATION.md](../../docs/CONFIGURATION.md) section 10 and
|
|
456
|
+
[docs/OPERATIONS.md](../../docs/OPERATIONS.md) "Where state lives".
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
cs_image_system/gcs_state_plugin/__init__.py,sha256=GAFRNaGWQxXquHOlCzxan4LkPZ9_NsiyswknxdPcX0c,122
|
|
2
|
+
cs_image_system/gcs_state_plugin/gcs_state_builder.py,sha256=LdElRP75nEh8eqDkaK0pfRWIV9GVdzxOumyilXXkqMQ,605
|
|
3
|
+
cs_image_system/gcs_state_plugin/gcs_state_models.py,sha256=cVvb7m7WED3Lh9cKT8CkImkbiReDbZizXA8MIIxThzk,5325
|
|
4
|
+
cs_image_system/gcs_state_plugin/main.py,sha256=MO3Ja-5HgkCkPT99gFcQNmaK99wB-3PGPpkNC2QwHfY,835
|
|
5
|
+
cs_image_system_gcs_state_plugin-0.1.1.dist-info/METADATA,sha256=vp7K2GTWmdhILKIC62Vh6umUA3h_NYqwy72K_CwK-Ho,28392
|
|
6
|
+
cs_image_system_gcs_state_plugin-0.1.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
7
|
+
cs_image_system_gcs_state_plugin-0.1.1.dist-info/entry_points.txt,sha256=W1WuAZnh9enkG9HEKmPK3y-E7VO-V50dgAuF6sn-ZFQ,100
|
|
8
|
+
cs_image_system_gcs_state_plugin-0.1.1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
9
|
+
cs_image_system_gcs_state_plugin-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|