Skip to content

Commit 6ae2522

Browse files
[DPE-4703] - feat: external k8s access via NodePort (#110)
1 parent 76b4b38 commit 6ae2522

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+3079
-674
lines changed

config.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,7 @@ options:
108108
description: The maximum percentage of the total cpu, disk and network capacity that is allowed to be used on a broker. For example, a value of `0.8` ensures that no broker should have >80% utilization
109109
type: float
110110
default: 0.8
111+
expose-external:
112+
description: "String to determine how to expose the Kafka cluster externally from the Kubernetes cluster. Possible values: 'nodeport', 'none'"
113+
type: string
114+
default: "nodeport"
+288
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
# Copyright 2023 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+
"""Handler for the sysctl config.
16+
17+
This library allows your charm to create and configure sysctl options to the machine.
18+
19+
Validation and merge capabilities are added, for situations where more than one application
20+
are setting values. The following files can be created:
21+
22+
- /etc/sysctl.d/90-juju-<app-name>
23+
Requirements from one application requesting to configure the values.
24+
25+
- /etc/sysctl.d/95-juju-sysctl.conf
26+
Merged file resulting from all other `90-juju-*` application files.
27+
28+
29+
A charm using the sysctl lib will need a data structure like the following:
30+
```
31+
{
32+
"vm.swappiness": "1",
33+
"vm.max_map_count": "262144",
34+
"vm.dirty_ratio": "80",
35+
"vm.dirty_background_ratio": "5",
36+
"net.ipv4.tcp_max_syn_backlog": "4096",
37+
}
38+
```
39+
40+
Now, it can use that template within the charm, or just declare the values directly:
41+
42+
```python
43+
from charms.operator_libs_linux.v0 import sysctl
44+
45+
class MyCharm(CharmBase):
46+
47+
def __init__(self, *args):
48+
...
49+
self.sysctl = sysctl.Config(self.meta.name)
50+
51+
self.framework.observe(self.on.install, self._on_install)
52+
self.framework.observe(self.on.remove, self._on_remove)
53+
54+
def _on_install(self, _):
55+
# Altenatively, read the values from a template
56+
sysctl_data = {"net.ipv4.tcp_max_syn_backlog": "4096"}}
57+
58+
try:
59+
self.sysctl.configure(config=sysctl_data)
60+
except (sysctl.ApplyError, sysctl.ValidationError) as e:
61+
logger.error(f"Error setting values on sysctl: {e.message}")
62+
self.unit.status = BlockedStatus("Sysctl config not possible")
63+
except sysctl.CommandError:
64+
logger.error("Error on sysctl")
65+
66+
def _on_remove(self, _):
67+
self.sysctl.remove()
68+
```
69+
"""
70+
71+
import logging
72+
import re
73+
from pathlib import Path
74+
from subprocess import STDOUT, CalledProcessError, check_output
75+
from typing import Dict, List
76+
77+
logger = logging.getLogger(__name__)
78+
79+
# The unique Charmhub library identifier, never change it
80+
LIBID = "17a6cd4d80104d15b10f9c2420ab3266"
81+
82+
# Increment this major API version when introducing breaking changes
83+
LIBAPI = 0
84+
85+
# Increment this PATCH version before using `charmcraft publish-lib` or reset
86+
# to 0 if you are raising the major API version
87+
LIBPATCH = 4
88+
89+
CHARM_FILENAME_PREFIX = "90-juju-"
90+
SYSCTL_DIRECTORY = Path("/etc/sysctl.d")
91+
SYSCTL_FILENAME = SYSCTL_DIRECTORY / "95-juju-sysctl.conf"
92+
SYSCTL_HEADER = f"""# This config file was produced by sysctl lib v{LIBAPI}.{LIBPATCH}
93+
#
94+
# This file represents the output of the sysctl lib, which can combine multiple
95+
# configurations into a single file like.
96+
"""
97+
98+
99+
class Error(Exception):
100+
"""Base class of most errors raised by this library."""
101+
102+
@property
103+
def message(self):
104+
"""Return the message passed as an argument."""
105+
return self.args[0]
106+
107+
108+
class CommandError(Error):
109+
"""Raised when there's an error running sysctl command."""
110+
111+
112+
class ApplyError(Error):
113+
"""Raised when there's an error applying values in sysctl."""
114+
115+
116+
class ValidationError(Error):
117+
"""Exception representing value validation error."""
118+
119+
120+
class Config(Dict):
121+
"""Represents the state of the config that a charm wants to enforce."""
122+
123+
_apply_re = re.compile(r"sysctl: permission denied on key \"([a-z_\.]+)\", ignoring$")
124+
125+
def __init__(self, name: str) -> None:
126+
self.name = name
127+
self._data = self._load_data()
128+
129+
def __contains__(self, key: str) -> bool:
130+
"""Check if key is in config."""
131+
return key in self._data
132+
133+
def __len__(self):
134+
"""Get size of config."""
135+
return len(self._data)
136+
137+
def __iter__(self):
138+
"""Iterate over config."""
139+
return iter(self._data)
140+
141+
def __getitem__(self, key: str) -> str:
142+
"""Get value for key form config."""
143+
return self._data[key]
144+
145+
@property
146+
def charm_filepath(self) -> Path:
147+
"""Name for resulting charm config file."""
148+
return SYSCTL_DIRECTORY / f"{CHARM_FILENAME_PREFIX}{self.name}"
149+
150+
def configure(self, config: Dict[str, str]) -> None:
151+
"""Configure sysctl options with a desired set of params.
152+
153+
Args:
154+
config: dictionary with keys to configure:
155+
```
156+
{"vm.swappiness": "10", ...}
157+
```
158+
"""
159+
self._parse_config(config)
160+
161+
# NOTE: case where own charm calls configure() more than once.
162+
if self.charm_filepath.exists():
163+
self._merge(add_own_charm=False)
164+
165+
conflict = self._validate()
166+
if conflict:
167+
raise ValidationError(f"Validation error for keys: {conflict}")
168+
169+
snapshot = self._create_snapshot()
170+
logger.debug("Created snapshot for keys: %s", snapshot)
171+
try:
172+
self._apply()
173+
except ApplyError:
174+
self._restore_snapshot(snapshot)
175+
raise
176+
177+
self._create_charm_file()
178+
self._merge()
179+
180+
def remove(self) -> None:
181+
"""Remove config for charm.
182+
183+
The removal process won't apply any sysctl configuration. It will only merge files from
184+
remaining charms.
185+
"""
186+
self.charm_filepath.unlink(missing_ok=True)
187+
logger.info("Charm config file %s was removed", self.charm_filepath)
188+
self._merge()
189+
190+
def _validate(self) -> List[str]:
191+
"""Validate the desired config params against merged ones."""
192+
common_keys = set(self._data.keys()) & set(self._desired_config.keys())
193+
conflict_keys = []
194+
for key in common_keys:
195+
if self._data[key] != self._desired_config[key]:
196+
logger.warning(
197+
"Values for key '%s' are different: %s != %s",
198+
key,
199+
self._data[key],
200+
self._desired_config[key],
201+
)
202+
conflict_keys.append(key)
203+
204+
return conflict_keys
205+
206+
def _create_charm_file(self) -> None:
207+
"""Write the charm file."""
208+
with open(self.charm_filepath, "w") as f:
209+
f.write(f"# {self.name}\n")
210+
for key, value in self._desired_config.items():
211+
f.write(f"{key}={value}\n")
212+
213+
def _merge(self, add_own_charm=True) -> None:
214+
"""Create the merged sysctl file.
215+
216+
Args:
217+
add_own_charm : bool, if false it will skip the charm file from the merge.
218+
"""
219+
# get all files that start by 90-juju-
220+
data = [SYSCTL_HEADER]
221+
paths = set(SYSCTL_DIRECTORY.glob(f"{CHARM_FILENAME_PREFIX}*"))
222+
if not add_own_charm:
223+
paths.discard(self.charm_filepath)
224+
225+
for path in paths:
226+
with open(path, "r") as f:
227+
data += f.readlines()
228+
with open(SYSCTL_FILENAME, "w") as f:
229+
f.writelines(data)
230+
231+
# Reload data with newly created file.
232+
self._data = self._load_data()
233+
234+
def _apply(self) -> None:
235+
"""Apply values to machine."""
236+
cmd = [f"{key}={value}" for key, value in self._desired_config.items()]
237+
result = self._sysctl(cmd)
238+
failed_values = [
239+
self._apply_re.match(line) for line in result if self._apply_re.match(line)
240+
]
241+
logger.debug("Failed values: %s", failed_values)
242+
243+
if failed_values:
244+
msg = f"Unable to set params: {[f.group(1) for f in failed_values]}"
245+
logger.error(msg)
246+
raise ApplyError(msg)
247+
248+
def _create_snapshot(self) -> Dict[str, str]:
249+
"""Create a snapshot of config options that are going to be set."""
250+
cmd = ["-n"] + list(self._desired_config.keys())
251+
values = self._sysctl(cmd)
252+
return dict(zip(list(self._desired_config.keys()), values))
253+
254+
def _restore_snapshot(self, snapshot: Dict[str, str]) -> None:
255+
"""Restore a snapshot to the machine."""
256+
values = [f"{key}={value}" for key, value in snapshot.items()]
257+
self._sysctl(values)
258+
259+
def _sysctl(self, cmd: List[str]) -> List[str]:
260+
"""Execute a sysctl command."""
261+
cmd = ["sysctl"] + cmd
262+
logger.debug("Executing sysctl command: %s", cmd)
263+
try:
264+
return check_output(cmd, stderr=STDOUT, universal_newlines=True).splitlines()
265+
except CalledProcessError as e:
266+
msg = f"Error executing '{cmd}': {e.stdout}"
267+
logger.error(msg)
268+
raise CommandError(msg)
269+
270+
def _parse_config(self, config: Dict[str, str]) -> None:
271+
"""Parse a config passed to the lib."""
272+
self._desired_config = {k: str(v) for k, v in config.items()}
273+
274+
def _load_data(self) -> Dict[str, str]:
275+
"""Get merged config."""
276+
config = {}
277+
if not SYSCTL_FILENAME.exists():
278+
return config
279+
280+
with open(SYSCTL_FILENAME, "r") as f:
281+
for line in f:
282+
if line.startswith(("#", ";")) or not line.strip() or "=" not in line:
283+
continue
284+
285+
key, _, value = line.partition("=")
286+
config[key.strip()] = value.strip()
287+
288+
return config

0 commit comments

Comments
 (0)