cs-image-system-dummy-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/dummy_plugin/__init__.py +3 -0
- cs_image_system/dummy_plugin/dummy_builders.py +152 -0
- cs_image_system/dummy_plugin/dummy_models.py +43 -0
- cs_image_system/dummy_plugin/hooks.py +47 -0
- cs_image_system/dummy_plugin/main.py +31 -0
- cs_image_system/py.typed +0 -0
- cs_image_system_dummy_plugin-0.1.1.dist-info/METADATA +603 -0
- cs_image_system_dummy_plugin-0.1.1.dist-info/RECORD +11 -0
- cs_image_system_dummy_plugin-0.1.1.dist-info/WHEEL +4 -0
- cs_image_system_dummy_plugin-0.1.1.dist-info/entry_points.txt +5 -0
- cs_image_system_dummy_plugin-0.1.1.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from cs_image_system.base.basic.asset import Asset, AssetSet
|
|
8
|
+
from cs_image_system.base.basic.builder_base_group import GroupBuilderBase
|
|
9
|
+
from cs_image_system.base.basic.builder_base_user import UserBuilderBase
|
|
10
|
+
from cs_image_system.base.lifecycle import ExecutionLifecyclePhase
|
|
11
|
+
from cs_image_system.base.models.executable import ExecutableModel
|
|
12
|
+
from cs_image_system.base.models.executable_adds import CFExecutables
|
|
13
|
+
from cs_image_system.base.models.user import User
|
|
14
|
+
|
|
15
|
+
from .dummy_models import DUMMY, DummyGroupBuilderModel, DummyUserBuilderModel
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
Group provider implementation via Terraform/Tofu for Dummy groups using Dummy/Dummypam
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# TODO Specific type configs for DefaultGroupProvider if needed
|
|
26
|
+
class DummyGroupBuilder(GroupBuilderBase[DummyGroupBuilderModel]):
|
|
27
|
+
"""Dummy group provider implementation via Terraform/Tofu."""
|
|
28
|
+
@classmethod
|
|
29
|
+
def csis_name(cls) -> str:
|
|
30
|
+
return DUMMY
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def model(self) -> DummyGroupBuilderModel:
|
|
35
|
+
return self._model # type: ignore # FIXME: This is dangerous
|
|
36
|
+
|
|
37
|
+
def generate_items_before(
|
|
38
|
+
self, phase: ExecutionLifecyclePhase
|
|
39
|
+
) -> AssetSet: # list[tuple[Path, str]]:
|
|
40
|
+
"""Generate a list of items to create before a specific execution lifecycle
|
|
41
|
+
event."""
|
|
42
|
+
# items: list[tuple[Path, str]] = []
|
|
43
|
+
items: AssetSet = AssetSet()
|
|
44
|
+
if phase == ExecutionLifecyclePhase.GROUP_GENERATION:
|
|
45
|
+
...
|
|
46
|
+
return items
|
|
47
|
+
|
|
48
|
+
def get_commands_to_run_before(
|
|
49
|
+
self, phase: ExecutionLifecyclePhase
|
|
50
|
+
) -> CFExecutables:
|
|
51
|
+
"""Get a list of commands to run before a specific execution lifecycle event."""
|
|
52
|
+
return CFExecutables([], [])
|
|
53
|
+
|
|
54
|
+
def generate_items_during(
|
|
55
|
+
self, phase: ExecutionLifecyclePhase
|
|
56
|
+
) -> AssetSet:
|
|
57
|
+
"""Generate the items to create during a specific execution lifecycle
|
|
58
|
+
event: none, as an ``AssetSet`` (the contract's type; the generators
|
|
59
|
+
call ``sort_and_write`` on it, and a bare list broke the identity
|
|
60
|
+
lifecycle for any declared ``type: dummy`` builder -- stage 63 item 1)."""
|
|
61
|
+
return AssetSet()
|
|
62
|
+
|
|
63
|
+
def get_commands_to_run_during(
|
|
64
|
+
self, phase: ExecutionLifecyclePhase
|
|
65
|
+
) -> CFExecutables:
|
|
66
|
+
"""Get a list of commands to run during a specific execution lifecycle event."""
|
|
67
|
+
return CFExecutables([], [])
|
|
68
|
+
|
|
69
|
+
def generate_items_after(
|
|
70
|
+
self, phase: ExecutionLifecyclePhase
|
|
71
|
+
) -> AssetSet: # list[tuple[Path, str]]:
|
|
72
|
+
"""Generate a list of items to create after a specific execution lifecycle event."""
|
|
73
|
+
return AssetSet()
|
|
74
|
+
|
|
75
|
+
def get_commands_to_run_after(
|
|
76
|
+
self, phase: ExecutionLifecyclePhase
|
|
77
|
+
) -> CFExecutables:
|
|
78
|
+
"""Get a list of commands to run after a specific execution lifecycle event."""
|
|
79
|
+
# return ([],[]) # Shortcut!
|
|
80
|
+
commands: list[ExecutableModel] = []
|
|
81
|
+
if phase == ExecutionLifecyclePhase.GROUP_GENERATION:
|
|
82
|
+
...
|
|
83
|
+
return CFExecutables(commands, [])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# TODO Specific type configs for DefaultGroupProvider if needed
|
|
87
|
+
class DummyUserBuilder(UserBuilderBase[DummyUserBuilderModel]):
|
|
88
|
+
# class DummyUserBuilder(UserBuilderBase[DummyUserBuilderModel, User]):
|
|
89
|
+
"""Dummy user provider implementation via Terraform/Tofu."""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def csis_name(cls) -> str:
|
|
94
|
+
return DUMMY
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def query_existing_users(self) -> list[User]:
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
def generate_items_before(
|
|
101
|
+
self, phase: ExecutionLifecyclePhase
|
|
102
|
+
) -> AssetSet: # list[tuple[Path, str]]:
|
|
103
|
+
"""Generate a list of items to create before a specific execution lifecycle
|
|
104
|
+
event."""
|
|
105
|
+
files: AssetSet = AssetSet()
|
|
106
|
+
if phase == ExecutionLifecyclePhase.USER_GENERATION:
|
|
107
|
+
# stage 63: under the builder's own root like every other builder's
|
|
108
|
+
# (it wrote `Dummy-tf/dummy_users.tf` beside them)
|
|
109
|
+
rpath = self.get_path_for_phase(phase, "users", suffix=".tf")
|
|
110
|
+
files.append(
|
|
111
|
+
Asset((
|
|
112
|
+
rpath,
|
|
113
|
+
f"# Terraform configuration for Dummy users generated by "
|
|
114
|
+
f"{self.__class__.__name__}\n",
|
|
115
|
+
))
|
|
116
|
+
)
|
|
117
|
+
return files
|
|
118
|
+
|
|
119
|
+
def get_commands_to_run_before(
|
|
120
|
+
self, phase: ExecutionLifecyclePhase
|
|
121
|
+
) -> CFExecutables:
|
|
122
|
+
"""Get a list of commands to run before a specific execution lifecycle event."""
|
|
123
|
+
commands: list[ExecutableModel] = []
|
|
124
|
+
return CFExecutables(commands, [])
|
|
125
|
+
|
|
126
|
+
def generate_items_during(
|
|
127
|
+
self, phase: ExecutionLifecyclePhase
|
|
128
|
+
) -> AssetSet:
|
|
129
|
+
"""Generate the items to create during a specific execution lifecycle
|
|
130
|
+
event: none, as an ``AssetSet`` (stage 63 item 1)."""
|
|
131
|
+
return AssetSet()
|
|
132
|
+
|
|
133
|
+
def get_commands_to_run_during(
|
|
134
|
+
self, phase: ExecutionLifecyclePhase
|
|
135
|
+
) -> CFExecutables:
|
|
136
|
+
"""Get a list of commands to run during a specific execution lifecycle event."""
|
|
137
|
+
|
|
138
|
+
return CFExecutables([], [])
|
|
139
|
+
|
|
140
|
+
def generate_items_after(
|
|
141
|
+
self, phase: ExecutionLifecyclePhase
|
|
142
|
+
) -> AssetSet: # list[tuple[Path, str]]:
|
|
143
|
+
"""Generate a list of items to create after a specific execution lifecycle event."""
|
|
144
|
+
return AssetSet()
|
|
145
|
+
|
|
146
|
+
def get_commands_to_run_after(
|
|
147
|
+
self, phase: ExecutionLifecyclePhase
|
|
148
|
+
) -> CFExecutables:
|
|
149
|
+
"""Get a list of commands to run after a specific execution lifecycle event."""
|
|
150
|
+
commands: list[ExecutableModel] = []
|
|
151
|
+
after: list[ExecutableModel] = []
|
|
152
|
+
return CFExecutables(commands, after)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from cs_image_system.base.models.model_config import CSIS_MODEL_CONFIG
|
|
6
|
+
from pydantic.dataclasses import dataclass # stage 23: validation at construction
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from cs_image_system.base.models.group_builder import GroupBuilderModel
|
|
11
|
+
from cs_image_system.base.models.user_builder import UserBuilderModel
|
|
12
|
+
|
|
13
|
+
DUMMY: str = "dummy"
|
|
14
|
+
@dataclass(kw_only=True, config=CSIS_MODEL_CONFIG)
|
|
15
|
+
class DummyGroupBuilderModel(GroupBuilderModel):
|
|
16
|
+
"""The template's group builder model: the shape a group plugin's model
|
|
17
|
+
takes, and nothing more.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
org, team: two required example fields, validated at load and read
|
|
21
|
+
by nothing -- they show a plugin author how a builder declares
|
|
22
|
+
what its provider needs. (Stage 63: the credential-shaped `key`,
|
|
23
|
+
`secret` and `api_host` were removed, so the template never
|
|
24
|
+
suggests putting a credential in the tree.)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
org: str
|
|
28
|
+
team: str
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def csis_name(cls) -> str:
|
|
32
|
+
return DUMMY
|
|
33
|
+
|
|
34
|
+
@dataclass(kw_only=True, config=CSIS_MODEL_CONFIG)
|
|
35
|
+
class DummyUserBuilderModel(UserBuilderModel):
|
|
36
|
+
"""The template's user builder model; ``org`` and ``team`` as on the
|
|
37
|
+
group model: required example fields, read by nothing."""
|
|
38
|
+
org: str
|
|
39
|
+
team: str
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def csis_name(cls) -> str:
|
|
43
|
+
return DUMMY
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""Example hook plugin (EXPLORE "Expanding What Else Plugins Could Do").
|
|
6
|
+
|
|
7
|
+
Registered under the ``cs_image_system.plugins.hooks`` entry-point group.
|
|
8
|
+
Proves the two seams: an **on-summary notifier** (appends one JSON line per
|
|
9
|
+
run to the file named by ``CSIS_NOTIFY_FILE``, when set) and a
|
|
10
|
+
**plugin-registered lifecycle** (``notify``, positioned last, no phases:
|
|
11
|
+
it exists so the runner treats it exactly like a built-in -- its own
|
|
12
|
+
directory, hooks, gating).
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
from cs_image_system.base.commands.run_lifecycles import HookSet
|
|
21
|
+
from cs_image_system.base.lifecycles import LifecycleSpec
|
|
22
|
+
|
|
23
|
+
log = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
NOTIFY_FILE_ENV = "CSIS_NOTIFY_FILE"
|
|
26
|
+
# The example lifecycle is opt-in: a registered lifecycle joins `run --all`,
|
|
27
|
+
# and an example must not change the meta-workflow of every real run.
|
|
28
|
+
LIFECYCLE_ENV = "CSIS_DUMMY_LIFECYCLE"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def notify_summary(summary) -> None:
|
|
32
|
+
path = os.environ.get(NOTIFY_FILE_ENV)
|
|
33
|
+
if not path:
|
|
34
|
+
log.debug("dummy notifier: CSIS_NOTIFY_FILE unset; nothing to do")
|
|
35
|
+
return
|
|
36
|
+
with open(path, "a") as f:
|
|
37
|
+
f.write(json.dumps({"run": summary.run_id, "ok": summary.ok,
|
|
38
|
+
"requested": summary.requested, "apply": summary.apply}) + "\n")
|
|
39
|
+
log.info(f"dummy notifier: appended run {summary.run_id} to {path}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def initialize() -> HookSet:
|
|
43
|
+
lifecycles = []
|
|
44
|
+
if os.environ.get(LIFECYCLE_ENV):
|
|
45
|
+
lifecycles.append(LifecycleSpec(name="notify", after=None,
|
|
46
|
+
description="example plugin lifecycle: no phases, no builders"))
|
|
47
|
+
return HookSet(on_summary=[notify_summary], lifecycles=lifecycles)
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
from cs_image_system.dummy_plugin.dummy_builders import DummyGroupBuilder, DummyUserBuilder
|
|
10
|
+
from cs_image_system.dummy_plugin.dummy_models import DUMMY, DummyGroupBuilderModel, DummyUserBuilderModel
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
class DummyPluginMetadata(AbstractPluginMetadata,PluginMetadataProtocol):
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
super().__init__("1", "3.13",
|
|
17
|
+
{
|
|
18
|
+
DUMMY:[
|
|
19
|
+
DummyGroupBuilderModel,
|
|
20
|
+
DummyGroupBuilder,
|
|
21
|
+
DummyUserBuilderModel,
|
|
22
|
+
DummyUserBuilder,
|
|
23
|
+
]},
|
|
24
|
+
{DummyGroupBuilderModel: DummyGroupBuilder,
|
|
25
|
+
DummyUserBuilderModel: DummyUserBuilder}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def initialize():
|
|
30
|
+
log.debug("Initializing Dummy Plugin")
|
|
31
|
+
return DummyPluginMetadata()
|
cs_image_system/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cs-image-system-dummy-plugin
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: The extension template and test double: a builder plugin and a hook plugin in their smallest working form
|
|
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/dummy-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-system==0.1.1
|
|
13
|
+
Requires-Dist: pydantic>=2.13
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# cs-image-system-dummy-plugin
|
|
17
|
+
|
|
18
|
+
The extension template and test double. It exists to show, in the smallest
|
|
19
|
+
working form, the two ways a package extends the system: a *builder plugin*
|
|
20
|
+
that registers model and builder classes selectable from YAML, and a *hook
|
|
21
|
+
plugin* that participates in the runner without any YAML at all. It
|
|
22
|
+
registers a `dummy` group builder and a `dummy` user builder that generate
|
|
23
|
+
almost nothing, an on-summary notifier, and an optional `notify` lifecycle.
|
|
24
|
+
Nothing in the frozen fixture selects its builders; the runner's tests
|
|
25
|
+
exercise its hooks. Copy it to start a new plugin.
|
|
26
|
+
|
|
27
|
+
Two things to know before copying it. First, the builders are a template
|
|
28
|
+
of the *shape*, not a working example: a declared `type: dummy` builder
|
|
29
|
+
loads, validates and runs the identity lifecycle (since stage 63; until
|
|
30
|
+
2026-09-24 it failed at generation, see [When it fails](#when-it-fails)),
|
|
31
|
+
but it manages nothing: the group builder writes nothing and the user
|
|
32
|
+
builder writes one comment-only file. The hook plugin is the part the
|
|
33
|
+
tests prove most. Second, the package has no tests of its own (there is no
|
|
34
|
+
`packages/dummy-plugin/tests` directory, and since stage 63 the root
|
|
35
|
+
[pyproject.toml](../../pyproject.toml) no longer lists one among its
|
|
36
|
+
`testpaths`); the repository's
|
|
37
|
+
[tests/test_v2_explore_plugin_hooks.py](../../tests/test_v2_explore_plugin_hooks.py)
|
|
38
|
+
covers the hooks, and
|
|
39
|
+
[tests/test_v2_defects_low_hanging.py](../../tests/test_v2_defects_low_hanging.py)
|
|
40
|
+
and [tests/test_v2_defects_dead.py](../../tests/test_v2_defects_dead.py)
|
|
41
|
+
cover the builders and models.
|
|
42
|
+
|
|
43
|
+
## What it registers
|
|
44
|
+
|
|
45
|
+
Two entry points are declared in [pyproject.toml](pyproject.toml):
|
|
46
|
+
|
|
47
|
+
```toml
|
|
48
|
+
[project.entry-points."cs_image_system.plugins.runtime"]
|
|
49
|
+
dummy_plugin = "cs_image_system.dummy_plugin.main:initialize"
|
|
50
|
+
|
|
51
|
+
[project.entry-points."cs_image_system.plugins.hooks"]
|
|
52
|
+
dummy_hooks = "cs_image_system.dummy_plugin.hooks:initialize"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The first is a builder plugin. `initialize()` in
|
|
56
|
+
[main.py](src/cs_image_system/dummy_plugin/main.py) returns a
|
|
57
|
+
`DummyPluginMetadata` whose services map the key `dummy` to four classes
|
|
58
|
+
and whose `builders_for_models` names a builder class per model class.
|
|
59
|
+
The entry-point group is `runtime`, but the group only decides load order:
|
|
60
|
+
the loader ([loader.py](../base/src/cs_image_system/base/loader.py))
|
|
61
|
+
registers each class under the classification its own `csis_classifier()`
|
|
62
|
+
reports.
|
|
63
|
+
|
|
64
|
+
| Class | `csis_name()` | `csis_classifier()` | Registered under |
|
|
65
|
+
|---|---|---|---|
|
|
66
|
+
| `DummyGroupBuilderModel` ([dummy_models.py](src/cs_image_system/dummy_plugin/dummy_models.py)) | `dummy` | `VCT.GROUP_BUILDER_MODEL` | `group_builder_model` |
|
|
67
|
+
| `DummyGroupBuilder` ([dummy_builders.py](src/cs_image_system/dummy_plugin/dummy_builders.py)) | `dummy` | `VCT.GROUP_BUILDER` | `group_builder` |
|
|
68
|
+
| `DummyUserBuilderModel` ([dummy_models.py](src/cs_image_system/dummy_plugin/dummy_models.py)) | `dummy` | `VCT.USER_BUILDER_MODEL` | `user_builder_model` |
|
|
69
|
+
| `DummyUserBuilder` ([dummy_builders.py](src/cs_image_system/dummy_plugin/dummy_builders.py)) | `dummy` | `VCT.USER_BUILDER` | `user_builder` |
|
|
70
|
+
|
|
71
|
+
How a model finds its builder: not through `builders_for_models`. The
|
|
72
|
+
registry ([registry.py](../base/src/cs_image_system/base/registry.py))
|
|
73
|
+
stores that map (and refuses a model class bound twice), but the lookup
|
|
74
|
+
the context performs when it builds a declared builder
|
|
75
|
+
(`Registry.get_builder_for_model(type_name, classification)`) goes by the
|
|
76
|
+
service registry: the class registered under the same `csis_name()` and
|
|
77
|
+
the builder classification. That is why model and builder must report the
|
|
78
|
+
same name. The `builders_for_models` map is registered and read by
|
|
79
|
+
nothing; its one reader, `Registry.get_builder(model_type)`, had no caller
|
|
80
|
+
and is gone (stage 67). The map stays as the template's shape.
|
|
81
|
+
|
|
82
|
+
A `group_builders:` entry selects the group pair with `type: dummy`; a
|
|
83
|
+
`user_builders:` entry selects the user pair with the same key. There are
|
|
84
|
+
no type aliases. The constant `DUMMY = "dummy"` in `dummy_models.py` is the
|
|
85
|
+
key. No other package imports anything from this one. (The AWS and GCE
|
|
86
|
+
runtime plugins used to carry unregistered copies of
|
|
87
|
+
`DummyGroupBuilderModel` left from the template; those copies are gone
|
|
88
|
+
from their source.)
|
|
89
|
+
|
|
90
|
+
The second is a hook plugin. `initialize()` in
|
|
91
|
+
[hooks.py](src/cs_image_system/dummy_plugin/hooks.py) returns a `HookSet`
|
|
92
|
+
(defined in
|
|
93
|
+
[run_lifecycles.py](../base/src/cs_image_system/base/commands/run_lifecycles.py))
|
|
94
|
+
carrying:
|
|
95
|
+
|
|
96
|
+
| Hook | Registered | What it does |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `on_summary`: `notify_summary` | always | When the environment variable `CSIS_NOTIFY_FILE` names a file, appends one JSON line per run: `{"run", "ok", "requested", "apply"}`. Unset, it logs at debug level and does nothing. |
|
|
99
|
+
| `lifecycles`: `LifecycleSpec(name="notify", after=None)` | only when `CSIS_DUMMY_LIFECYCLE` is set | A lifecycle with no phases and no builders, positioned last. It gets its own `generated/notify/` directory, joins `run --all`, and is a no-op at apply time because it defers nothing. |
|
|
100
|
+
|
|
101
|
+
The `HookSet` also accepts `validators`, `after_generate`, `before_apply`
|
|
102
|
+
and `after_apply` lists; the dummy plugin leaves them empty.
|
|
103
|
+
|
|
104
|
+
## Models
|
|
105
|
+
|
|
106
|
+
### `DummyGroupBuilderModel`
|
|
107
|
+
|
|
108
|
+
Extends `GroupBuilderModel` in
|
|
109
|
+
[group_builder.py](../base/src/cs_image_system/base/models/group_builder.py),
|
|
110
|
+
which extends `BuilderModel` and `NameTyped` in
|
|
111
|
+
[builder_model.py](../base/src/cs_image_system/base/models/builder_model.py).
|
|
112
|
+
The base adds no fields of its own beyond `BuilderModel`.
|
|
113
|
+
|
|
114
|
+
Fields it adds:
|
|
115
|
+
|
|
116
|
+
| Field | Type | Default | Meaning |
|
|
117
|
+
|---|---|---|---|
|
|
118
|
+
| `org` | `str` | required | An organisation label: a required example field, validated at load and read by nothing. It shows a plugin author how a builder declares what its provider needs. |
|
|
119
|
+
| `team` | `str` | required | A team label; the same. |
|
|
120
|
+
|
|
121
|
+
`key`, `secret` and `api_host` are gone (stage 63): they are now unknown
|
|
122
|
+
keys, refused at load like any other. Until 2026-09-25 the model accepted
|
|
123
|
+
them with the default `default` and read none of them; they were removed
|
|
124
|
+
so the template never suggests putting a credential in the tree. A real
|
|
125
|
+
plugin reads a credential from the environment or a credential cache,
|
|
126
|
+
never from the configuration.
|
|
127
|
+
|
|
128
|
+
Base fields it inherits: `name`, `type`, `description`, `aliases`,
|
|
129
|
+
`executable`, `is_default`, `config`, `gitignore`, `tags`. It overrides
|
|
130
|
+
`csis_name()` to return `dummy`. It rejects unknown keys and the retired
|
|
131
|
+
`parameters:` key like every builder model.
|
|
132
|
+
|
|
133
|
+
### `DummyUserBuilderModel`
|
|
134
|
+
|
|
135
|
+
Extends `UserBuilderModel` in
|
|
136
|
+
[user_builder.py](../base/src/cs_image_system/base/models/user_builder.py).
|
|
137
|
+
|
|
138
|
+
Fields it adds:
|
|
139
|
+
|
|
140
|
+
| Field | Type | Default | Meaning |
|
|
141
|
+
|---|---|---|---|
|
|
142
|
+
| `org` | `str` | required | As on the group model: a required example field, validated and read by nothing. |
|
|
143
|
+
| `team` | `str` | required | The same. |
|
|
144
|
+
|
|
145
|
+
Base fields it inherits, and their meaning here:
|
|
146
|
+
|
|
147
|
+
| Field | Type | Default | Meaning |
|
|
148
|
+
|---|---|---|---|
|
|
149
|
+
| `default_user_email_template` | `str` | `{{ user.name }}` | Template for a user's email when the user declares none. |
|
|
150
|
+
| `default_user_description_template` | `str` | `User {{ user.name }} / {{ user.email }}` | Template for a user's description. |
|
|
151
|
+
| `email_domain` | `str` or null | null | The domain half of a derived address: when set and the email template contains `{{ builder.email_domain }}`, `get_user_email_template()` substitutes it in Python before the template renders. |
|
|
152
|
+
| `email_as_username` | `bool` | `True` | When true, `UserBuilderBase.add_user_to_builder()` requires `user.name` to equal `user.email` (case-folded) and fills a blank name from the email. |
|
|
153
|
+
| `name`, `type`, `description`, `aliases`, `executable`, `is_default`, `config`, `gitignore`, `tags` | | | From `BuilderModel`/`NameTyped`. |
|
|
154
|
+
|
|
155
|
+
It overrides `csis_name()` to return `dummy`; the YAML `type:` key is
|
|
156
|
+
required, as on every builder model (a model built without it fails with
|
|
157
|
+
`type: Field required`). Neither model narrows or adds validation. (Until
|
|
158
|
+
2026-09-25 both carried an inert, unannotated class attribute
|
|
159
|
+
`type = DUMMY` and docstrings naming attributes that did not exist; stage
|
|
160
|
+
63 removed them.)
|
|
161
|
+
|
|
162
|
+
## The builder
|
|
163
|
+
|
|
164
|
+
Both builders implement the six generation hooks of `BuilderBase` in
|
|
165
|
+
[builder_base.py](../base/src/cs_image_system/base/basic/builder_base.py):
|
|
166
|
+
`generate_items_before`, `get_commands_to_run_before`,
|
|
167
|
+
`generate_items_during`, `get_commands_to_run_during`,
|
|
168
|
+
`generate_items_after`, `get_commands_to_run_after`. The runner calls each
|
|
169
|
+
with a lifecycle phase; a builder answers with assets to write (an
|
|
170
|
+
`AssetSet` of path and content) or commands to run (a `CFExecutables` of
|
|
171
|
+
commands now and commands at finalization). The base class returns an
|
|
172
|
+
`AssetSet` from every `generate_items_*` hook, and so do the two dummy
|
|
173
|
+
builders (since stage 63 item 1; `generate_items_during` returned a plain
|
|
174
|
+
`list` until 2026-09-24, the defect described under
|
|
175
|
+
[When it fails](#when-it-fails)).
|
|
176
|
+
|
|
177
|
+
### `DummyGroupBuilder`
|
|
178
|
+
|
|
179
|
+
Extends `GroupBuilderBase` in
|
|
180
|
+
[builder_base_group.py](../base/src/cs_image_system/base/basic/builder_base_group.py).
|
|
181
|
+
Every hook returns empty, in the order the group lifecycle runs them:
|
|
182
|
+
|
|
183
|
+
| Phase | Hook | Returns |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `group-generation` | `generate_items_before` | An empty `AssetSet`. |
|
|
186
|
+
| `group-generation` | `get_commands_to_run_before` | No commands. |
|
|
187
|
+
| `group-generation` | `generate_items_during` | An empty `AssetSet` (since stage 63 item 1; a plain `list` until then, which the runner's `.sort_and_write()` call broke on). |
|
|
188
|
+
| `group-generation` | `get_commands_to_run_during` | No commands. |
|
|
189
|
+
| `group-generation` | `generate_items_after` | An empty `AssetSet`. |
|
|
190
|
+
| `group-generation` | `get_commands_to_run_after` | No commands. |
|
|
191
|
+
|
|
192
|
+
It inherits the identity contract unchanged: `identity_type()` is `dummy`
|
|
193
|
+
(the default is the plugin's name), so a base image declaring
|
|
194
|
+
`identity_types: [dummy]` bakes nothing for it (`base_image_prerequisites`,
|
|
195
|
+
`verify_commands`, `activation_commands`, `activation_verify_commands` and
|
|
196
|
+
`launch_parameters` are the empty base defaults, `enrollment_token_reference`
|
|
197
|
+
is `None`); `gid_policy()` is `provider-assigned`; `query_state()`,
|
|
198
|
+
`query_attributes()`, `attribute_conflicts()` and `export_gids()` raise
|
|
199
|
+
`NotImplementedError`, so the state query and the attributes probe leave it
|
|
200
|
+
out of the picture, while the gid shim reports the error (see
|
|
201
|
+
[What it tests and verifies](#what-it-tests-and-verifies)). The stage-55
|
|
202
|
+
server registry (`can_query_servers()` is `False`, `registered_servers()`
|
|
203
|
+
is `None`, `retire_servers_named()` raises), the stage-56 workload access
|
|
204
|
+
(`can_manage_workload_access()` is `False`, `workload_access_expected()`
|
|
205
|
+
and `workload_access_state()` are `None`, `ensure_workload_access()`
|
|
206
|
+
raises) and the stage-61 `prune_stale_attachments()` (returns 0, does
|
|
207
|
+
nothing) are likewise the base defaults.
|
|
208
|
+
|
|
209
|
+
### `DummyUserBuilder`
|
|
210
|
+
|
|
211
|
+
Extends `UserBuilderBase` in
|
|
212
|
+
[builder_base_user.py](../base/src/cs_image_system/base/basic/builder_base_user.py).
|
|
213
|
+
|
|
214
|
+
| Phase | Hook | Returns |
|
|
215
|
+
|---|---|---|
|
|
216
|
+
| any | `query_existing_users()` | An empty list. Plugin-local: nothing in the core calls it. |
|
|
217
|
+
| `user-generation` | `generate_items_before` | One asset at `get_path_for_phase(phase, "users", suffix=".tf")`, that is `<builder>/user-generation/<builder>-user-generation-users.tf` under the lifecycle directory, containing a single comment line naming the builder class (stage 63; it wrote `Dummy-tf/dummy_users.tf` through a plugin-local `get_subpath()`, now gone, until 2026-09-25). |
|
|
218
|
+
| `user-generation` | `get_commands_to_run_before` | No commands. |
|
|
219
|
+
| `user-generation` | `generate_items_during` | An empty `AssetSet` (since stage 63 item 1). |
|
|
220
|
+
| `user-generation` | `get_commands_to_run_during` | No commands. |
|
|
221
|
+
| `user-generation` | `generate_items_after` | An empty `AssetSet`. |
|
|
222
|
+
| `user-generation` | `get_commands_to_run_after` | No commands. |
|
|
223
|
+
|
|
224
|
+
It inherits `add_user_to_builder()` with its `email_as_username` check,
|
|
225
|
+
`default_managed()` (`True`), `validate_user()` (nothing to say),
|
|
226
|
+
`validate_attributes()` (any declared attributes are reported as
|
|
227
|
+
unsupported) and `query_attributes()` (raises `NotImplementedError`).
|
|
228
|
+
|
|
229
|
+
## Emission
|
|
230
|
+
|
|
231
|
+
With a `type: dummy` user builder named `<builder>` in the tree, the
|
|
232
|
+
identity lifecycle's before-phase hook writes
|
|
233
|
+
`generated/identity/<builder>/user-generation/<builder>-user-generation-users.tf`
|
|
234
|
+
with one comment line: under the builder's own root, the
|
|
235
|
+
`<builder>/<phase>/...` convention every other builder follows (see
|
|
236
|
+
[OPERATIONS.md](../../docs/OPERATIONS.md)). Since stage 63; until
|
|
237
|
+
2026-09-25 it wrote `generated/identity/Dummy-tf/dummy_users.tf`, beside
|
|
238
|
+
the builder directories instead of inside one. The group builder writes
|
|
239
|
+
nothing, and neither builder defers a command, so that one file is all a
|
|
240
|
+
run with a dummy builder produces. The frozen fixture declares neither, so
|
|
241
|
+
the golden emission under
|
|
242
|
+
[tests/fixtures/v2_golden/generated](../../tests/fixtures/v2_golden/generated)
|
|
243
|
+
carries no dummy output.
|
|
244
|
+
|
|
245
|
+
The hook plugin's emission is outside `generated/`: one JSON line appended
|
|
246
|
+
to the file named by `CSIS_NOTIFY_FILE` after every run, and, with
|
|
247
|
+
`CSIS_DUMMY_LIFECYCLE` set, a `generated/notify/.gitignore` and a `notify`
|
|
248
|
+
section in `generated/final_execution.sh`.
|
|
249
|
+
[tests/test_v2_explore_plugin_hooks.py](../../tests/test_v2_explore_plugin_hooks.py)
|
|
250
|
+
pins both: the notifier records the run id and the requested lifecycles;
|
|
251
|
+
the `notify` lifecycle takes its place after the four built-in lifecycles,
|
|
252
|
+
runs, and reports `no-script` at apply time.
|
|
253
|
+
|
|
254
|
+
## Example configuration
|
|
255
|
+
|
|
256
|
+
The plugin is configured through its entry points (above) and two
|
|
257
|
+
environment variables:
|
|
258
|
+
|
|
259
|
+
| Variable | Effect |
|
|
260
|
+
|---|---|
|
|
261
|
+
| `CSIS_NOTIFY_FILE=/path/to/notify.jsonl` | The notifier appends a line per run. |
|
|
262
|
+
| `CSIS_DUMMY_LIFECYCLE=1` | The `notify` lifecycle is registered. |
|
|
263
|
+
|
|
264
|
+
No file under
|
|
265
|
+
[tests/fixtures/config/cfg](../../tests/fixtures/config/cfg) selects the
|
|
266
|
+
builders. An entry that would, shown only to illustrate the shape (the
|
|
267
|
+
fixture's real group and user builders are Okta ones in
|
|
268
|
+
[group-builders.yml](../../tests/fixtures/config/cfg/group-builders.yml)).
|
|
269
|
+
It loads, validates and runs the identity lifecycle, which emits nothing
|
|
270
|
+
for its groups and one comment-only file for the user builder (since
|
|
271
|
+
stage 63 item 1; before that the lifecycle failed at generation, see
|
|
272
|
+
[When it fails](#when-it-fails)). `org` and `team` are the only fields of
|
|
273
|
+
its own either entry takes; `key`, `secret` and `api_host` are refused as
|
|
274
|
+
unknown keys (stage 63). A tree that manages real
|
|
275
|
+
identities needs a real identity plugin instead.
|
|
276
|
+
|
|
277
|
+
```yaml
|
|
278
|
+
group_builders:
|
|
279
|
+
- name: dummy-groups
|
|
280
|
+
type: dummy
|
|
281
|
+
org: example-org
|
|
282
|
+
team: example-team
|
|
283
|
+
|
|
284
|
+
user_builders:
|
|
285
|
+
- name: dummy-users
|
|
286
|
+
type: dummy
|
|
287
|
+
org: example-org
|
|
288
|
+
team: example-team
|
|
289
|
+
email_as_username: false
|
|
290
|
+
default_user_email_template: "{{ user.name }}@example.invalid"
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Using it as a template
|
|
294
|
+
|
|
295
|
+
To write a new builder plugin: copy this package, rename the `cs_image_system.<name>`
|
|
296
|
+
module, keep one model per builder type extending the base model for that
|
|
297
|
+
classification (`GroupBuilderModel`, `UserBuilderModel`,
|
|
298
|
+
`StorageBuilderModel`, ...), give each model and builder the same
|
|
299
|
+
`csis_name()` (the `type:` key), list them in the metadata's services and
|
|
300
|
+
`builders_for_models`, and declare the entry point under the
|
|
301
|
+
`cs_image_system.plugins.<kind>` group for the builder kind (`runtime`,
|
|
302
|
+
`state`, `modification`, `storage`, `group`, `user`, `image`, `os`,
|
|
303
|
+
`instance`, `other`; the list is `PLUGIN_TYPES` in
|
|
304
|
+
[constants.py](../base/src/cs_image_system/base/constants.py)). Return an
|
|
305
|
+
`AssetSet` from every `generate_items_*` hook, not a list. To write a
|
|
306
|
+
hook plugin: return a `HookSet` from an entry point in the
|
|
307
|
+
`cs_image_system.plugins.hooks` group. Registration is validated: a
|
|
308
|
+
lifecycle cannot reuse a built-in name, its `after` anchor must exist, and
|
|
309
|
+
registering the same name twice with a different definition is an error.
|
|
310
|
+
A second worked example lives under
|
|
311
|
+
[docs/example-plugin/plugin-b](../../docs/example-plugin/plugin-b).
|
|
312
|
+
|
|
313
|
+
## Prerequisites and integration
|
|
314
|
+
|
|
315
|
+
Nothing beyond the core. The plugin touches no account, cloud API, IAM
|
|
316
|
+
role, external tool, credential or network; it never calls
|
|
317
|
+
`get_executable_copy()`, so it needs no executable declared. What it does
|
|
318
|
+
need:
|
|
319
|
+
|
|
320
|
+
- **The package installed** in the same environment as the CLI. Both entry
|
|
321
|
+
points are discovered through `importlib.metadata`, so an uninstalled
|
|
322
|
+
package registers nothing: `type: dummy` is then an unknown type and the
|
|
323
|
+
`notify` lifecycle does not exist. In this repository it is a workspace
|
|
324
|
+
member pinned by the root `cs-image-system` metapackage; `just init`
|
|
325
|
+
installs it. Its own requirements ([pyproject.toml](pyproject.toml)) are
|
|
326
|
+
Python 3.13 or newer, `cs-image-system-system` at the same version (the
|
|
327
|
+
CLI package, which brings the base) and `pydantic` 2.13 or newer.
|
|
328
|
+
- **Two environment variables**, both optional, both read by
|
|
329
|
+
[hooks.py](src/cs_image_system/dummy_plugin/hooks.py) straight from
|
|
330
|
+
`os.environ`:
|
|
331
|
+
|
|
332
|
+
| Variable | Read when | Effect |
|
|
333
|
+
|---|---|---|
|
|
334
|
+
| `CSIS_NOTIFY_FILE` | at the end of every run, inside `notify_summary` | The path of a file the notifier appends to (`open(path, "a")`). The directory must already exist; the file is created on first use. Unset or empty: the notifier logs `dummy notifier: CSIS_NOTIFY_FILE unset; nothing to do` at debug level and returns. |
|
|
335
|
+
| `CSIS_DUMMY_LIFECYCLE` | once per process, when `load_hook_plugins()` first runs `initialize()` | Any non-empty value registers the `notify` lifecycle. It is read at load, not at run: setting it after the hooks have loaded changes nothing until a new process (the loader keeps a `_HOOK_PLUGINS_LOADED` flag; only `force=True`, which the tests use, reloads). |
|
|
336
|
+
|
|
337
|
+
- **When the hooks load.** `load_hook_plugins()` runs at the start of
|
|
338
|
+
`run_lifecycles()` and `validate_only()` in
|
|
339
|
+
[run_lifecycles.py](../base/src/cs_image_system/base/commands/run_lifecycles.py),
|
|
340
|
+
and the `run` command in
|
|
341
|
+
[cli.py](../system/src/cs_image_system/system/cli.py) calls it before it
|
|
342
|
+
resolves lifecycle names, so `notify` is a valid name for `run notify`
|
|
343
|
+
and part of `run --all` in the same process (the reason is a failure
|
|
344
|
+
that happened; see [When it fails](#when-it-fails)).
|
|
345
|
+
- **No credential, anywhere.** The plugin has no provider to talk to and
|
|
346
|
+
its models have no credential-shaped field: `key`, `secret` and
|
|
347
|
+
`api_host` were removed in stage 63 (they had been accepted and never
|
|
348
|
+
read until 2026-09-25) and are now refused as unknown keys. A credential
|
|
349
|
+
value never belongs in the tree, encrypted or not.
|
|
350
|
+
|
|
351
|
+
## Configuration reference
|
|
352
|
+
|
|
353
|
+
The plugin owns two list entries of the configuration tree, one under
|
|
354
|
+
`group_builders:` and one under `user_builders:`, both selected by
|
|
355
|
+
`type: dummy`. The manual's general builder fields are in
|
|
356
|
+
[CONFIGURATION.md](../../docs/CONFIGURATION.md) section 4.1; what follows
|
|
357
|
+
is every field these two models accept and what, if anything, reads it.
|
|
358
|
+
|
|
359
|
+
### Common builder fields (both models)
|
|
360
|
+
|
|
361
|
+
| Field | Type | Default | Meaning |
|
|
362
|
+
|---|---|---|---|
|
|
363
|
+
| `name` | str | required | The builder's name; normalised by `safe_name`, must not contain `/` or `\`, must not be a reserved word (`default`, `self`, `none`, empty or null, in any case), and must be unique within its list. Users and groups name it in their own `type:` to attach to it. |
|
|
364
|
+
| `type` | str | required | Must be `dummy` to select this plugin's pair. Any other value selects another plugin or is unknown. |
|
|
365
|
+
| `description` | str or null | null | Accepted, not read by anything in this plugin's path. |
|
|
366
|
+
| `aliases` | set of str | empty | Alternative names, normalised like `name` and registered at load; an alias already registered under the same classification is refused. |
|
|
367
|
+
| `executable` | str or null | null | Accepted, not read: the dummy builders run no commands and never resolve an executable. |
|
|
368
|
+
| `is_default` | bool | `false` | Whether users (or groups) that declare no `type:` attach to this builder. Exactly one builder per list must be the default, dummy or otherwise; two is an error, none is an error. |
|
|
369
|
+
| `config` | dict | empty | Accepted, not read. |
|
|
370
|
+
| `gitignore` | list of str | empty | Accepted, not read. |
|
|
371
|
+
| `tags` | dict of str | empty | Accepted, not read. |
|
|
372
|
+
|
|
373
|
+
### `group_builders:` entry (`DummyGroupBuilderModel`)
|
|
374
|
+
|
|
375
|
+
| Field | Type | Default | Meaning |
|
|
376
|
+
|---|---|---|---|
|
|
377
|
+
| `org` | str | required | Required and validated at load; read by nothing (an example field). |
|
|
378
|
+
| `team` | str | required | Required and validated at load; read by nothing (an example field). |
|
|
379
|
+
| `key`, `secret`, `api_host` | -- | -- | GONE (stage 63): an unknown key, refused at load. Until 2026-09-25 each was accepted with the default `default` and read by nothing. |
|
|
380
|
+
|
|
381
|
+
### `user_builders:` entry (`DummyUserBuilderModel`)
|
|
382
|
+
|
|
383
|
+
| Field | Type | Default | Meaning |
|
|
384
|
+
|---|---|---|---|
|
|
385
|
+
| `org` | str | required | Required and validated at load; read by nothing (an example field). |
|
|
386
|
+
| `team` | str | required | Required and validated at load; read by nothing (an example field). |
|
|
387
|
+
| `default_user_email_template` | str | `{{ user.name }}` | Read by the core's template resolution for a user that declares no `email`. |
|
|
388
|
+
| `default_user_description_template` | str | `User {{ user.name }} / {{ user.email }}` | Read by the core's template resolution for a user that declares no description. |
|
|
389
|
+
| `email_domain` | str or null | null | Folded into the email template in place of `{{ builder.email_domain }}` by `get_user_email_template()`; ignored when the template has no placeholder. |
|
|
390
|
+
| `email_as_username` | bool | `true` | Read by the inherited `add_user_to_builder()` at load. |
|
|
391
|
+
|
|
392
|
+
### Items that attach to these builders
|
|
393
|
+
|
|
394
|
+
Users ([user.py](../base/src/cs_image_system/base/models/user.py)) and
|
|
395
|
+
groups carry a `type:` whose default is `default`. The context
|
|
396
|
+
([global_context.py](../base/src/cs_image_system/base/global_context.py))
|
|
397
|
+
resolves `default` to the list's default builder and any other value to a
|
|
398
|
+
builder by name or alias, then calls the builder's `add_user_to_builder()`
|
|
399
|
+
or `add_group_to_builder()`. So `type: dummy-users` on a user attaches it
|
|
400
|
+
to the example builder above; a user with no `type:` goes to whichever
|
|
401
|
+
user builder is `is_default: true`.
|
|
402
|
+
|
|
403
|
+
### Environment
|
|
404
|
+
|
|
405
|
+
| Variable | Type | Default | Meaning |
|
|
406
|
+
|---|---|---|---|
|
|
407
|
+
| `CSIS_NOTIFY_FILE` | path | unset | See [Prerequisites and integration](#prerequisites-and-integration). |
|
|
408
|
+
| `CSIS_DUMMY_LIFECYCLE` | any non-empty string | unset | See [Prerequisites and integration](#prerequisites-and-integration). |
|
|
409
|
+
|
|
410
|
+
### Variations
|
|
411
|
+
|
|
412
|
+
- **When `CSIS_DUMMY_LIFECYCLE` is set**, `initialize()` returns a
|
|
413
|
+
`HookSet` carrying `LifecycleSpec(name="notify", after=None)` and the
|
|
414
|
+
runner registers it: `notify` appears last in `all_lifecycles()`, joins
|
|
415
|
+
`run --all`, is accepted by `run notify`, gets `generated/notify/`
|
|
416
|
+
(wiped and regenerated with a `.gitignore` each time it is requested)
|
|
417
|
+
and a `notify` section in `final_execution.sh`. Unset, the `HookSet`
|
|
418
|
+
carries no lifecycle, `run notify` is refused as an unknown lifecycle,
|
|
419
|
+
and `run --all` is the built-ins plus whatever other plugins register.
|
|
420
|
+
- **When `CSIS_NOTIFY_FILE` is set**, every run (successful, failed, dry or
|
|
421
|
+
real, validation-refused included, because the summary hooks run in the
|
|
422
|
+
runner's `finally`) appends one line to it and logs
|
|
423
|
+
`dummy notifier: appended run <id> to <path>` at info level. Unset, it
|
|
424
|
+
logs at debug level and does nothing.
|
|
425
|
+
- **Dry run versus real run**: the notifier writes whatever the summary
|
|
426
|
+
holds, so the line's `apply` map shows `dry-run` per lifecycle under the
|
|
427
|
+
default dry run and `executed` under `--no-dry-run`; the `notify`
|
|
428
|
+
lifecycle itself reports `no-script` either way because it defers no
|
|
429
|
+
command and so writes no runner script. With apply switched off
|
|
430
|
+
(`apply=False` in the runner) every lifecycle reports `not-attempted`.
|
|
431
|
+
- **`email_as_username: true` (the default)** makes the load refuse a user
|
|
432
|
+
whose `name` differs from its `email` (case-folded) and fill a blank name
|
|
433
|
+
from the email; `false` attaches the user as declared.
|
|
434
|
+
- **`email_domain` set** substitutes into the email template only when the
|
|
435
|
+
template contains `{{ builder.email_domain }}`; otherwise it is ignored.
|
|
436
|
+
- **`is_default: true`** on a dummy builder routes every user (or group)
|
|
437
|
+
without a `type:` to it; `false` leaves the builder with only the items
|
|
438
|
+
that name it. In a tree whose only user builder is a dummy one, it must
|
|
439
|
+
be the default or the load fails.
|
|
440
|
+
- **Declared versus not declared**: with no `type: dummy` entry the
|
|
441
|
+
builder plugin does nothing at all; the four classes sit in the registry
|
|
442
|
+
and no code path reaches them. With one declared, the load, validation
|
|
443
|
+
and identity lifecycle succeed; the group builder emits nothing and the
|
|
444
|
+
user builder one comment-only file under its own root (since stage 63;
|
|
445
|
+
the lifecycle failed at generation until 2026-09-24, and the user file
|
|
446
|
+
sat at `Dummy-tf/dummy_users.tf` until 2026-09-25).
|
|
447
|
+
- **One runtime versus another, `--only-runtime`, `--apply-runtime`**:
|
|
448
|
+
no effect. The models carry no `runtime` field and identity builders are
|
|
449
|
+
not runtime-scoped, so a scoped run treats them like any identity
|
|
450
|
+
builder.
|
|
451
|
+
- **Encrypted versus clear values**: the plugin has no encryption-specific
|
|
452
|
+
behaviour; it reads none of its own fields, so a marker in `org` or
|
|
453
|
+
`team` is decrypted or not by the core alone.
|
|
454
|
+
- **`validate` versus `run`**: both load the hook plugins, so the `notify`
|
|
455
|
+
lifecycle exists for both when the variable is set; neither exercises
|
|
456
|
+
the notifier differently.
|
|
457
|
+
|
|
458
|
+
## What it tests and verifies
|
|
459
|
+
|
|
460
|
+
The plugin adds no checks of its own: its `HookSet` has empty
|
|
461
|
+
`validators`, `after_generate`, `before_apply` and `after_apply` lists,
|
|
462
|
+
its builders validate nothing and its `validate_user()` returns nothing.
|
|
463
|
+
What is checked around it, and where the verdict lands:
|
|
464
|
+
|
|
465
|
+
- **At load (pydantic, model construction).** `org` and `team` must be
|
|
466
|
+
present (`org: Field required`); an unknown key is refused
|
|
467
|
+
(`Unexpected keyword argument`, `extra="forbid"`), and since stage 63
|
|
468
|
+
that includes `key`, `secret` and `api_host`
|
|
469
|
+
([test_v2_defects_dead.py](../../tests/test_v2_defects_dead.py),
|
|
470
|
+
`test_the_template_models_keep_org_and_team_and_refuse_credentials`); the retired
|
|
471
|
+
`parameters:` key is refused with the stage-26 message; `name` and
|
|
472
|
+
`aliases` are checked for `/`, `\` and the reserved words (`default`,
|
|
473
|
+
`self`, `none`, empty, null -- refused first where the file is read,
|
|
474
|
+
naming the file and the entry); the YAML
|
|
475
|
+
reader refuses a duplicate `name` in the list and template markers in
|
|
476
|
+
`name` or `type`. After construction the context refuses two defaults
|
|
477
|
+
(`Multiple default builders found`), no default
|
|
478
|
+
(`No default builder found in <classification> list.`), a user or
|
|
479
|
+
group naming a builder that is not declared
|
|
480
|
+
(`users builder '<type>' specified for '<name>' not configured`), and,
|
|
481
|
+
through `add_user_to_builder()`, a name/email mismatch under
|
|
482
|
+
`email_as_username`. Every one of these is a `ValueError` (pydantic's
|
|
483
|
+
`ValidationError` is one) raised while the configuration loads, before
|
|
484
|
+
any lifecycle: the CLI exits non-zero with the message; no run summary
|
|
485
|
+
is written because there is no run.
|
|
486
|
+
- **At `validate` (and at the start of every run).** The core's
|
|
487
|
+
`validate_identity_items` in
|
|
488
|
+
[identity_attributes.py](../base/src/cs_image_system/base/identity_attributes.py)
|
|
489
|
+
asks the owning builder about each user and group. For a dummy builder
|
|
490
|
+
that yields one error string per item that declares `attributes:`
|
|
491
|
+
(`user '<name>': builder <builder> (dummy) supports no attributes
|
|
492
|
+
(declared [...])`) and nothing otherwise. `validate_base_images` in
|
|
493
|
+
[v2_validation.py](../base/src/cs_image_system/base/v2_validation.py)
|
|
494
|
+
accepts `identity_types: [dummy]` on a base image exactly when a dummy
|
|
495
|
+
group builder is declared (its `identity_type()` is `dummy`), and
|
|
496
|
+
`validate_capabilities` accepts an instance image whose group is
|
|
497
|
+
attached to a dummy builder when its root base declares `dummy`. Errors
|
|
498
|
+
land as `validation: <message>` log lines, in `validation_errors` of the
|
|
499
|
+
run summary (`generated/run-summary.json`), in the run's entry in
|
|
500
|
+
`meta-state/runs.yaml` (`error: LifecycleRunError: Validation failed
|
|
501
|
+
with N error(s)`) and in the notifier's line (`"ok": false`).
|
|
502
|
+
- **At generation.** Nothing is verified. The dummy builders' hooks
|
|
503
|
+
return empty asset sets, or the user builder's one asset, and no
|
|
504
|
+
commands. The user builder's file lands under its own root,
|
|
505
|
+
`generated/identity/<builder>/user-generation/` (stage 63,
|
|
506
|
+
`test_the_dummy_user_builder_writes_under_its_own_root` in
|
|
507
|
+
[test_v2_defects_dead.py](../../tests/test_v2_defects_dead.py), which
|
|
508
|
+
also checks that no `Dummy-tf` directory appears).
|
|
509
|
+
- **At apply.** Nothing. The builders defer no commands, so no runner
|
|
510
|
+
script names them; the `notify` lifecycle has no phases and no script
|
|
511
|
+
and reports `no-script` in the summary's `apply` map.
|
|
512
|
+
- **After apply.** No `after_apply` hooks. The `on_summary` notifier runs
|
|
513
|
+
after every run, including failed ones; it never changes the verdict
|
|
514
|
+
and cannot fail the run (the runner logs a failing notifier and moves
|
|
515
|
+
on).
|
|
516
|
+
- **In the state query** ([state_query.py](../base/src/cs_image_system/base/state_query.py)):
|
|
517
|
+
`query_state()` raises `NotImplementedError`, which the query treats as
|
|
518
|
+
"not part of the picture": groups under a dummy builder appear nowhere
|
|
519
|
+
in `generated/state-report.json`, not even under `unavailable`. The
|
|
520
|
+
identity attributes probe lists them instead:
|
|
521
|
+
`groups/<name>: dummy cannot query attributes` under `unavailable`, and
|
|
522
|
+
skips `attribute_conflicts()`. The identity read-model written after
|
|
523
|
+
the identity lifecycle records each attached group with
|
|
524
|
+
`identity_type: dummy` and `gid_policy: provider-assigned`, and each
|
|
525
|
+
dummy user builder as `<name>: dummy`.
|
|
526
|
+
- **The gid shim** (`cs-image-system identity export-gids`, in
|
|
527
|
+
[identity_gids.py](../base/src/cs_image_system/base/commands/identity_gids.py))
|
|
528
|
+
does not skip it: a query naming `identity_type: dummy` finds
|
|
529
|
+
`DummyGroupBuilder`, whose `export_gids()` raises, and the command
|
|
530
|
+
prints `export-gids: DummyGroupBuilder does not implement a gid shim;
|
|
531
|
+
its terraform root must expose gids some other queryable way (N7)` on
|
|
532
|
+
stderr and exits 1. Nothing emits such a query for a dummy builder;
|
|
533
|
+
this is what would happen if something did.
|
|
534
|
+
- **Lifecycle registration** ([lifecycles.py](../base/src/cs_image_system/base/lifecycles.py)):
|
|
535
|
+
when the `notify` spec is installed, `register_lifecycle` verifies the
|
|
536
|
+
name is not a built-in, not `all`, not empty and has no `/`, that the
|
|
537
|
+
`after` anchor (here none) exists, and that a spec already registered
|
|
538
|
+
under the name is equal. Re-registering the identical spec (the tests'
|
|
539
|
+
`force=True` reload) is a no-op.
|
|
540
|
+
|
|
541
|
+
## When it fails
|
|
542
|
+
|
|
543
|
+
Failures that have happened, first.
|
|
544
|
+
|
|
545
|
+
- **2026-08-27, the `notify` lifecycle leaked into every run.** The first
|
|
546
|
+
version of [hooks.py](src/cs_image_system/dummy_plugin/hooks.py)
|
|
547
|
+
registered `notify` unconditionally; because a registered lifecycle
|
|
548
|
+
joins `run --all`, every golden and gate run of the exploration branch
|
|
549
|
+
grew a `generated/notify/` directory and a fifth lifecycle. Symptom: an
|
|
550
|
+
unexpected `notify` entry in run summaries and golden diffs. Fix: the
|
|
551
|
+
lifecycle is opt-in behind `CSIS_DUMMY_LIFECYCLE`. Recorded in
|
|
552
|
+
[docs/history/EXPLORE.md](../../docs/history/EXPLORE.md) ("Expanding
|
|
553
|
+
What Else Plugins Could Do", findings). If `notify` appears in a run you
|
|
554
|
+
did not ask for it in, the variable is set in that shell.
|
|
555
|
+
- **2026-09-08, hook plugins loaded after the lifecycle names resolved
|
|
556
|
+
(finding 62).** A fresh CLI process resolved `run --all` to the four
|
|
557
|
+
built-ins because `load_hook_plugins()` ran only inside
|
|
558
|
+
`run_lifecycles()`; every plugin-registered lifecycle (`release` then,
|
|
559
|
+
`notify` alike) was silently absent from `--all` and `run <name>` was
|
|
560
|
+
refused with `Unknown lifecycle '<name>'; expected one of [...]` and
|
|
561
|
+
exit code 2. Fix: the `run` command loads the hooks before parsing
|
|
562
|
+
names. Recorded in [docs/history/LEDGER.md](../../docs/history/LEDGER.md)
|
|
563
|
+
(Stage 1 live-run findings, 61 to 64) and in the same ledger's release
|
|
564
|
+
cycle, where the registration test of the `release` lifecycle turned
|
|
565
|
+
out to pass only after another test had loaded the hook plugins; it now
|
|
566
|
+
loads them itself. If `run notify` is refused although the variable is
|
|
567
|
+
set, check that the command path loads the hooks before it resolves
|
|
568
|
+
names (a library caller must call `load_hook_plugins()` before
|
|
569
|
+
`parse_lifecycles()`).
|
|
570
|
+
- **A declared `type: dummy` builder used to fail the identity lifecycle**
|
|
571
|
+
(reproduced over a copy of the frozen fixture on 2026-09-23; never live,
|
|
572
|
+
since no tree declares one). Both builders' `generate_items_during`
|
|
573
|
+
returned a plain `list` where the contract wants an `AssetSet`, and the
|
|
574
|
+
during step ([gen_users.py](../base/src/cs_image_system/base/commands/gen_users.py),
|
|
575
|
+
[gen_groups.py](../base/src/cs_image_system/base/commands/gen_groups.py))
|
|
576
|
+
died on `.sort_and_write()`:
|
|
577
|
+
|
|
578
|
+
```text
|
|
579
|
+
ERROR cs_image_system.base.commands.run_lifecycles:AttributeError: 'list' object has no attribute 'sort_and_write'
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
Fixed in stage 63 item 1 (2026-09-24): both return `AssetSet()`, and a
|
|
583
|
+
declared dummy builder with a group on it now loads, validates and runs
|
|
584
|
+
the identity lifecycle dry, emitting nothing
|
|
585
|
+
([tests/test_v2_defects_low_hanging.py](../../tests/test_v2_defects_low_hanging.py)).
|
|
586
|
+
- **Until 2026-09-25 the user builder wrote outside its own root.** Its
|
|
587
|
+
one file went to `generated/identity/Dummy-tf/dummy_users.tf`, beside the
|
|
588
|
+
builder directories, because it built the path from a plugin-local
|
|
589
|
+
`get_subpath()` instead of `get_path_for_phase()`; a template copied
|
|
590
|
+
from it taught the wrong layout. Stage 63 moved it to
|
|
591
|
+
`<builder>/user-generation/<builder>-user-generation-users.tf`.
|
|
592
|
+
- **Declaring `key`, `secret` or `api_host` on a dummy builder is refused
|
|
593
|
+
at load** (stage 63; accepted and ignored until 2026-09-25). pydantic
|
|
594
|
+
names the key (`Unexpected keyword argument`); delete it. The template
|
|
595
|
+
has no credential field on purpose.
|
|
596
|
+
|
|
597
|
+
## Related
|
|
598
|
+
|
|
599
|
+
- Base classes: [group_builder.py](../base/src/cs_image_system/base/models/group_builder.py), [user_builder.py](../base/src/cs_image_system/base/models/user_builder.py), [builder_base.py](../base/src/cs_image_system/base/basic/builder_base.py), [builder_base_group.py](../base/src/cs_image_system/base/basic/builder_base_group.py), [builder_base_user.py](../base/src/cs_image_system/base/basic/builder_base_user.py), [abstract_plugin_metadata.py](../base/src/cs_image_system/base/basic/abstract_plugin_metadata.py).
|
|
600
|
+
- Plugin loading: [loader.py](../base/src/cs_image_system/base/loader.py) (builder plugins), [run_lifecycles.py](../base/src/cs_image_system/base/commands/run_lifecycles.py) (hook plugins and `HookSet`), [lifecycles.py](../base/src/cs_image_system/base/lifecycles.py) (`LifecycleSpec`).
|
|
601
|
+
- The real identity plugin the fixture uses: [okta-opa-plugin](../okta-opa-plugin).
|
|
602
|
+
- [docs/PLUGINS.md](../../docs/PLUGINS.md) for the package index; [docs/DESIGN.md](../../docs/DESIGN.md) for the design.
|
|
603
|
+
- [The configuration reference](../../docs/CONFIGURATION.md) -- every field of the YAML this plugin reads, with an example.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
cs_image_system/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
cs_image_system/dummy_plugin/__init__.py,sha256=GAFRNaGWQxXquHOlCzxan4LkPZ9_NsiyswknxdPcX0c,122
|
|
3
|
+
cs_image_system/dummy_plugin/dummy_builders.py,sha256=9gTM1WDSHrga2psq-AxS2p7W4TJ0c6KJY9w_XiGU5JQ,5662
|
|
4
|
+
cs_image_system/dummy_plugin/dummy_models.py,sha256=oaeE3MJdfno7ZcrNX1aInAEUzSZs6jrAjpB8agdRcM0,1463
|
|
5
|
+
cs_image_system/dummy_plugin/hooks.py,sha256=uUT3UxE9sfuqSzcvfA3kyNxqcAW8kNI4fjWIfiunEQE,1826
|
|
6
|
+
cs_image_system/dummy_plugin/main.py,sha256=v0eQzh0Fc5qJeiaIQRAhNT8JAGUJtC-bpgRsTOsML6M,1162
|
|
7
|
+
cs_image_system_dummy_plugin-0.1.1.dist-info/METADATA,sha256=PH0VR0I4tdUgcw1iT-iwoUrxFdyLKDq8GlBdME1lwQY,36035
|
|
8
|
+
cs_image_system_dummy_plugin-0.1.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
9
|
+
cs_image_system_dummy_plugin-0.1.1.dist-info/entry_points.txt,sha256=lONdW_HhuyxOs5MxhLwF_Ujq09AgA0XjDal6APMmx8c,187
|
|
10
|
+
cs_image_system_dummy_plugin-0.1.1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
11
|
+
cs_image_system_dummy_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.
|