Git Product home page Git Product logo

operator's Introduction

The ops library

CI Status Publish

The ops library is a Python framework for developing and testing Kubernetes and machine charms. While charms can be written in any language, ops defines the latest standard, and charmers are encouraged to use Python with ops for all charms. The library is an official component of the Charm SDK, itself a part of the Juju universe.

Juju Learn how to quickly deploy, integrate, and manage charms on any cloud with Juju.
It's as simple as juju deploy foo, juju integrate foo bar, and so on -- on any cloud.
Charmhub Sample our existing charms on Charmhub.
A charm can be a cluster (OpenStack, Kubernetes), a data platform (PostgreSQL, MongoDB, etc.), an observability stack (Canonical Observability Stack), an MLOps solution (Kubeflow), and so much more.
👉 Charm SDK Write your own charm!
Juju is written in Go, but our SDK supports easy charm development in Python.

Give it a try

Let's use ops to build a Kubernetes charm:

Set up

See Charm SDK | Set up an Ubuntu charm-dev VM with Multipass.
Choose the MicroK8s track.

Write your charm

On your Multipass VM, create a charm directory and use Charmcraft to initialise your charm file structure:

mkdir ops-example
cd ops-example
charmcraft init

This has created a standard charm directory structure:

$ ls -R
.:
CONTRIBUTING.md  README.md        pyproject.toml    src    tox.ini
LICENSE          charmcraft.yaml  requirements.txt  tests

./src:
charm.py

./tests:
integration  unit

./tests/integration:
test_charm.py

./tests/unit:
test_charm.py

Things to note:

  • The charmcraft.yaml file shows that what we have is an example charm called ops-example, which uses an OCI image resource httpbin from kennethreitz/httpbin.

  • The requirements.txt file lists the version of ops to use.

  • The src/charm.py file imports ops and uses ops constructs to create a charm class OpsExampleCharm, observe Juju events, and pair them to event handlers:

import ops

class OpsExampleCharm(ops.CharmBase):
    """Charm the service."""

    def __init__(self, *args):
        super().__init__(*args)
        self.framework.observe(self.on['httpbin'].pebble_ready, self._on_httpbin_pebble_ready)
        self.framework.observe(self.on.config_changed, self._on_config_changed)

    def _on_httpbin_pebble_ready(self, event: ops.PebbleReadyEvent):
        """Define and start a workload using the Pebble API.

        Change this example to suit your needs. You'll need to specify the right entrypoint and
        environment configuration for your specific workload.

        Learn more about interacting with Pebble at at https://juju.is/docs/sdk/pebble.
        """
        # Get a reference the container attribute on the PebbleReadyEvent
        container = event.workload
        # Add initial Pebble config layer using the Pebble API
        container.add_layer("httpbin", self._pebble_layer, combine=True)
        # Make Pebble reevaluate its plan, ensuring any services are started if enabled.
        container.replan()
        # Learn more about statuses in the SDK docs:
        # https://juju.is/docs/sdk/constructs#heading--statuses
        self.unit.status = ops.ActiveStatus()

See more: ops.PebbleReadyEvent

  • The tests/unit/test_charm.py file imports ops.testing and uses it to set up a testing harness:
import ops.testing

class TestCharm(unittest.TestCase):
    def setUp(self):
        self.harness = ops.testing.Harness(OpsExampleCharm)
        self.addCleanup(self.harness.cleanup)
        self.harness.begin()

    def test_httpbin_pebble_ready(self):
        # Expected plan after Pebble ready with default config
        expected_plan = {
            "services": {
                "httpbin": {
                    "override": "replace",
                    "summary": "httpbin",
                    "command": "gunicorn -b 0.0.0.0:80 httpbin:app -k gevent",
                    "startup": "enabled",
                    "environment": {"GUNICORN_CMD_ARGS": "--log-level info"},
                }
            },
        }
        # Simulate the container coming up and emission of pebble-ready event
        self.harness.container_pebble_ready("httpbin")
        # Get the plan now we've run PebbleReady
        updated_plan = self.harness.get_container_pebble_plan("httpbin").to_dict()
        # Check we've got the plan we expected
        self.assertEqual(expected_plan, updated_plan)
        # Check the service was started
        service = self.harness.model.unit.get_container("httpbin").get_service("httpbin")
        self.assertTrue(service.is_running())
        # Ensure we set an ActiveStatus with no message
        self.assertEqual(self.harness.model.unit.status, ops.ActiveStatus())

See more: ops.testing.Harness

Explore further, start editing the files, or skip ahead and pack the charm:

charmcraft pack

If you didn't take any wrong turn or simply left the charm exactly as it was, this has created a file called ops-example_ubuntu-22.04-amd64.charm (the architecture bit may be different depending on your system's architecture). Use this name and the resource from the metadata.yaml to deploy your example charm to your local MicroK8s cloud:

juju deploy ./ops-example_ubuntu-22.04-amd64.charm --resource httpbin-image=kennethreitz/httpbin

Congratulations, you’ve just built your first Kubernetes charm using ops!

Clean up

See Charm SDK | Clean up.

Next steps

Learn more

Chat with us

Read our Code of conduct and:

File an issue

Make your mark

operator's People

Contributors

abuelodelanada avatar amandahla avatar balbirthomas avatar benhoyt avatar carlcsaposs-canonical avatar chipaca avatar dependabot[bot] avatar dimaqq avatar dshcherb avatar dstathis avatar facundobatista avatar hpidcock avatar ironcore864 avatar jameinel avatar jnsgruk avatar johnsca avatar markshuttle avatar mthaddon avatar niemeyer avatar pengale avatar pietropasotti avatar rbarry82 avatar rwcarlsen avatar sed-i avatar tinvaan avatar tmihoc avatar tonyandrewmeyer avatar vultaire avatar weiiwang01 avatar yanksyoon avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

operator's Issues

Framework Attribute Name

self.framework seems like a long variable to type to me:

  • self.framework.observe(self.on.config_changed, self)
  • self.framework.model.config['server_name']

I would like to discuss potential shorter alternatives before this is carved in stone - maybe we can come up with something short and straightforward.

I think of this framework as:

  • A charm runtime library;
  • A base library for building charms.

Thus, I have the following candidates:

  • self.rt
    • self.rtl ~ "Runtime Library"
    • self.rte ~ "Runtime Environment"
    • self.crt ~ "Charm Runtime"
  • self.base
  • self.f and self.fw crossed my mind as contractions of framework as well.

self.rt is short and the two characters are next to each other on a QWERTY keyboard.
self.base has a potential of being confused with the base class concept.
self.f and self.fw correspond to the current type name Framework.

As this is a rather subjective topic any feedback is appreciated.

Rename project "charmed_ops"

The project's name has gone through lots of churn already, but it's still not right. ops is far too generic. It will be impossible to search for and difficult to describe to others. charmed_ops, would work well. It's easy to say, retains the "ops"/"operator" motif, stays on brand with the rest of Canonical's offerings and says exactly what it does.

Are application relation settings inherantly racy?

I've been pondering code like this:

class MyCharm(CharmBase):
    def __init__(self, *args, **kw):
        super().__init__(*args, **kw)
        self.framework.observe(self.on.my_relation_joined, self)

    def on_my_relation_joined(self, event):
        if self.model.unit.is_leader():
            event.relation[event.unit.app]['secret'] = mkpasswd(128)

I very much like the idea of application level relation settings, as it avoids a lot of mess dealing with eventual consistency. And I think the above is how the operator framework guides charm authors to set this data. My concern is that I believe it is possible for all units to run the on_my_relation_joined handler while none of them are actually leader, and the setting never gets set. Unlike the reactive framework, which encourages charm authors to decouple this sort of thing from hooks, and ends up with different race condition that are not relevant here.

When running the example charm, if the leader becomes unavailable when a relation is being added (netsplit, reboot), then other units will run their joined hooks while leadership remains with the isolated unit. A period later, one of the remaining units will gain leadership after some time. When the deposed leader again becomes available, it will run the joined hook as non-leader. All units have run the joined hook, and none of them set the application relation setting.

Does Juju actually prevent this somehow? Could it?

Do we need charms to be provably correct? The above race is certainly rather unlikely to occur, highly improbable outside of a test case.

One way of addressing the issue is to defer the relation-joined event:

    def on_my_relation_joined(self, event):
        if self.model.unit.is_leader():
            event.relation[event.unit.app]['secret'] = mkpasswd(128)
        elif event.relation[event.unit.app].get('secret') is None:
            event.defer()

Can the operator framework guide charm authors to correct code, even if only in documentation? There is a push for more code to only be run on lead units, so far relation application settings and pod-set-spec, and I have encountered developers tripping over both of these and neglecting the 'is_leader()' guard. Perhaps the charm code to be run on the leader could be decoupled from the charm code to be run on all units? Or is this solution worse than the problem?

class MyCharm(CharmBase):
    [...]

class MyCharmLeader(CharmLeader):
    [...]
    def on_my_relation_joined(self, event):
        event.relation[event.unit.app]['secret'] = mkpasswd(128)

Project needs setup.py; otherwise IDEs are unable to import it

The end-user ergonomics of the library are significantly impaired because of how it needs to be vendored into a charm's source code. Despite my best efforts with sys.path hacking, git subtree and symbolic links, I haven't been able to convince PyCharm to believe that ops is a valid Python package.

Defer important status messages until commit?

A common problem with charms has been ensuring workload_status contains the most important information, and not overwritten. For example, ensuring that if charm code puts a unit into a 'blocked' status then later code does not overwrite it with a 'maintenance' message or worse 'active' status.

With the operator framework, it seems the common cause of this will be deferred events. For example, a handler for a relation-changed event may need to set the status to 'waiting' because the other end is not yet ready. Other events may also be emitted (in this hook or subsequent), and their handlers may overwrite a more important 'waiting' or 'blocked' status with maintenance messages, and the end user ends up unaware why their deployment has hung.

An approach used by a charms.reactive layer (layer-status) was to only set 'maintenance' status immediately. Maintenance status allowed the charm to inform the user about what operations it is doing. If the charm set blocked, waiting or maintenance then the message stored instead of emitted via status-set. At the end of the hook, the most important message was emitted. Blocked messages had highest priority, followed by waiting, and active only emitted if no blocked or waiting messages were set by the charm. I found this approach to work very well, pushing for its use to ensure problems with charm deployments were always visible, without a lot of complexity or tight coupling. It was also the only sane way to ensure that workload_status states set by shared code did not get obliterated by charm code. Shared code could set a blocked status, and know it or another blocked status got escalated to the end user. Without it, shared code had to instead fail and put the charm into an error state.

I'm currently looking at the following implementation to add this behavior to my operator framework charm, and thought I'd open the issue here in case the operator framework could provide similar behavior to all charms If provided by the operator framework, then shared code such as relation implementations could also rely on the behavior, rather than fighting with the charm for control of the workload_status:

class MyCharm(ops.charm.CharmBase):
    def __init__(self, *args):
        super().__init__(*args)
        self.framework.observe(self.framework.on.pre_commit, self)

    _active_msg = None
    _block_msg = None
    _wait_msg = None

    def set_active(self, msg):
        # Active status is deferred until commit, overriding maintenance only.
        self._active_msg = msg

    def set_blocked(self, msg):
        # Blocked status is deferred until commit, overriding all other status
        self._block_msg = msg

    def set_waiting(self, msg):
        # Waiting status is deferred until commit, overriding maintenance and active
        self._wait_msg = msg

    def set_maintenance(self, msg):
        # Maintenance status is emitted immediately, informing end user of charm activity
        self.unit.status = ops.model.MaintenanceStatus(msg)

    def update_status(self):
        if self._block_msg is not None:
            self.unit.status = ops.model.BlockedStatus(self._block_msg)
        elif self._wait_msg is not None:
            self.unit.status = ops.model.WaitingStatus(self._wait_msg)
        elif self._active_msg is not None:
            self.unit.status = ops.model.ActiveStatus(self._active_msg)

RFC: Grafana and Prometheus Charms

Hi, apologies if this is the wrong place for this but I could not find a mailing list or a discourse site where this would be more appropriate to discuss. I finished the first iteration of the operators for prom and grafana on k8s and I’m submitting it here for the community to scrutinize: https://youtu.be/bf-YClFjANw

Links to the repos are in the video description.

Missing README/docs link

In order to know how to get started with the operator framework, one needs to follow documentation. I don't know if the documentation exists, or if it does, where it lives (I would expect it to be on rtfd.org but don't mind as long as there's a link).

Framework source not imported when deploying on microk8s

