Skip to content

Commit

Permalink
Sidecar template (all changes from PR#300
Browse files Browse the repository at this point in the history
  • Loading branch information
facundobatista committed Apr 27, 2021
1 parent 099d7e6 commit 3ed6fd9
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 87 deletions.
52 changes: 30 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ Collection.

Use `charmcraft` to:

* Init a new charmed operator file structure
* Build your operator into a charmed operator for distribution
* Register your charmed operator name on Charmhub
* Upload your charmed operators to Charmhub
* Release your charmed operators into channels
- Init a new charmed operator file structure
- Build your operator into a charmed operator for distribution
- Register your charmed operator name on Charmhub
- Upload your charmed operators to Charmhub
- Release your charmed operators into channels

You can use charmcraft with operators written in any language but we
recommend the [Python Operator Framework on
Expand Down Expand Up @@ -46,13 +46,20 @@ Use `charmcraft init` to create a new template charm operator file tree:
```bash
$ mkdir my-new-charm; cd my-new-charm
$ charmcraft init
All done.
There are some notes about things we think you should do.
These are marked with ‘TODO:’, as is customary. Namely:
README.md: fill out the description
README.md: explain how to use the charm
Charm operator package file and directory tree initialized.
TODO:

README.md: Describe your charm in a few paragraphs of Markdown
README.md: Provide high-level usage, such as required config or relations
actions.yaml: change this example to suit your needs.
config.yaml: change this example to suit your needs.
metadata.yaml: fill out the charm's description
metadata.yaml: fill out the charm's summary
metadata.yaml: replace with containers for your workload (delete for non-k8s)
metadata.yaml: each container defined above must specify an oci-image resource
src/charm.py: change this example to suit your needs.
src/charm.py: change this example to suit your needs.
src/charm.py: change this example to suit your needs.
```

You will now have all the essential files for a charmed operator, including
Expand All @@ -74,7 +81,8 @@ Created 'test-charm.charm'.
`charmcraft build` will fetch additional files into the tree from PyPI based
on `requirements.txt` and will compile modules using a virtualenv.

The charmed operator is just a zipfile with metadata and the operator code itself:
The charmed operator is just a zipfile with metadata and the operator code
itself:

```text
$ unzip -l test-charm.charm
Expand All @@ -88,15 +96,15 @@ Archive: test-charm.charm
812617 84 files
```

Now, if you have a Kubernetes cluster with the Juju OLM accessible you can
directly `juju deploy <test-charm.charm>` to the cluster. You do not need to
publish your operator on Charmhub, you can pass the charmed operator file around
directly to users, or for CI/CD purposes.
Now, if you have a Kubernetes cluster with the Juju OLM accessible you can issue
`juju deploy ./my-new-charm.charm --resource httpbin-image=kennethreitz/httpbin`.
You do not need to publish your operator on Charmhub, you can pass the charmed
operator file around directly to users, or for CI/CD purposes.

## Charmhub login and charm name reservations

[Charmhub](https://charmhub.io/) is the world's largest repository of
operators. It makes it easy to share and collaborate on operators. The
operators. It makes it easy to share and collaborate on operators. The
community are interested in operators for a very wide range of purposes,
including infrastructure-as-code and legacy application management, and of
course Kubernetes operators.
Expand All @@ -105,10 +113,10 @@ Use `charmcraft login` and `charmcraft logout` to sign into Charmhub.

## Charmhub name registration

You can register operator names in Charmhub with `charmcraft register
<name>`. Many common names have been reserved, you are encouraged to discuss
your interest in leading or collaborating on a charmed operator in [Charmhub
Discourse](https://discourse.charmhub.io/).
You can register operator names in Charmhub with `charmcraft register <name>`.
Many common names have been reserved, you are encouraged to discuss your
interest in leading or collaborating on a charmed operator in
[Charmhub Discourse](https://discourse.charmhub.io/).

Charmhub naming policy is the principle of least surprise - a well-known
name should map to an operator that most people would expect to get for that
Expand All @@ -134,8 +142,8 @@ latest/edge
```

Use `charmcraft upload` to get a new revision number for your freshly built
charmed operator, and `charmcraft release` to release a revision into any particular
channel for your users.
charmed operator, and `charmcraft release` to release a revision into any
particular channel for your users.

# Charmcraft source

Expand Down
13 changes: 0 additions & 13 deletions charmcraft/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
import re
from datetime import date

import yaml

from charmcraft.cmdbase import BaseCommand, CommandError
from charmcraft.utils import make_executable, get_templates_environment

Expand Down Expand Up @@ -75,11 +73,6 @@ def fill_parser(self, parser):
parser.add_argument(
"--author",
help="The charm author; defaults to the current user name per GECOS")
parser.add_argument(
"--series",
help=(
"A comma-separated list of supported platform series; "
"defaults to 'kubernetes' with a reminder to change it"))
parser.add_argument(
"-f", "--force", action="store_true",
help="Initialize even if the directory is not empty (will not overwrite files)")
Expand All @@ -106,17 +99,11 @@ def run(self, args):
if not re.match(r"[a-z][a-z0-9-]*[a-z0-9]$", args.name):
raise CommandError("{} is not a valid charm name".format(args.name))

if args.series is None:
series = "[kubernetes] # TEMPLATE-TODO: change to an Ubuntu series if not using k8s"
else:
series = yaml.dump(args.series.split(","), default_flow_style=True)

context = {
"name": args.name,
"author": args.author,
"year": date.today().year,
"class_name": "".join(re.split(r"\W+", args.name.title())) + "Charm",
"series": series,
}

env = get_templates_environment('init')
Expand Down
7 changes: 7 additions & 0 deletions charmcraft/templates/init/.gitignore.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
venv/
build/
*.charm

.coverage
__pycache__/
*.py[cod]
12 changes: 11 additions & 1 deletion charmcraft/templates/init/metadata.yaml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,15 @@ description: |
TEMPLATE-TODO: fill out the charm's description
summary: |
TEMPLATE-TODO: fill out the charm's summary
series: {{ series }}

# TEMPLATE-TODO: replace with containers for your workload (delete for non-k8s)
containers:
httpbin:
resource: httpbin-image

# TEMPLATE-TODO: each container defined above must specify an oci-image resource
resources:
httpbin-image:
type: oci-image
description: OCI image for httpbin (kennethreitz/httpbin)

2 changes: 1 addition & 1 deletion charmcraft/templates/init/requirements.txt.j2
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ops
ops >= 1.2.0
36 changes: 32 additions & 4 deletions charmcraft/templates/init/src/charm.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ develop a new k8s charm using the Operator Framework:
import logging

from ops.charm import CharmBase
from ops.main import main
from ops.framework import StoredState
from ops.main import main
from ops.model import ActiveStatus

logger = logging.getLogger(__name__)
Expand All @@ -29,13 +29,41 @@ class {{ class_name }}(CharmBase):

def __init__(self, *args):
super().__init__(*args)
self.framework.observe(self.on.install, self._on_install)
self.framework.observe(self.on.httpbin_pebble_ready, self._on_httpbin_pebble_ready)
self.framework.observe(self.on.config_changed, self._on_config_changed)
self.framework.observe(self.on.fortune_action, self._on_fortune_action)
self._stored.set_default(things=[])

def _on_install(self, _):
"""Perform any install steps and report status."""
def _on_httpbin_pebble_ready(self, event):
"""Define and start a workload using the Pebble API.
TEMPLATE-TODO: change this example to suit your needs.
You'll need to specify the right entrypoint and environment
configuration for your specific workload. Tip: you can see the
standard entrypoint of an existing container using docker inspect
Learn more about Pebble layers at https://github.com/canonical/pebble
"""
# Get a reference the container attribute on the PebbleReadyEvent
container = event.workload
# Define an initial Pebble layer configuration
pebble_layer = {
"summary": "httpbin layer",
"description": "pebble config layer for httpbin",
"services": {
"httpbin": {
"override": "replace",
"summary": "httpbin",
"command": "gunicorn -b 0.0.0.0:80 httpbin:app -k gevent",
"startup": "enabled",
"environment": {"thing": self.model.config["thing"]},
}
},
}
# Add intial Pebble config layer using the Pebble API
container.add_layer("httpbin", pebble_layer, combine=True)
# Autostart any services that were defined with startup: enabled
container.autostart()
# Learn more about statuses in the SDK docs:
# https://juju.is/docs/sdk/constructs#heading--statuses
self.unit.status = ActiveStatus()
Expand Down
55 changes: 42 additions & 13 deletions charmcraft/templates/init/tests/test_charm.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,61 @@
import unittest
from unittest.mock import Mock

from ops.testing import Harness
from charm import {{ class_name }}
from ops.model import ActiveStatus
from ops.testing import Harness


class TestCharm(unittest.TestCase):
def setUp(self):
self.harness = Harness({{ class_name }})
self.addCleanup(self.harness.cleanup)
self.harness.begin()

def test_config_changed(self):
harness = Harness({{ class_name }})
self.addCleanup(harness.cleanup)
harness.begin()
self.assertEqual(list(harness.charm._stored.things), [])
harness.update_config({"thing": "foo"})
self.assertEqual(list(harness.charm._stored.things), ["foo"])
self.assertEqual(list(self.harness.charm._stored.things), [])
self.harness.update_config({"thing": "foo"})
self.assertEqual(list(self.harness.charm._stored.things), ["foo"])

def test_action(self):
harness = Harness({{ class_name }})
harness.begin()
# the harness doesn't (yet!) help much with actions themselves
action_event = Mock(params={"fail": ""})
harness.charm._on_fortune_action(action_event)
self.harness.charm._on_fortune_action(action_event)

self.assertTrue(action_event.set_results.called)

def test_action_fail(self):
harness = Harness({{ class_name }})
harness.begin()
action_event = Mock(params={"fail": "fail this"})
harness.charm._on_fortune_action(action_event)
self.harness.charm._on_fortune_action(action_event)

self.assertEqual(action_event.fail.call_args, [("fail this",)])

def test_httpbin_pebble_ready(self):
# Check the initial Pebble plan is empty
initial_plan = self.harness.get_container_pebble_plan("httpbin")
self.assertEqual(initial_plan.to_yaml(), "{}\n")
# 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": {"thing": "🎁"},
}
},
}
# Get the httpbin container from the model
container = self.harness.model.unit.get_container("httpbin")
# Emit the PebbleReadyEvent carrying the httpbin container
self.harness.charm.on.httpbin_pebble_ready.emit(container)
# 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, ActiveStatus())
2 changes: 1 addition & 1 deletion completion.bash
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ _charmcraft()
COMPREPLY=( $(compgen -W "${globals[*]} --revision --channel --resource" -- "$cur") )
;;
init)
COMPREPLY=( $(compgen -W "${globals[*]} --name --author --series --force" -- "$cur") )
COMPREPLY=( $(compgen -W "${globals[*]} --name --author --force" -- "$cur") )
;;
upload)
COMPREPLY=( $(compgen -W "${globals[*]} --release" -- "$cur") )
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

coverage
flake8
ops >= 0.8.0
ops >= 1.2.0
pydocstyle
pytest
responses
32 changes: 1 addition & 31 deletions tests/commands/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
from argparse import Namespace

import pytest
import yaml

from charmcraft.cmdbase import CommandError
from charmcraft.commands.init import InitCommand
from charmcraft.utils import S_IXALL
Expand Down Expand Up @@ -53,6 +51,7 @@ def test_all_the_files(tmp_path, config):
cmd.run(Namespace(name='my-charm', author="ಅಪರಿಚಿತ ವ್ಯಕ್ತಿ", series='k8s', force=False))
assert sorted(str(p.relative_to(tmp_path)) for p in tmp_path.glob("**/*")) == [
".flake8",
".gitignore",
".jujuignore",
"LICENSE",
"README.md",
Expand Down Expand Up @@ -117,32 +116,3 @@ def test_tests(tmp_path, config):
cmd.run(Namespace(name='my-charm', author="だれだれ", series='k8s', force=False))

subprocess.run(["./run_tests"], cwd=str(tmp_path), check=True, env=env)


def test_series_defaults(tmp_path, config):
"""Check that series defaults to kubernetes including a TODO message."""
cmd = InitCommand('group', config)
# series default comes from the parsing itself
cmd.run(Namespace(name='my-charm', author="fred", series=None, force=False))

# verify the value is correct at a YAML level
metadata_filepath = tmp_path / "metadata.yaml"
metadata = yaml.safe_load(metadata_filepath.read_text())
assert metadata.get("series") == ['kubernetes']

# verify a TODO is added at a text level
for line in metadata_filepath.open('rt'):
if line.startswith('series'):
assert "# TEMPLATE-TODO" in line
break
else:
pytest.fail("ERROR, 'series' line not found") # just in case


def test_manual_overrides_defaults(tmp_path, config):
cmd = InitCommand('group', config)
cmd.run(Namespace(name='my-charm', author="fred", series='xenial,precise', force=False))

with (tmp_path / "metadata.yaml").open("rt", encoding="utf8") as f:
metadata = yaml.safe_load(f)
assert metadata.get("series") == ['xenial', 'precise']

0 comments on commit 3ed6fd9

Please sign in to comment.