charmlibs-interfaces-k8s-backup-target 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,184 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .coverage-*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+ cover/
54
+ .report/
55
+
56
+ # Translations
57
+ *.mo
58
+ *.pot
59
+
60
+ # Django stuff:
61
+ *.log
62
+ local_settings.py
63
+ db.sqlite3
64
+ db.sqlite3-journal
65
+
66
+ # Flask stuff:
67
+ instance/
68
+ .webassets-cache
69
+
70
+ # Scrapy stuff:
71
+ .scrapy
72
+
73
+ # Sphinx documentation
74
+ docs/_build/
75
+
76
+ # PyBuilder
77
+ .pybuilder/
78
+ target/
79
+
80
+ # Jupyter Notebook
81
+ .ipynb_checkpoints
82
+
83
+ # IPython
84
+ profile_default/
85
+ ipython_config.py
86
+
87
+ # pyenv
88
+ # For a library or package, you might want to ignore these files since the code is
89
+ # intended to run in multiple environments; otherwise, check them in:
90
+ # .python-version
91
+
92
+ # pipenv
93
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
95
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
96
+ # install all needed dependencies.
97
+ #Pipfile.lock
98
+
99
+ # UV
100
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
101
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
102
+ # commonly ignored for libraries.
103
+ #uv.lock
104
+
105
+ # poetry
106
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
107
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
108
+ # commonly ignored for libraries.
109
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
110
+ #poetry.lock
111
+
112
+ # pdm
113
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
114
+ #pdm.lock
115
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
116
+ # in version control.
117
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
118
+ .pdm.toml
119
+ .pdm-python
120
+ .pdm-build/
121
+
122
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
123
+ __pypackages__/
124
+
125
+ # Celery stuff
126
+ celerybeat-schedule
127
+ celerybeat.pid
128
+
129
+ # SageMath parsed files
130
+ *.sage.py
131
+
132
+ # Environments
133
+ .env
134
+ .venv
135
+ env/
136
+ venv/
137
+ ENV/
138
+ env.bak/
139
+ venv.bak/
140
+
141
+ # Spyder project settings
142
+ .spyderproject
143
+ .spyproject
144
+
145
+ # Rope project settings
146
+ .ropeproject
147
+
148
+ # mkdocs documentation
149
+ /site
150
+
151
+ # mypy
152
+ .mypy_cache/
153
+ .dmypy.json
154
+ dmypy.json
155
+
156
+ # Pyre type checker
157
+ .pyre/
158
+
159
+ # pytype static type analyzer
160
+ .pytype/
161
+
162
+ # Cython debug symbols
163
+ cython_debug/
164
+
165
+ # PyCharm
166
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
167
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
168
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
169
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
170
+ .idea/
171
+
172
+ # PyPI configuration file
173
+ .pypirc
174
+
175
+ # packed charms
176
+ .packed
177
+
178
+ # temporary directory for packing charms
179
+ .tmp
180
+
181
+ # uv.lock from example libraries as we don't commit these
182
+ .example/**/uv.lock
183
+ .tutorial/**/uv.lock
184
+ interfaces/.example/**/uv.lock
@@ -0,0 +1,3 @@
1
+ # 0.1.0 - 5 March 2026
2
+
3
+ Initial release of the `k8s_backup_target` interface library.
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: charmlibs-interfaces-k8s-backup-target
3
+ Version: 0.1.0
4
+ Summary: The charmlibs.interfaces.k8s-backup-target package.
5
+ Project-URL: Repository, https://github.com/canonical/charmlibs
6
+ Project-URL: Issues, https://github.com/canonical/charmlibs/issues
7
+ Author: The Charm Tech team at Canonical
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: ops
15
+ Requires-Dist: pydantic
16
+ Description-Content-Type: text/markdown
17
+
18
+ # charmlibs.interfaces.k8s_backup_target
19
+
20
+ The `k8s_backup_target` interface library.
21
+
22
+ To install, add `charmlibs-interfaces-k8s-backup-target` to your Python dependencies. Then in your Python code, import as:
23
+
24
+ ```py
25
+ from charmlibs.interfaces import k8s_backup_target
26
+ ```
27
+
28
+ See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/k8s_backup_target).
@@ -0,0 +1,11 @@
1
+ # charmlibs.interfaces.k8s_backup_target
2
+
3
+ The `k8s_backup_target` interface library.
4
+
5
+ To install, add `charmlibs-interfaces-k8s-backup-target` to your Python dependencies. Then in your Python code, import as:
6
+
7
+ ```py
8
+ from charmlibs.interfaces import k8s_backup_target
9
+ ```
10
+
11
+ See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/k8s_backup_target).
@@ -0,0 +1,60 @@
1
+ # `k8s_backup_target/v0`
2
+
3
+ ## Overview
4
+
5
+ The `k8s_backup_target` interface enables a **client charm** to specify what Kubernetes data should be backed up by relating to a **backup charm or backup integrator charm**. In practice, a client charm declares the namespaces and resource types to include or exclude in backups, and the backup charm or integrator uses this specification to forward backup requests to a backup operator. This design lets the client define backup requirements **without direct cluster elevated permissions**, delegating the backup execution to the backup operator charm.
6
+
7
+ ## Direction
8
+
9
+ This is a unidirectional interface where the provider (client) sends data and the requirer (backup charm or backup integrator) only receives data. There is no data sent back from the requirer side.
10
+
11
+ ```mermaid
12
+ flowchart TD
13
+ Provider -- endpoint, app_name, model, spec --> Requirer
14
+ ```
15
+
16
+ ## Behavior
17
+
18
+ - **Provider (Client Charm)**: Provides the backup specification via its application relation data. Typically, a client charm will set this data as soon as the relation is formed.
19
+ - **Requirer (Backup Charm or Backup Integrator Charm)**: Consumes the spec. It does not send any data over the relation. If the requirer is an integrator charm, it should listens for changes to the relation data and forward the received spec to the backup operator it is related to.
20
+
21
+ ## Relation Data
22
+
23
+ [\[Pydantic Schema\]](./schema.py)
24
+
25
+ On the provider side, the application data bag must contain structure with the following fields:
26
+
27
+ - `app` (string, required): Name of the client application that requires backups.
28
+ - `relation_name` (string, required): Name of the relation on the client side through which this spec is sent (from metadata.yaml).
29
+ - `model` (string, required): Name of the model where the client application is deployed.
30
+ - `spec` (dict, required): A dictionary defining what to back up. The spec structure is based on [Velero's resource filtering model](https://velero.io/docs/main/resource-filtering/) but is designed to be generic enough for alternative backup providers in future. This includes the following keys:
31
+ - `include_namespaces` (list of str, optional): Specific Kubernetes namespaces to include in the backup. If not provided (None), all namespaces are included.
32
+ - `include_resources` (list of str, optional): Specific Kubernetes resource kinds to include in the backup. If not provided (None), all resource types are included.
33
+ - `exclude_namespaces` (list of str, optional): Namespaces to exclude from the backup.
34
+ - `exclude_resources` (list of str, optional): Resource kinds to exclude from the backup.
35
+ - `include_cluster_resources` (bool, optional): Whether to include cluster-scoped resources in the backup.
36
+ - `label_selector` (dict, optional): A label selector to filter resources for backup.
37
+ - `ttl` (string, optional): Time-to-live duration for the backup (e.g., "72h", "30d").
38
+
39
+ ### Provider
40
+
41
+ #### Example
42
+
43
+ ```yaml
44
+ application-data:
45
+ app: my-app
46
+ relation_name: backup
47
+ model: my-model
48
+ spec:
49
+ include_namespaces: ["my-namespace"]
50
+ include_resources: ["persistentvolumeclaims", "services", "deployments"]
51
+ exclude_resources: null
52
+ exclude_namespaces: null
53
+ label_selector: {"app": "my-app"}
54
+ include_cluster_resources: false
55
+ ttl: "24h"
56
+ ```
57
+
58
+ ### Requirer
59
+
60
+ N/A
@@ -0,0 +1,16 @@
1
+ name: k8s_backup_target
2
+ version: 0
3
+ status: draft
4
+ lib: charmlibs.interfaces.k8s_backup_target
5
+ summary: Configure Kubernetes backup target specifications.
6
+ description: |
7
+ The `k8s_backup_target` interface allows client charms to specify Kubernetes backup requirements to a backup integrator charm.
8
+ The provider charm (client) provides backup specifications including namespaces, resource types, and retention policies, while the requirer charm (backup integrator) receives these specifications and forwards them to a backup operator.
9
+
10
+ requirers:
11
+ - name: velero-integrator-operator
12
+ url: https://github.com/canonical/velero-integrator-operator
13
+
14
+ providers: []
15
+
16
+ maintainer: analytics
@@ -0,0 +1 @@
1
+ ../../src/charmlibs/interfaces/k8s_backup_target/_schema.py
@@ -0,0 +1,76 @@
1
+ [project]
2
+ name = "charmlibs-interfaces-k8s-backup-target"
3
+ description = "The charmlibs.interfaces.k8s-backup-target package."
4
+ readme = "README.md"
5
+ requires-python = ">=3.10"
6
+ authors = [
7
+ {name="The Charm Tech team at Canonical"},
8
+ ]
9
+ classifiers = [
10
+ "Programming Language :: Python :: 3",
11
+ "License :: OSI Approved :: Apache Software License",
12
+ "Intended Audience :: Developers",
13
+ "Operating System :: POSIX :: Linux",
14
+ "Development Status :: 4 - Beta",
15
+ ]
16
+ dynamic = ["version"]
17
+ dependencies = [
18
+ "ops",
19
+ "pydantic",
20
+ ]
21
+
22
+ [dependency-groups]
23
+ lint = [
24
+ ]
25
+ unit = [
26
+ "ops[testing]",
27
+ "pytest",
28
+ ]
29
+ functional = [
30
+ ]
31
+ integration = [
32
+ ]
33
+
34
+ [project.urls]
35
+ "Repository" = "https://github.com/canonical/charmlibs"
36
+ "Issues" = "https://github.com/canonical/charmlibs/issues"
37
+
38
+ [build-system]
39
+ requires = ["hatchling"]
40
+ build-backend = "hatchling.build"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/charmlibs"]
44
+
45
+ [tool.hatch.version]
46
+ path = "src/charmlibs/interfaces/k8s_backup_target/_version.py"
47
+
48
+ [tool.ruff]
49
+ extend = "../../pyproject.toml"
50
+ src = ["src", "tests/unit", "tests/functional", "tests/integration"]
51
+
52
+ [tool.ruff.format]
53
+ quote-style = "preserve"
54
+
55
+ [tool.ruff.lint.extend-per-file-ignores]
56
+ "./**/schema.py" = [
57
+ "CPY", # copyright
58
+ "D", # docs
59
+ "E501", # line too long
60
+ ]
61
+ "tests/**/*" = [
62
+ "E501",
63
+ ]
64
+
65
+ [tool.pyright]
66
+ extends = "../../pyproject.toml"
67
+ include = ["src", "tests"]
68
+ pythonVersion = "3.10"
69
+
70
+ [tool.charmlibs.functional]
71
+ ubuntu = []
72
+ pebble = []
73
+ sudo = false
74
+
75
+ [tool.charmlibs.integration]
76
+ tags = []
@@ -0,0 +1,18 @@
1
+ extend = "../../pyproject.toml"
2
+
3
+ [format]
4
+ quote-style = "preserve"
5
+
6
+ [lint.extend-per-file-ignores]
7
+ "./**/schema.py" = [
8
+ "CPY", # copyright
9
+ "D", # docs
10
+ "E501", # line too long
11
+ ]
12
+ "./interface/v*/tests/*.py" = [
13
+ "CPY", # copyright
14
+ "D", # docs
15
+ "S", # security
16
+ "E501", # line too long
17
+ "F841", # assignment to unused variable
18
+ ]
@@ -0,0 +1,89 @@
1
+ # Copyright 2026 Canonical Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """K8s Backup Target library.
16
+
17
+ This library implements the Requirer and Provider roles for the ``k8s_backup_target`` relation
18
+ interface. It is used by client charms to declare backup specifications, and by backup charms or
19
+ backup integrator charms to consume them and forward to backup operators.
20
+
21
+ The ``k8s_backup_target`` interface allows a charm (the provider) to provide a declarative
22
+ description of what Kubernetes resources should be included in a backup. These specifications are
23
+ sent to the backup charm or backup integrator charm (the requirer), which merges them with schedule
24
+ configuration and forwards to the backup operator.
25
+
26
+ This interface follows a least-privilege model: client charms do not manipulate cluster resources
27
+ themselves. Instead, they define what should be backed up and leave execution to the backup
28
+ operator.
29
+
30
+ Provider Example
31
+ ================
32
+
33
+ ::
34
+
35
+ from charmlibs.interfaces.k8s_backup_target import (
36
+ K8sBackupTargetProvider,
37
+ K8sBackupTargetSpec,
38
+ )
39
+
40
+ class SomeCharm(CharmBase):
41
+ def __init__(self, *args):
42
+ # ...
43
+ self.backup = K8sBackupTargetProvider(
44
+ self,
45
+ relation_name="backup",
46
+ spec=K8sBackupTargetSpec(
47
+ include_namespaces=["my-namespace"],
48
+ include_resources=["persistentvolumeclaims", "services", "deployments"],
49
+ ttl=str(self.config["ttl"]),
50
+ ),
51
+ # Optional: refresh the data on custom events
52
+ refresh_event=[self.on.config_changed],
53
+ )
54
+
55
+ Requirer Example
56
+ ================
57
+
58
+ ::
59
+
60
+ from charmlibs.interfaces.k8s_backup_target import (
61
+ K8sBackupTargetRequirer,
62
+ )
63
+
64
+ class BackupIntegratorCharm(CharmBase):
65
+ def __init__(self, *args):
66
+ # ...
67
+ self.backup_requirer = K8sBackupTargetRequirer(self, relation_name="k8s-backup-target")
68
+
69
+ def _on_backup_action(self, event):
70
+ spec = self.backup_requirer.get_backup_spec(
71
+ app_name=event.params["app"],
72
+ endpoint=event.params["endpoint"],
73
+ model=event.params["model"],
74
+ )
75
+ ...
76
+ """
77
+
78
+ from ._backup_target import (
79
+ K8sBackupTargetProvider,
80
+ K8sBackupTargetRequirer,
81
+ )
82
+ from ._schema import K8sBackupTargetSpec
83
+ from ._version import __version__ as __version__
84
+
85
+ __all__ = [
86
+ "K8sBackupTargetProvider",
87
+ "K8sBackupTargetRequirer",
88
+ "K8sBackupTargetSpec",
89
+ ]
@@ -0,0 +1,162 @@
1
+ # Copyright 2026 Canonical Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """K8s Backup Target library implementation."""
16
+
17
+ import logging
18
+
19
+ import ops
20
+ from ops import BoundEvent, EventBase
21
+ from ops.charm import CharmBase
22
+ from ops.framework import Object
23
+
24
+ from ._schema import BackupTargetEntry, K8sBackupTargetSpec, ProviderAppData
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class K8sBackupTargetRequirer:
30
+ """Requirer class for the backup target configuration relation."""
31
+
32
+ def __init__(self, charm: CharmBase, relation_name: str):
33
+ """Initialize the requirer.
34
+
35
+ Args:
36
+ charm: The charm instance that requires backup configuration.
37
+ relation_name: The name of the relation (from metadata.yaml).
38
+ """
39
+ self._charm = charm
40
+ self._relation_name = relation_name
41
+
42
+ @staticmethod
43
+ def _load_provider_data(relation: ops.Relation) -> ProviderAppData | None:
44
+ """Load and validate provider app data from a relation."""
45
+ try:
46
+ return relation.load(ProviderAppData, relation.app)
47
+ except (ops.RelationDataError, ValueError):
48
+ return None
49
+
50
+ @property
51
+ def is_ready(self) -> bool:
52
+ """Check if the relation has valid backup target data.
53
+
54
+ Returns:
55
+ True if at least one relation has parseable backup_targets data.
56
+ """
57
+ relations = self._charm.model.relations[self._relation_name]
58
+ for relation in relations:
59
+ data = self._load_provider_data(relation)
60
+ if data and data.backup_targets:
61
+ return True
62
+ return False
63
+
64
+ def get_backup_spec(
65
+ self, app_name: str, endpoint: str, model: str
66
+ ) -> K8sBackupTargetSpec | None:
67
+ """Get a K8sBackupTargetSpec for a given (app, endpoint, model).
68
+
69
+ Args:
70
+ app_name: The name of the application for which the backup is configured.
71
+ endpoint: The name of the relation (from metadata.yaml).
72
+ model: The model name of the application.
73
+
74
+ Returns:
75
+ The backup specification if available, otherwise None.
76
+ """
77
+ relations = self._charm.model.relations[self._relation_name]
78
+
79
+ for relation in relations:
80
+ data = self._load_provider_data(relation)
81
+ if not data:
82
+ continue
83
+ for entry in data.backup_targets:
84
+ if (
85
+ entry.app == app_name
86
+ and entry.model == model
87
+ and entry.relation_name == endpoint
88
+ ):
89
+ return entry.spec
90
+
91
+ logger.warning("No backup spec found for app '%s' and endpoint '%s'", app_name, endpoint)
92
+ return None
93
+
94
+
95
+ class K8sBackupTargetProvider(Object):
96
+ """Provider class for the backup target configuration relation."""
97
+
98
+ def __init__(
99
+ self,
100
+ charm: CharmBase,
101
+ relation_name: str,
102
+ spec: K8sBackupTargetSpec,
103
+ refresh_event: BoundEvent | list[BoundEvent] | None = None,
104
+ ):
105
+ """Initialize the provider with the specified backup configuration.
106
+
107
+ Args:
108
+ charm: The charm instance that provides backup.
109
+ relation_name: The name of the relation (from metadata.yaml).
110
+ spec: The backup specification to be used.
111
+ refresh_event: Optional event(s) to trigger data sending.
112
+ """
113
+ super().__init__(charm, relation_name)
114
+ self._charm = charm
115
+ self._app_name = self._charm.app.name
116
+ self._model = self._charm.model.name
117
+ self._relation_name = relation_name
118
+ self._spec = spec
119
+
120
+ self.framework.observe(self._charm.on.leader_elected, self._send_data)
121
+ self.framework.observe(
122
+ self._charm.on[self._relation_name].relation_created, self._send_data
123
+ )
124
+ self.framework.observe(self._charm.on.upgrade_charm, self._send_data)
125
+
126
+ if refresh_event:
127
+ if not isinstance(refresh_event, tuple | list):
128
+ refresh_event = [refresh_event]
129
+ for event in refresh_event:
130
+ self.framework.observe(event, self._send_data)
131
+
132
+ def _send_data(self, event: EventBase):
133
+ """Handle any event where we should send data to the relation."""
134
+ if not self._charm.model.unit.is_leader():
135
+ logger.debug(
136
+ "K8sBackupTargetProvider handled send_data event when it is not a leader. "
137
+ "Skipping event - no data sent"
138
+ )
139
+ return
140
+
141
+ relations = self._charm.model.relations.get(self._relation_name)
142
+
143
+ if not relations:
144
+ logger.warning(
145
+ "K8sBackupTargetProvider handled send_data event but no relation '%s' found. "
146
+ "Skipping event - no data sent",
147
+ self._relation_name,
148
+ )
149
+ return
150
+
151
+ entry = BackupTargetEntry(
152
+ app=self._app_name,
153
+ relation_name=self._relation_name,
154
+ model=self._model,
155
+ spec=self._spec,
156
+ )
157
+ provider_data = ProviderAppData(
158
+ backup_targets=[entry],
159
+ )
160
+
161
+ for relation in relations:
162
+ relation.save(provider_data, self._charm.app)