(Not sure if this is the right place to post this as I don't know if we have a chat room for this project. Apologies in advance if this is spam)

I'm getting my hands dirty with this framework (quite happily, fyi). Right now, I'm seeing that the framework (as git submodule) isn't being downloaded into the operator pod:

$ kubectl exec -it -n demo-charm demo-charm-operator-0 /bin/sh
# cd /var/lib/juju/agents/application-demo-charm/charm
# ls
README.md  config.yaml  hooks  lib  metadata.yaml  mod  revision  src  version
# ls -l mod/operator/ops
ls: cannot access 'mod/operator/ops': No such file or directory
# ls -l mod
total 4
drwxr-xr-x 2 root root 4096 Jan 29 04:58 operator
# ls -l mod/operator
total 0
# pwd
/var/lib/juju/agents/application-demo-charm/charm

Effect is that the ops package cannot imported at runtime:

# python3.6
Python 3.6.9 (default, Nov  7 2019, 10:44:02)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
'/var/lib/juju/agents/application-demo-charm/charm'
>>> import sys
>>> sys.path.append(f"{os.getcwd()}/lib")
>>> import ops
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'ops'

What did I miss here? My demo project is at https://github.com/relaxdiego/demo-charm/tree/4495e821da3b1191148609698f3fdbca72d8398a

Event and BoundEvent classes should be changed to become class decorators

Event and BoundEvent are represented as classes, but users are being told to use them as class decorators:

operator/op/framework.py

Lines 115 to 125 in a14375b

class Event:
"""Event creates class descriptors to operate with events.
It is generally used as:
class SomethingHappened(EventBase):
pass
class SomeObject:
something_happened = Event(SomethingHappened)

It would be more Pythonic to use actual class decorators, rather than classes divorced from the class hierarchy.

Current singularity tracking implementation breaks nested event emission

I ran into an issue with the current object singularity tracking implementation when writing a test charm where I needed to emit an event in an event handler - this currently results in a RuntimeError (two objects claiming to be {obj.handle.path} have been created).

This draft #102 demonstrates the issue by modifying one of our tests.

CharmBase stutters

Strongly recommend renaming charm.CharmBase to charm.Base. Otherwise users will need to put up with nonsense like this:

from ops import charm

class Charm(charm.CharmBase):
    ...

Bear in mind that this is in a file called charm.py that's probably in a code repository titled <project>-charm.

framework.Object needs a better name

Calling a type Object, even within a namespace, is bound to cause confusion. (Sidebar - "framework" is not an effective namespace). Colliding with object will alienate experienced programmers and will cause beginners to become trapped.

Kubernetes pod state read

The framework des not seem to have a way to read kubernetes pod state for the underalying service. It would be nice to have a way to check if the pod that is run by the given unit is already started and update unit state accordingly.

Expose open-port and close-port hook tools

The open-port and close-port Juju hook environment tools are not exposed by the operator framework.

It would be good if the framework handled closing the old port when a new one is opened. eg.

self.open_port('grpc', 1234)  # Remembers 'grpc' port
self.open_port('grpc', 4242)  # Automatically close-port 1234 before open-port 4242

ActiveStatus() doesn't accept a message

Many of our current pre-operator charms use the Message field of "juju status" to display useful information about the running unit, e.g. the Postgres charm displays whether it's running as master or slave, the Livepatch charm displays the current patch status, and so on.

This functionality seems to be deliberately disabled in the ActiveStatus class, with messages only able to be set within a BlockedStatus, a MaintenanceStatus, or a WaitingStatus.

Can this be reconsidered? Sometimes a unit is not waiting, blocked or in maintenance, but we still want to see what it's doing.

Automatically transform config into pod spec

In working on the charms in the Kubeflow bundle, one pattern that has emerged for all of them is that they're actually just some Python wrappers around watching for any changes to state and rerendering some YAML that gets handed off to pod_spec_set, e.g. the ambassador charm:

https://github.com/juju-solutions/bundle-kubeflow/blob/master/charms/ambassador/reactive/ambassador.py

Would it be possible to support just writing a charm as a pod spec set template that gets rerendered whenever state changes (e.g. config changes, relation changes)? This would make handling state trivial in a charm, as it then manages none itself.

Add documentation

I know this is stating the obvious but the lack of documentation makes writing a charm a much less pleasurable experience. It is not always clear what the intent is of parts of the design, it means features that could be leveraged are missed and on a practical level it means lots of hunting around to try and workout how to do trivial things.

I would be reluctant to publish a charm and mark it production if there was no documentation supporting the framework it is written in.

I'm sure this on someones roadmap but I thought I'd raise a bug to track it.

Implement a method to expose the version (application-version-set)

Currently it is not possible to set the version string of the code that the charm is running.

I think it's a matter of adding it to the ModelBackend class in model.py, but there might be a reason it's not implemented yet, so I am willing to implement, unless there are objections.

How to ensure charm apt dependencies

Before a charm does 'import yaml' or 'import psycopg2', we need to ensure that these non-core dependencies are installed on the host system.

Currently, it looks like we need a stanza like this at the start of src/charm.py:

EDIT(niemeyer): Please don't do that as it's an ugly hack. See notes below.

import subprocess
try:
    import yaml
    import psycopg2
except ImportError:
    subprocess.check_call([
        'apt-get', '--assume-yes', '--option=Dpkg::Options::=--force-confold', 'install',
        'python3-yaml', 'python3-psycopg2',
    ])
    import yaml
    import psycopg2

I don't think we can hide this uglyness in hooks/install, as it is expected to be a symlink.

It would be best if this could somehow be bootstrapped using code in the operator framework, so that the 'apt-get installs' get run in a consistent manner with any OS release specific settings.

Perhaps the operator framework could detect at import time that it is a) being imported from a script in hooks/* b) in a hook environment, and if so run platform specific bootstrapping to install apt packages listed in a .txt or .yaml file. Extra points if we can support PPAs, but they are much more likely to contain application dependencies that the charm should be installing, rather than charm dependencies needed for the charm code to run.

Retrieving app data on a non-leader seems to cause a hook error

I have a charm that contains the following:

    @property
    def admin_password(self):
        logging.info("Retrieving admin password")
        password = self.peer_rel.data[self.peer_rel.app].get(self.PASSWORD_KEY)
        logging.info("Password: {}".format(password))
        return password

The error in the charm logs is:

2020-02-28 09:29:51 ERROR juju.worker.uniter.context context.go:830 could not write settings from "ceph-client-relation-changed" to relation 0: permission denied
2020-02-28 09:29:51 ERROR juju.worker.uniter.operation runhook.go:132 hook "ceph-client-relation-changed" failed: could not write settings from "ceph-client-relation-changed" to relation 0: permission denied

On the non-leaders this causes a failure. Some more context:

  • Failure only occurs on non-leader
  • The method successfully retrieves the password
  • In a debug-hooks session the hook returns a return code of 0 and the error is not in the logs. The error appears immediately after the debug-hook session is exited
  • It is a 100% reproducible
  • Switching the method to return a hard-coded password 'fixes' the issue to I am confident it is the data retrieval that is somehow causing the error.

We need to build strings using the logger infrastructure

IOW do NOT do this:

logger.debug('Creating a new relative symlink at {} pointing to {}'.format(event_path, target_path))

but this:

logger.debug('Creating a new relative symlink at %s pointing to %s', event_path, target_path)

Versioning for the Operator Framework?

Firstly, great job on this framework!

Currently, the readme recommends adding the framework as a submodule, using the master branch, to a charm's directory structure. Is there already an existing plan to implement versioning (preferably SemVer 2.x-compliant) for the framework? The reason I ask is because using just master is a bit worrisome since it introduces some variability when developing charms and it will be hard to determine whether it's a good time to upgrade the framework or not.

Consider subtrees instead of submodules

Git has two standard ways of embedding a branch inside another branch: submodules and subtrees.

submodules embed a reference to the embedded branch. The command 'git submodule update' needs to be run to prepare the branch for use.

subtrees embed a copy of the embedded branch. A freshly cloned branch does not need to be prepared for use; it already contains a complete copy of all the embedded dependencies.

For our use case of embedding the operator framework and other shared charm dependencies in charms, subtrees seem preferable. The charm using subtrees can be cloned an immediately deployed. Deployment tools such as juju-deployer and mojo do not need to be taught about the new branch 'prepare' stage that is replacing the reactive charm 'build' stage. Deployments from the charm store are unaffected either way, so this really only affects charm developers and sites needing to deploy directly from branches.

Symlink for `hooks/upgrade-charm` is not created when missing

Hi there, this is a bit of an edge case but I've just been troubleshooting a problem caused by this sequence of events:

  1. Create a charm that includes a hooks/upgrade-charm script
  2. Deploy the charm (juju deploy foo ...)
  3. Remove hooks/upgrade-charm from the charm's sources
  4. Deploy the new version of the charm (juju upgrade-charm --path foo ...)

As a result, the hooks/upgrade-charm symlink is missing and the upgrade_charm event is no longer passed through to the user's charm.py. This is reported in the logs as juju.worker.uniter.operation skipped "upgrade-charm" hook (missing). This may happen for other event types as well, but I haven't checked yet and I know start, install, and upgrade-charm are kind of special so I'd guess this problem is particular to upgrade-charm.

I think it would be best if all missing symlinks were created any time the code in /var/lib/juju/agents/*/charm changes (I guess whenever juju.downloader does its thing?) so that the resulting behaviour always matches what the user has defined in the deployed version of the charm, regardless of what past versions might have looked like. Thoughts?

Add support for writing to juju logs

Please add support for writing to juju logs. Obviously this can be done manually but it would be much cleaner if this was provided by the framework. Something like:

self.log(msg)
self.log(msg, "DEBUG")

A way to easily drop the developer inside their running Python code

(chat excerpt so we don't loose the idea)

facubatista: which is the fastest/simplest/easiest way to "locally debug a charm"? IOW, I have some code, and I want to run it, send events, see what happens, etc
niemeyer: facubatista: This is an interesting area to think about.. it would be great to have a way to very easily drop people inside their Python code
niemeyer: facubatista: debug-hook will drop you into a shell
niemeyer: facubatista: What we're talking about now is enabling someone to run the charm for real and having a breakpoint on it
facubatista: 1. a tool/mechanism for charm developers to debug their charms in real situations; a way to do something like "import operator_pdb;operator_pdb.set_trace()" in the code, then "juju deploy mycharm" in a real life situation, which would block, and "juju connect-pdb something"
niemeyer: facubatista: Yeah, except hopefully the breakpoint mechanism is generic instead of Python specific, but otherwise yes
niemeyer: facubatista: Also, ideally it wouldn't depend on you adding that breakpoint into the code itself
niemeyer: facubatista: In other words, the framework should support stopping at interesting points by itself
niemeyer: facubatista: So you don't need to change the charm to introspect it

[Question] Do we need a separate base class for interfaces?

Hi Charm team,

I have been trying to document the framework and have come around to Interfaces, when I noticed Issue 86.

With this, I was wondering if we should be providing a different base class for Interfaces and Charms themselves? Was the choice to have a single Object class deliberate? It is a bad idea to have a base for Charms and a Base for interfaces?

Thank you for you help, I will be using this to add to the docs

Cheers,
Peter

self.model.pod.set_spec should only be run on leader

Currently self.model.pod.set_spec should only be run on leader to avoid the following charm error:

raise ModelError(‘cannot set a pod spec as this unit is not a leader’)

Would it be worth updating that function call to only execute on the leader, to avoid the need for charm writers to do this?

QUESTION: Is this advisable to do in a handler?

Given a handler for on.config_changed:

def on_config_changed(self, event):
    unit_status = None

    while not isinstance(unit_status, ActiveStatus):
        # CODE THAT DOES STUFF & SETS unit_status HERE

        self.framework.model.unit.status = unit_status

Is that while loop advisable? Will Juju timeout the code if it ever runs too long?

Cache `Handle.__hash__()`

Hashing a long chain of handles could get expensive. Handles are immutable and therefore the result of hashing operation could be cached and re-used.

def __hash__(self):
return hash((self.parent, self.kind, self.key))

Support for charm class selector

In the OpenStack reactive charms we have put in place code which selects the charm class to use based on the version of OpenStack to be installed and the version of Ubuntu running the charm. Here is an example of the resulting charm classes https://github.com/openstack/charm-aodh/blob/master/src/lib/charm/openstack/aodh.py#L129

It would be really useful if the operator framework supported something like this. The charm author could provide a selector function that the framework could use to identify the right charm class to use for the deploy.

Operator Framework logo

Hi there,

I'm in the process of documenting the Framework, one thing I noticed is there's no logo! can we get one? 😄

Cheers,
Peter

Remote app is not added to RelationMapping for relations in some cases

  • Two apps, one primary, one subordinate;
  • Both have two units;
  • A leader subordinate writes app relation data to a container-scoped relation for all primaries to read;
  • The first primary observes the leader subordinate (relation-joined) which then writes some data to the app relation data bag;
  • The second primary comes up but gets a relation-changed event for the app relation data before it observes (relation-joined) its own subordinate - relation-list returns no units.

The reason why it fails is that we currently retrieve a remote app from a remote unit and special-case the peer relation but do not do anything for subordinates we do not yet observe.

operator/ops/model.py

Lines 335 to 342 in 8ef8bd7

if is_peer:
self.app = our_unit.app
try:
for unit_name in backend.relation_list(self.id):
unit = cache.get(Unit, unit_name)
self.units.add(unit)
if self.app is None:
self.app = unit.app

It might be that Juju needs to make a sure subordinate -joined event fires first for a primary and only then -changed events are fired (created https://bugs.launchpad.net/juju/+bug/1866828).

Additional info:

juju status
Model    Controller           Cloud/Region         Version  SLA          Timestamp
default  localhost-localhost  localhost/localhost  2.7.4.1  unsupported  14:09:00+03:00

App           Version  Status   Scale  Charm         Store  Rev  OS      Notes
apache-httpd           error        2  apache-httpd  local    0  ubuntu  
dummy-vhost            waiting    1/2  dummy-vhost   local    0  ubuntu  

Unit              Workload  Agent       Machine  Public address  Ports  Message
apache-httpd/0*   active    idle        0        10.209.240.137         
  dummy-vhost/0*  active    idle                 10.209.240.137         
apache-httpd/1    error     idle        1        10.209.240.253         hook failed: "vhost-config-relation-changed"
  dummy-vhost/1   waiting   allocating           10.209.240.253         agent initializing

The unit for which everything is OK:

juju show-status-log apache-httpd/0
Time                        Type       Status       Message
10 Mar 2020 14:07:41+03:00  juju-unit  allocating   
10 Mar 2020 14:07:41+03:00  workload   waiting      waiting for machine
10 Mar 2020 14:08:19+03:00  workload   waiting      installing agent
10 Mar 2020 14:08:21+03:00  workload   waiting      agent initializing
10 Mar 2020 14:08:22+03:00  workload   maintenance  installing charm software
10 Mar 2020 14:08:22+03:00  juju-unit  executing    running install hook
10 Mar 2020 14:08:32+03:00  juju-unit  executing    running leader-elected hook
10 Mar 2020 14:08:33+03:00  juju-unit  executing    running config-changed hook
10 Mar 2020 14:08:33+03:00  workload   active       
10 Mar 2020 14:08:33+03:00  juju-unit  executing    running start hook
10 Mar 2020 14:08:34+03:00  juju-unit  idle         
10 Mar 2020 14:08:40+03:00  juju-unit  executing    running vhost-config-relation-joined hook
10 Mar 2020 14:08:41+03:00  juju-unit  executing    running vhost-config-relation-changed hook
10 Mar 2020 14:08:42+03:00  juju-unit  idle         
10 Mar 2020 14:08:57+03:00  juju-unit  executing    running httpd-peer-relation-joined hook
10 Mar 2020 14:08:58+03:00  juju-unit  executing    running httpd-peer-relation-changed hook
10 Mar 2020 14:08:58+03:00  juju-unit  idle         

The failing unit:

Time                        Type       Status       Message
10 Mar 2020 14:08:44+03:00  workload   waiting      agent initializing
10 Mar 2020 14:08:44+03:00  workload   maintenance  installing charm software
10 Mar 2020 14:08:44+03:00  juju-unit  executing    running install hook
10 Mar 2020 14:08:56+03:00  juju-unit  executing    running leader-settings-changed hook
10 Mar 2020 14:08:56+03:00  juju-unit  executing    running config-changed hook
10 Mar 2020 14:08:56+03:00  workload   active       
10 Mar 2020 14:08:56+03:00  juju-unit  executing    running start hook
10 Mar 2020 14:08:57+03:00  juju-unit  executing    running httpd-peer-relation-joined hook
10 Mar 2020 14:08:58+03:00  juju-unit  executing    running httpd-peer-relation-changed hook
10 Mar 2020 14:08:59+03:00  juju-unit  executing    running vhost-config-relation-changed hook
10 Mar 2020 14:08:59+03:00  juju-unit  error        hook failed: "vhost-config-relation-changed"
./hooks/vhost-config-relation-changed 
> /var/lib/juju/agents/unit-apache-httpd-1/charm/hooks/vhost-config-relation-changed(212)on_vhost_config_relation_changed()
-> vhosts_serialized = event.relation.data[event.app].get('vhosts')
(Pdb) c
Traceback (most recent call last):
  File "./hooks/vhost-config-relation-changed", line 272, in <module>
    main(Charm)
  File "lib/ops/main.py", line 195, in main
    _emit_charm_event(charm, juju_event_name)
  File "lib/ops/main.py", line 120, in _emit_charm_event
    event_to_emit.emit(*args, **kwargs)
  File "lib/ops/framework.py", line 199, in emit
    framework._emit(event)
  File "lib/ops/framework.py", line 633, in _emit
    self._reemit(event_path)
  File "lib/ops/framework.py", line 668, in _reemit
    custom_handler(event)
  File "./hooks/vhost-config-relation-changed", line 212, in on_vhost_config_relation_changed
    vhosts_serialized = event.relation.data[event.app].get('vhosts')
  File "lib/ops/model.py", line 372, in __getitem__
    return self._data[key]
KeyError: <ops.model.Application
(Pdb) dict(event.relation.data)
{<ops.model.Unit apache-httpd/1>: <ops.model.RelationDataContent object at 0x7f5d38f4ab00>, <ops.model.Application apache-httpd>: <ops.model.RelationDataContent object at 0x7f5d38f4aa58>}

[1]+  Stopped                 ./hooks/vhost-config-relation-changed
root@juju-df5eba-1:/var/lib/juju/agents/unit-apache-httpd-1/charm# relation-ids
vhost-config:1
root@juju-df5eba-1:/var/lib/juju/agents/unit-apache-httpd-1/charm# relation-list
root@juju-df5eba-1:/var/lib/juju/agents/unit-apache-httpd-1/charm# relation-list ; echo $?
0
root@juju-df5eba-1:/var/lib/juju/agents/unit-apache-httpd-1/charm# env | grep JUJU_REMOTE
JUJU_REMOTE_UNIT=
JUJU_REMOTE_APP=dummy-vhost
root@juju-df5eba-1:/var/lib/juju/agents/unit-apache-httpd-1/charm# relation-get --app
vhosts: '- {port: "80", template: PFZpcnR1YWxIb3N0ICo6ODA+CglTZXJ2ZXJBZG1pbiB3ZWJtYXN0ZXJAbG9jYWxob3N0CglEb2N1bWVudFJvb3QgL3Zhci93d3cvZHVtbXktdmhvc3QKCUVycm9yTG9nICR7QVBBQ0hFX0xPR19ESVJ9L2Vycm9yLmxvZwoJQ3VzdG9tTG9nICR7QVBBQ0hFX0xPR19ESVJ9L2FjY2Vzcy5sb2cgY29tYmluZWQKPC9WaXJ0dWFsSG9zdD4K}'
cat metadata.yaml 
name: apache-httpd
# ... 
requires:
    vhost-config:
        interface: apache-vhost-config
        scope: container

[Idea] Remove Dependency on System Python With PyOxidizer

This isn't extremely well thought out or anything, but I was entertaining the idea of using PyOxidizer to package an operator framework charm so that it wasn't dependent on the system Python. This would help make it more ( or completely? ) distro agnostic as that was one of the burdens to writing Centos charms. Obviously it wouldn't make your charms distro agnostic, but it would help the framework to be.

This would produce a dependency on the CPU architecture, so you would have to have builds for multiple architectures to be able to use the framework on multiple architectures.

Interface class repository

The reactive framework, apart from a notion of layers, had also a notion of defined interfaces.

The new framework also provides the interfaces and a pattern of creating interface class. Since the interface code in essence would be reusable, is there a plan of creating some kind of repository where interface definitions would be stored? This way, when we refer to mysql relation we would have MySQLInterfaceRequires and MySQLInterfaceProvides stored in a central place and regardles of a charm providing it, we would have a consistent set of data expected and provided regardles of whoever wrote the charm internals.

Handling of application level relation config feels wrong

I believe that part of the purpose of the operator framework is to abstract the charm author from having to understand the mechanics of Juju. When it comes to application level relation config you have to know how the juju mechanics work to be able to utilise it. You have to know that it can only be set by the leader and that it is a special subclass of relation data accessed via the '.app' relation attribute. It felt a bit unclean to me when using it but I appreciate that it is subjective and a minor issue.

lib/charm.py and lib/ops/charm.py collide

We should consider altering the name of the internal module. That will prevent any confusion with where object definitions are located for less experienced programmers.

Metrics hook need to be handled specially

The hook for updating metrics has a limited hook context (can't access many hook tools) and can be run concurrently with other hooks, so we need to handle it specially in the framework. I think currently we don't even have any support for it, so it will not cause issues until we add that.

ResourceMeta requires "filename" but that's optional

We currently require a filename key for resources, but that isn't actually a requirement.

operator/ops/charm.py

Lines 226 to 232 in af37ba0

class ResourceMeta:
"""Object containing metadata about a resource definition."""
def __init__(self, name, raw):
self.resource_name = name
self.type = raw['type']
self.filename = raw['filename']

Many k8s charms omit them:

https://github.com/wallyworld/caas/blob/84018271459aaa124b10f824b36159aecc7f1cad/charms/mysql/metadata.yaml#L19-L21

Introduce base classes for interface into the framework

Currently the InterfaceProvides and InterfaceRequires classes inherit after ops.framework.Object class. I would like to propose creating interim base class serving as a base for interface and including what effectively is a boilerplate code:

  • relation name setup
  • relations and fields properties
  • is joined status
  • is ready status
  • goal state

EventsBase instances can conflict

Because EventsBase overrides handle_kind at the class level, two instances assigned to the same parent will have conflicting handle paths, potentially causing events to collide and lead to duplicate events being emitted. Instances attached to different parents will not have this issue, of course, so it's fairly limited in scope of impact. The one use-case I can come up with for this would be for a component to have separate private and public event collections. It would be fairly easy, though, to have the handle_kind driven by the attribute name rather than being hard-coded.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.