Commit 62b5c41f authored by aktas's avatar aktas Committed by eminaktas
Browse files

Add Native K8s Charm for Scaling



This package built out of native_k8s_charm_*.
This commit includes terminate primitive config.
This package will be used for a robot test.

Change-Id: I472665e3cb66ef49d1a3a62d539162d198cefb03
Signed-off-by: aktas's avataraktas <emin.aktas@ulakhaberlesme.com.tr>
parent 2d41926d
Pipeline #815 passed with stage
in 1 minute and 25 seconds
......@@ -64,3 +64,6 @@
[submodule "charm-packages/proxy_native_relation_vnf/charms/simple_requires/mod/operator"]
path = charm-packages/proxy_native_relation_vnf/charms/simple_requires/mod/operator
url = https://github.com/canonical/operator.git
[submodule "charm-packages/native_k8s_scale_charm_vnf/charms/nginx-k8s/mod/operator"]
path = charm-packages/native_k8s_scale_charm_vnf/charms/nginx-k8s/mod/operator
url = https://github.com/canonical/operator
nsd:
nsd:
- description: NS with 1 KDU connected to the mgmtnet VL
df:
- id: default-df
vnf-profile:
- id: native_k8s_scale_charm-vnf
virtual-link-connectivity:
- constituent-cpd-id:
- constituent-base-element-id: native_k8s_scale_charm-vnf
constituent-cpd-id: mgmt-ext
virtual-link-profile-id: mgmtnet
vnfd-id: native_k8s_scale_charm-vnf
id: native_k8s_scale_charm-ns
name: native_k8s_scale_charm-ns
version: '1.0'
virtual-link-desc:
- id: mgmtnet
mgmt-network: true
vnfd-id:
- native_k8s_scale_charm-vnf
# Native K8S Scale Charm
## Upload packages
```bash
osm upload-package native_k8s_scale_charm_vnf
osm upload-package native_k8s_scale_charm_ns
```
## Deploy the service
```bash
osm ns-create --ns_name native_k8s_scale --nsd_name native_k8s_scale_charm-ns --vim_account <vim-account> --config '{vld: [ {name: mgmtnet, vim-network-name: <vim-network-name>} ] }'
```
## Scale the service
### Scale-out
```bash
osm vnf-scale native_k8s_scale native_k8s_scale_charm-vnf --scaling-group scale-kdu --scale-out
```
### Scale-in
```bash
osm vnf-scale native_k8s_scale native_k8s_scale_charm-vnf --scaling-group scale-kdu --scale-in
```
Subproject commit 169794cdda03d31268f0383220f965daa05c534b
description: Squid Bundle
bundle: kubernetes
applications:
nginx:
charm: './charms/nginx-k8s'
scale: 1
changecontent:
description: "Change content of default html"
params:
customtitle:
description: "New Title"
default: ""
required:
- customtitle
#!/bin/bash
CUSTOM_TITLE=`action-get customtitle`
cat > /usr/share/nginx/html/index.html <<EOF
<!DOCTYPE html>
<html>
<head>
<title>$CUSTOM_TITLE</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>$CUSTOM_TITLE</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
EOF
options:
port:
description: Zookeeper client port
type: int
default: 80
image:
description: Docker image name
type: string
default: nginx
#!/usr/bin/env python3
import sys
import logging
sys.path.append("lib")
from ops.charm import CharmBase
from ops.framework import StoredState
from ops.main import main
from ops.model import (
ActiveStatus,
MaintenanceStatus,
)
logger = logging.getLogger(__name__)
class NginxK8sCharm(CharmBase):
state = StoredState()
def __init__(self, framework, key):
super().__init__(framework, key)
self.state.set_default(spec=None)
# Observe Charm related events
self.framework.observe(self.on.config_changed, self.on_config_changed)
self.framework.observe(self.on.start, self.on_start)
self.framework.observe(self.on.upgrade_charm, self.on_upgrade_charm)
def _apply_spec(self):
# Only apply the spec if this unit is a leader.
if not self.framework.model.unit.is_leader():
return
new_spec = self.make_pod_spec()
if new_spec == self.state.spec:
return
self.framework.model.pod.set_spec(new_spec)
self.state.spec = new_spec
def make_pod_spec(self):
config = self.framework.model.config
ports = [
{
"name": "port",
"containerPort": config["port"],
"protocol": "TCP",
}
]
spec = {
"version": 2,
"containers": [
{
"name": self.framework.model.app.name,
"image": "{}".format(config["image"]),
"ports": ports,
}
],
}
return spec
def on_config_changed(self, event):
"""Handle changes in configuration"""
unit = self.model.unit
unit.status = MaintenanceStatus("Applying new pod spec")
self._apply_spec()
unit.status = ActiveStatus("Ready")
def on_start(self, event):
"""Called when the charm is being installed"""
unit = self.model.unit
unit.status = MaintenanceStatus("Applying pod spec")
self._apply_spec()
unit.status = ActiveStatus("Ready")
def on_upgrade_charm(self, event):
"""Upgrade the charm."""
unit = self.model.unit
unit.status = MaintenanceStatus("Upgrading charm")
self.on_start(event)
if __name__ == "__main__":
main(NginxK8sCharm)
# Copyright 2020 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The Operator Framework."""
__version__ = '0.6.1'
# Import here the bare minimum to break the circular import between modules
from . import charm # NOQA
# Copyright 2020 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
from functools import total_ordering
@total_ordering
class JujuVersion:
PATTERN = r'''^
(?P<major>\d{1,9})\.(?P<minor>\d{1,9}) # <major> and <minor> numbers are always there
((?:\.|-(?P<tag>[a-z]+))(?P<patch>\d{1,9}))? # sometimes with .<patch> or -<tag><patch>
(\.(?P<build>\d{1,9}))?$ # and sometimes with a <build> number.
'''
def __init__(self, version):
m = re.match(self.PATTERN, version, re.VERBOSE)
if not m:
raise RuntimeError('"{}" is not a valid Juju version string'.format(version))
d = m.groupdict()
self.major = int(m.group('major'))
self.minor = int(m.group('minor'))
self.tag = d['tag'] or ''
self.patch = int(d['patch'] or 0)
self.build = int(d['build'] or 0)
def __repr__(self):
if self.tag:
s = '{}.{}-{}{}'.format(self.major, self.minor, self.tag, self.patch)
else:
s = '{}.{}.{}'.format(self.major, self.minor, self.patch)
if self.build > 0:
s += '.{}'.format(self.build)
return s
def __eq__(self, other):
if self is other:
return True
if isinstance(other, str):
other = type(self)(other)
elif not isinstance(other, JujuVersion):
raise RuntimeError('cannot compare Juju version "{}" with "{}"'.format(self, other))
return (
self.major == other.major
and self.minor == other.minor
and self.tag == other.tag
and self.build == other.build
and self.patch == other.patch)
def __lt__(self, other):
if self is other:
return False
if isinstance(other, str):
other = type(self)(other)
elif not isinstance(other, JujuVersion):
raise RuntimeError('cannot compare Juju version "{}" with "{}"'.format(self, other))
if self.major != other.major:
return self.major < other.major
elif self.minor != other.minor:
return self.minor < other.minor
elif self.tag != other.tag:
if not self.tag:
return False
elif not other.tag:
return True
return self.tag < other.tag
elif self.patch != other.patch:
return self.patch < other.patch
elif self.build != other.build:
return self.build < other.build
return False
# Copyright 2020 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
import os
import re
from ast import literal_eval
from importlib.util import module_from_spec
from importlib.machinery import ModuleSpec
from pkgutil import get_importer
from types import ModuleType
from typing import Tuple, Dict, List, Iterator, Optional
_libraries = {} # type: Dict[Tuple[str,str], List[_Lib]]
_libline_re = re.compile(r'''^LIB([A-Z]+)\s+=\s+([0-9]+|['"][a-zA-Z0-9_.-@]+['"])\s*$''')
_libname_re = re.compile(r'''^[a-z][a-z0-9]+$''')
# Not perfect, but should do for now.
_libauthor_re = re.compile(r'''^[A-Za-z0-9_+.-]+@[a-z0-9_-]+(?:\.[a-z0-9_-]+)*\.[a-z]{2,3}$''')
def use(name: str, api: int, author: str) -> ModuleType:
"""Use a library from the ops libraries.
Args:
name: the name of the library requested.
api: the API version of the library.
author: the author of the library. If not given, requests the
one in the standard library.
Raises:
ImportError: if the library cannot be found.
TypeError: if the name, api, or author are the wrong type.
ValueError: if the name, api, or author are invalid.
"""
if not isinstance(name, str):
raise TypeError("invalid library name: {!r} (must be a str)".format(name))
if not isinstance(author, str):
raise TypeError("invalid library author: {!r} (must be a str)".format(author))
if not isinstance(api, int):
raise TypeError("invalid library API: {!r} (must be an int)".format(api))
if api < 0:
raise ValueError('invalid library api: {} (must be ≥0)'.format(api))
if not _libname_re.match(name):
raise ValueError("invalid library name: {!r} (chars and digits only)".format(name))
if not _libauthor_re.match(author):
raise ValueError("invalid library author email: {!r}".format(author))
versions = _libraries.get((name, author), ())
for lib in versions:
if lib.api == api:
return lib.import_module()
others = ', '.join(str(lib.api) for lib in versions)
if others:
msg = 'cannot find "{}" from "{}" with API version {} (have {})'.format(
name, author, api, others)
else:
msg = 'cannot find library "{}" from "{}"'.format(name, author)
raise ImportError(msg, name=name)
def autoimport():
_libraries.clear()
for spec in _find_all_specs(sys.path):
lib = _parse_lib(spec)
if lib is None:
continue
versions = _libraries.setdefault((lib.name, lib.author), [])
versions.append(lib)
versions.sort(reverse=True)
def _find_all_specs(path: List[str]) -> Iterator[ModuleSpec]:
for sys_dir in path:
if sys_dir == "":
sys_dir = "."
try:
top_dirs = os.listdir(sys_dir)
except OSError:
continue
for top_dir in top_dirs:
opslib = os.path.join(sys_dir, top_dir, 'opslib')
try:
lib_dirs = os.listdir(opslib)
except OSError:
continue
finder = get_importer(opslib)
if finder is None or not hasattr(finder, 'find_spec'):
continue
for lib_dir in lib_dirs:
spec = finder.find_spec(lib_dir)
if spec is None:
continue
if spec.loader is None:
# a namespace package; not supported
continue
yield spec
# only the first this many lines of a file are looked at for the LIB* constants
_MAX_LIB_LINES = 99
def _parse_lib(spec: ModuleSpec) -> Optional['_Lib']:
if spec.origin is None:
return None
_expected = {'NAME': str, 'AUTHOR': str, 'API': int, 'PATCH': int}
try:
with open(spec.origin, 'rt', encoding='utf-8') as f:
libinfo = {}
for n, line in enumerate(f):
if len(libinfo) == len(_expected):
break
if n > _MAX_LIB_LINES:
return None
m = _libline_re.match(line)
if m is None:
continue
key, value = m.groups()
if key in _expected:
value = literal_eval(value)
if not isinstance(value, _expected[key]):
return None
libinfo[key] = value
else:
if len(libinfo) != len(_expected):
return None
except Exception:
return None
return _Lib(spec, libinfo['NAME'], libinfo['AUTHOR'], libinfo['API'], libinfo['PATCH'])
class _Lib:
def __init__(self, spec: ModuleSpec, name: str, author: str, api: int, patch: int):
self.spec = spec
self.name = name
self.author = author
self.api = api
self.patch = patch
self._module = None # type: Optional[ModuleType]
def __repr__(self):
return "<_Lib {0.name} by {0.author}, API {0.api}, patch {0.patch}>".format(self)
def import_module(self) -> ModuleType:
if self._module is None:
module = module_from_spec(self.spec)
self.spec.loader.exec_module(module)
self._module = module
return self._module
def __eq__(self, other):
if not isinstance(other, _Lib):
return NotImplemented
a = (self.name, self.author, self.api, self.patch)
b = (other.name, other.author, other.api, other.patch)
return a == b
def __lt__(self, other):
if not isinstance(other, _Lib):
return NotImplemented
a = (self.name, self.author, self.api, self.patch)
b = (other.name, other.author, other.api, other.patch)
return a < b
# Copyright 2020 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
class JujuLogHandler(logging.Handler):
"""A handler for sending logs to Juju via juju-log."""
def __init__(self, model_backend, level=logging.DEBUG):
super().__init__(level)
self.model_backend = model_backend
def emit(self, record):
self.model_backend.juju_log(record.levelname, self.format(record))
def setup_root_logging(model_backend, debug=False):
"""Setup python logging to forward messages to juju-log.
By default, logging is set to DEBUG level, and messages will be filtered by Juju.
Charmers can also set their own default log level with::
logging.getLogger().setLevel(logging.INFO)
model_backend -- a ModelBackend to use for juju-log
debug -- if True, write logs to stderr as well as to juju-log.
"""
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
logger.addHandler(JujuLogHandler(model_backend))
if debug:
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
#!/usr/bin/env python3
# Copyright 2019 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import subprocess
import sys
from pathlib import Path
import yaml
import ops.charm
import ops.framework
import ops.model
import logging
from ops.log import setup_root_logging
CHARM_STATE_FILE = '.unit-state.db'
logger = logging.getLogger()
def _get_charm_dir():
charm_dir = os.environ.get("JUJU_CHARM_DIR")
if charm_dir is None:
# Assume $JUJU_CHARM_DIR/lib/op/main.py structure.
charm_dir = Path('{}/../../..'.format(__file__)).resolve()
else:
charm_dir = Path(charm_dir).resolve()
return charm_dir
def _load_metadata(charm_dir):
metadata = yaml.safe_load((charm_dir / 'metadata.yaml').read_text())
actions_meta = charm_dir / 'actions.yaml'
if actions_meta.exists():
actions_metadata = yaml.safe_load(actions_meta.read_text())
else:
actions_metadata = {}
return metadata, actions_metadata
def _create_event_link(charm, bound_event):
"""Create a symlink for a particular event.
charm -- A charm object.
bound_event -- An event for which to create a symlink.
"""
if issubclass(bound_event.event_type, ops.charm.HookEvent):
event_dir = charm.framework.charm_dir / 'hooks'
event_path = event_dir / bound_event.event_kind.replace('_', '-')
elif issubclass(bound_event.event_type, ops.charm.ActionEvent):
if not bound_event.event_kind.endswith("_action"):
raise RuntimeError(
'action event name {} needs _action suffix'.format(bound_event.event_kind))
event_dir = charm.framework.charm_dir / 'actions'
# The event_kind is suffixed with "_action" while the executable is not.
event_path = event_dir / bound_event.event_kind[:-len('_action')].replace('_', '-')
else:
raise RuntimeError(
'cannot create a symlink: unsupported event type {}'.format(bound_event.event_type))
event_dir.mkdir(exist_ok=True)
if not event_path.exists():
# CPython has different implementations for populating sys.argv[0] for Linux and Windows.
# For Windows it is always an absolute path (any symlinks are resolved)
# while for Linux it can be a relative path.
target_path = os.path.relpath(os.path.realpath(sys.argv[0]), str(event_dir))
# Ignore the non-symlink files or directories
# assuming the charm author knows what they are doing.
logger.debug(
'Creating a new relative symlink at %s pointing to %s',
event_path, target_path)
event_path.symlink_to(target_path)
def _setup_event_links(charm_dir, charm):
"""Set up links for supported events that originate from Juju.
Whether a charm can handle an event or not can be determined by
introspecting which events are defined on it.
Hooks or actions are created as symlinks to the charm code file
which is determined by inspecting symlinks provided by the charm
author at hooks/install or hooks/start.
charm_dir -- A root directory of the charm.
charm -- An instance of the Charm class.
"""
for bound_event in charm.on.events().values():
# Only events that originate from Juju need symlinks.
if issubclass(bound_event.event_type, (ops.charm.HookEvent, ops.charm.ActionEvent)):
_create_event_link(charm, bound_event)
def _emit_charm_event(charm, event_name):
"""Emits a charm event based on a Juju event name.
charm -- A charm instance to emit an event from.
event_name -- A Juju event name to emit on a charm.
"""
event_to_emit = None
try:
event_to_emit = getattr(charm.on, event_name)
except AttributeError:
logger.debug("Event %s not defined for %s.", event_name, charm)
# If the event is not supported by the charm implementation, do
# not error out or try to emit it. This is to support rollbacks.
if event_to_emit is not None:
args, kwargs = _get_event_args(charm, event_to_emit)
logger.debug('Emitting Juju event %s.', event_name)
event_to_emit.emit(*args, **kwargs)
def _get_event_args(charm, bound_event):
event_type = bound_event.event_type
model = charm.framework.model
if issubclass(event_type, ops.charm.RelationEvent):
relation_name = os.environ['JUJU_RELATION']
relation_id = int(os.environ['JUJU_RELATION_ID'].split(':')[-1])
relation = model.get_relation(relation_name, relation_id)
else:
relation = None
remote_app_name = os.environ.get('JUJU_REMOTE_APP', '')
remote_unit_name = os.environ.get('JUJU_REMOTE_UNIT', '')
if remote_app_name or remote_unit_name:
if not remote_app_name:
if '/' not in remote_unit_name:
raise RuntimeError('invalid remote unit name: {}'.format(remote_unit_name))
remote_app_name = remote_unit_name.split('/')[0]
args = [relation, model.get_app(remote_app_name)]
if remote_unit_name:
args.append(model.get_unit(remote_unit_name))
return args, {}
elif relation:
return [relation], {}
return [], {}
class _Dispatcher:
"""Encapsulate how to figure out what event Juju wants us to run.
Also knows how to run “legacy” hooks when Juju called us via a top-level
``dispatch`` binary.
Args:
charm_dir: the toplevel directory of the charm
Attributes:
event_name: the name of the event to run
is_dispatch_aware: are we running under a Juju that knows about the
dispatch binary?
"""
def __init__(self, charm_dir: Path):
self._charm_dir = charm_dir
self._exec_path = Path(sys.argv[0])
if 'JUJU_DISPATCH_PATH' in os.environ and (charm_dir / 'dispatch').exists():
self._init_dispatch()
else:
self._init_legacy()
def ensure_event_links(self, charm):
"""Make sure necessary symlinks are present on disk"""
if self.is_dispatch_aware:
# links aren't needed
return
# When a charm is force-upgraded and a unit is in an error state Juju
# does not run upgrade-charm and instead runs the failed hook followed
# by config-changed. Given the nature of force-upgrading the hook setup
# code is not triggered on config-changed.
#
# 'start' event is included as Juju does not fire the install event for
# K8s charms (see LP: #1854635).
if (self.event_name in ('install', 'start', 'upgrade_charm')
or self.event_name.endswith('_storage_attached')):
_setup_event_links(self._charm_dir, charm)
def run_any_legacy_hook(self):
"""Run any extant legacy hook.
If there is both a dispatch file and a legacy hook for the
current event, run the wanted legacy hook.
"""
if not self.is_dispatch_aware:
# we *are* the legacy hook
return
dispatch_path = self._charm_dir / self._dispatch_path
if not dispatch_path.exists():
logger.debug("Legacy %s does not exist.", self._dispatch_path)
return
# super strange that there isn't an is_executable
if not os.access(str(dispatch_path), os.X_OK):
logger.warning("Legacy %s exists but is not executable.", self._dispatch_path)
return
if dispatch_path.resolve() == self._exec_path.resolve():
logger.debug("Legacy %s is just a link to ourselves.", self._dispatch_path)
return
argv = sys.argv.copy()
argv[0] = str(dispatch_path)
logger.info("Running legacy %s.", self._dispatch_path)
try:
subprocess.run(argv, check=True)
except subprocess.CalledProcessError as e:
logger.warning(
"Legacy %s exited with status %d.",
self._dispatch_path, e.returncode)
sys.exit(e.returncode)
else:
logger.debug("Legacy %s exited with status 0.", self._dispatch_path)
def _set_name_from_path(self, path: Path):
"""Sets the name attribute to that which can be inferred from the given path."""
name = path.name.replace('-', '_')
if path.parent.name == 'actions':
name = '{}_action'.format(name)
self.event_name = name
def _init_legacy(self):
"""Set up the 'legacy' dispatcher.
The current Juju doesn't know about 'dispatch' and calls hooks
explicitly.
"""
self.is_dispatch_aware = False
self._set_name_from_path(self._exec_path)
def _init_dispatch(self):
"""Set up the new 'dispatch' dispatcher.
The current Juju will run 'dispatch' if it exists, and otherwise fall
back to the old behaviour.
JUJU_DISPATCH_PATH will be set to the wanted hook, e.g. hooks/install,
in both cases.
"""
self._dispatch_path = Path(os.environ['JUJU_DISPATCH_PATH'])
if 'OPERATOR_DISPATCH' in os.environ:
logger.debug("Charm called itself via %s.", self._dispatch_path)
sys.exit(0)
os.environ['OPERATOR_DISPATCH'] = '1'
self.is_dispatch_aware = True
self._set_name_from_path(self._dispatch_path)
def main(charm_class):
"""Setup the charm and dispatch the observed event.
The event name is based on the way this executable was called (argv[0]).
"""
charm_dir = _get_charm_dir()
model_backend = ops.model._ModelBackend()
debug = ('JUJU_DEBUG' in os.environ)
setup_root_logging(model_backend, debug=debug)
dispatcher = _Dispatcher(charm_dir)
dispatcher.run_any_legacy_hook()
metadata, actions_metadata = _load_metadata(charm_dir)
meta = ops.charm.CharmMeta(metadata, actions_metadata)
unit_name = os.environ['JUJU_UNIT_NAME']
model_name = os.environ.get('JUJU_MODEL_NAME')
model = ops.model.Model(unit_name, meta, model_backend, model_name=model_name)
# TODO: If Juju unit agent crashes after exit(0) from the charm code
# the framework will commit the snapshot but Juju will not commit its
# operation.
charm_state_path = charm_dir / CHARM_STATE_FILE
framework = ops.framework.Framework(charm_state_path, charm_dir, meta, model)
try:
charm = charm_class(framework, None)
dispatcher.ensure_event_links(charm)
# TODO: Remove the collect_metrics check below as soon as the relevant
# Juju changes are made.
#
# Skip reemission of deferred events for collect-metrics events because
# they do not have the full access to all hook tools.
if dispatcher.event_name != 'collect_metrics':
framework.reemit()
_emit_charm_event(charm, dispatcher.event_name)
framework.commit()
finally:
framework.close()
name: nginx-k8s
summary: A nginx charm
description: |
Describe your charm here
series:
- kubernetes
min-juju-version: 2.7.0
deployment:
type: stateless
service: loadbalancer
#!/usr/bin/env python3
import sys
import logging
sys.path.append("lib")
from ops.charm import CharmBase
from ops.framework import StoredState
from ops.main import main
from ops.model import (
ActiveStatus,
MaintenanceStatus,
)
logger = logging.getLogger(__name__)
class NginxK8sCharm(CharmBase):
state = StoredState()
def __init__(self, framework, key):
super().__init__(framework, key)
self.state.set_default(spec=None)
# Observe Charm related events
self.framework.observe(self.on.config_changed, self.on_config_changed)
self.framework.observe(self.on.start, self.on_start)
self.framework.observe(self.on.upgrade_charm, self.on_upgrade_charm)
def _apply_spec(self):
# Only apply the spec if this unit is a leader.
if not self.framework.model.unit.is_leader():
return
new_spec = self.make_pod_spec()
if new_spec == self.state.spec:
return
self.framework.model.pod.set_spec(new_spec)
self.state.spec = new_spec
def make_pod_spec(self):
config = self.framework.model.config
ports = [
{
"name": "port",
"containerPort": config["port"],
"protocol": "TCP",
}
]
spec = {
"version": 2,
"containers": [
{
"name": self.framework.model.app.name,
"image": "{}".format(config["image"]),
"ports": ports,
}
],
}
return spec
def on_config_changed(self, event):
"""Handle changes in configuration"""
unit = self.model.unit
unit.status = MaintenanceStatus("Applying new pod spec")
self._apply_spec()
unit.status = ActiveStatus("Ready")
def on_start(self, event):
"""Called when the charm is being installed"""
unit = self.model.unit
unit.status = MaintenanceStatus("Applying pod spec")
self._apply_spec()
unit.status = ActiveStatus("Ready")
def on_upgrade_charm(self, event):
"""Upgrade the charm."""
unit = self.model.unit
unit.status = MaintenanceStatus("Upgrading charm")
self.on_start(event)
if __name__ == "__main__":
main(NginxK8sCharm)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment