Skip to content

Instantly share code, notes, and snippets.

@jnsgruk

jnsgruk/slide02.sh Secret

Last active May 7, 2021
Embed
What would you like to do?
Operator Day 2021 - Demo Slide Gists
# Install/Setup MicroK8s
sudo snap install --classic microk8s
sudo usermod -aG microk8s $(whoami)
sudo microk8s status --wait-ready
sudo microk8s enable storage dns ingress
sudo snap alias microk8s.kubectl kubectl
newgrp microk8s
# Install Charmcraft
sudo snap install charmcraft
# Install Juju
sudo snap install juju --classic
# Bootstrap MicroK8s
juju bootstrap microk8s micro
juju add-model development
# Install/Setup MicroK8s
sudo snap install --classic microk8s
sudo usermod -aG microk8s $(whoami)
sudo microk8s status --wait-ready
sudo microk8s enable storage dns ingress
sudo snap alias microk8s.kubectl kubectl
newgrp microk8s
# Install Charmcraft
sudo snap install charmcraft
# Install Juju
sudo snap install juju --classic
# Bootstrap MicroK8s
juju bootstrap microk8s micro
juju add-model development
# Create the Charm directory
mkdir hello-kubecon; cd hello-kubecon
# Initialise the Charm directory
charmcraft init
# Ensure that virtualenv support is installed
sudo apt update && sudo apt install -y python3-virtualenv
# Create a virtualenv for the charm code
virtualenv venv
# Activate the venv
source ./venv/bin/activate
# Install dependencies
pip install -r requirements-dev.txt
# Copyright 2021 Jon Seager
# See LICENSE file for licensing details.
name: hello-kubecon
description: |
A basic demonstration charm that hosts a placeholder webpage with links
to various Juju/Charmed Operator SDK pages. Hosted using a small, custom
webserver written in Go (https://github.com/jnsgruk/gosherve). Illustrates
the use of charm workloads, actions, config, storage and relations.
summary: |
A demonstration charm for Kubecon Operator Day 2021.
containers:
gosherve:
resource: gosherve-image
resources:
gosherve-image:
type: oci-image
description: OCI image for gosherve
def _on_gosherve_pebble_ready(self, event):
"""Define and start a workload using the Pebble API"""
# Get a reference the container attribute on the PebbleReadyEvent
container = event.workload
# Define an initial Pebble layer configuration
pebble_layer = {
"summary": "gosherve layer",
"description": "pebble config layer for gosherve",
"services": {
"gosherve": {
"override": "replace",
"summary": "gosherve",
"command": "/gosherve",
"startup": "enabled",
"environment": {
"REDIRECT_MAP_URL": "https://jnsgr.uk/demo-routes"
},
}
},
}
# Add intial Pebble config layer using the Pebble API
container.add_layer("gosherve", 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()
# Build the charm
charmcraft pack
# Deploy the charm
juju deploy ./hello-kubecon.charm --resource gosherve-image=jnsgruk/gosherve
# Check the Juju status
watch -n1 --color juju status --color
# Set the log level to DEBUG for the development model
juju model-config logging-config="<root>=WARNING;unit=DEBUG"
# Follow the debug-log
juju debug-log
# Explore with kubectl
kubectl -n development get pods
# Copyright 2021 Jon Seager
# See LICENSE file for licensing details.
#
# Learn more about config at: https://juju.is/docs/sdk/config
options:
redirect-map:
default: "https://jnsgr.uk/demo-routes"
description: A URL pointing to a list of redirects for Gosherve.
type: string
def _gosherve_layer(self):
"""Returns a Pebble configration layer for Gosherve"""
return {
"summary": "gosherve layer",
"description": "pebble config layer for gosherve",
"services": {
"gosherve": {
"override": "replace",
"summary": "gosherve",
"command": "/gosherve",
"startup": "enabled",
"environment": {
"REDIRECT_MAP_URL": self.config["redirect-map"]
},
}
},
}
def _on_config_changed(self, event):
"""Handle the config-changed event"""
# Get the gosherve container so we can configure/manipulate it
container = self.unit.get_container("gosherve")
# Create a new config layer
layer = self._gosherve_layer()
# Get the current config
services = container.get_plan().to_dict().get("services", {})
# Check if there are any changes to services
if services != layer["services"]:
# Changes were made, add the new layer
container.add_layer("gosherve", layer, combine=True)
logging.info("Added updated layer 'gosherve' to Pebble plan")
# Stop the service if it is already running
if container.get_service("gosherve").is_running():
container.stop("gosherve")
# Restart it and report a new status to Juju
container.start("gosherve")
logging.info("Restarted gosherve service")
# All is well, set an ActiveStatus
self.unit.status = ActiveStatus()
# Build the charm
charmcraft pack
# Deploy the charm
juju refresh hello-kubecon --path=./hello-kubecon.charm
# Check the juju status - note the blocked status
watch -n1 --color juju status --color
# Change the configuration
juju config hello-kubecon redirect-map="https://jnsgr.uk/demo-routes-alt"
# Check the juju status - note the active status
watch -n1 --color juju status --color
def _gosherve_layer(self):
"""Returns a Pebble configration layer for Gosherve"""
return {
"summary": "gosherve layer",
"description": "pebble config layer for gosherve",
"services": {
"gosherve": {
"override": "replace",
"summary": "gosherve",
"command": "/gosherve",
"startup": "enabled",
"environment": {
"REDIRECT_MAP_URL": self.config["redirect-map"],
"WEBROOT": "/srv",
},
}
},
}
# Copyright 2021 Jon Seager
# See LICENSE file for licensing details.
name: hello-kubecon
description: |
A basic demonstration charm that hosts a placeholder webpage with links
to various Juju/Charmed Operator SDK pages. Hosted using a small, custom
webserve written in Go (https://github.com/jnsgruk/gosherve). Illustrates
the use of charm workloads, actions, config, storage and relations.
summary: |
A demonstration charm for Kubecon Operator Day 2021.
containers:
gosherve:
resource: gosherve-image
mounts:
- storage: webroot
location: /srv
resources:
gosherve-image:
type: oci-image
description: OCI image for gosherve
storage:
webroot:
type: filesystem
location: /srv
def _gosherve_layer(self):
"""Returns a Pebble configration layer for Gosherve"""
return {
"summary": "gosherve layer",
"description": "pebble config layer for gosherve",
"services": {
"gosherve": {
"override": "replace",
"summary": "gosherve",
"command": "/gosherve",
"startup": "enabled",
"environment": {
"REDIRECT_MAP_URL": self.config["redirect-map"],
"WEBROOT": "/srv",
},
}
},
}
class HelloKubeconCharm(CharmBase):
"""Charm the service."""
def __init__(self, *args):
super().__init__(*args)
self.framework.observe(self.on.install, self._on_install)
self.framework.observe(self.on.config_changed, self._on_config_changed)
def _on_install(self, _):
# Download the site
self._fetch_site()
def _fetch_site(self):
"""Fetch latest copy of website from Github and move into webroot"""
# Set the site URL
site_src = "https://jnsgr.uk/demo-site"
# Set some status and do some logging
self.unit.status = MaintenanceStatus("Fetching web site")
logger.info("Downloading site from %s", site_src)
# Download the site
urllib.request.urlretrieve(site_src, "/srv/index.html")
# Set the unit status back to Active
self.unit.status = ActiveStatus()
# Build the charm
charmcraft pack
# Remove old charm (cannot refresh -- storage)
juju remove-application hello-kubecon
# Redeploy
juju deploy ./hello-kubecon.charm --resource gosherve-image=jnsgruk/gosherve
# Check the juju status
watch -n1 --color juju status --color
# Verify
$ curl "http://$(juju show-unit hello-kubecon/0 | grep -Po 'address: \K.+'):8080/"
# Explore
kubectl -n development describe pod hello-kubecon-0
# Copyright 2021 Jon Seager
# See LICENSE file for licensing details.
#
# Learn more about actions at: https://juju.is/docs/sdk/actions
pull-site:
description: Unpack latest version of website from remote source.
# ...
def __init__(self, *args):
super().__init__(*args)
self.framework.observe(self.on.install, self._on_install)
self.framework.observe(self.on.config_changed, self._on_config_changed)
self.framework.observe(self.on.pull_site_action, self._pull_site_action)
# ...
def _pull_site_action(self, event):
"""Action handler that pulls the latest site archive and unpacks it"""
self._fetch_site()
event.set_results({"result": "site pulled"})
# ...
# Build the charm
charmcraft pack
# Refresh the deployment
juju refresh hello-kubecon --path=./hello-kubecon.charm
# Check the juju status
watch -n1 --color juju status --color
# Opt-in to new actions UX
export JUJU_FEATURES=actions-v2
# Run the action
juju run hello-kubecon/0 pull-site
juju run hello-kubecon/0 pull-site --format=yaml
juju run hello-kubecon/0 pull-site --background
# Check the debug-log
juju debug-log
# Check the there is a library associated with the charm
charmcraft list-lib nginx-ingress-integrator
# Fetch the library into our charm
charmcraft fetch-lib charms.nginx_ingress_integrator.v0.ingress
# Ensure library was imported into the correct place
ls -l lib/charms/nginx_ingress_integrator/v0/
name: hello-kubecon
description: |
A basic demonstration charm that hosts a placeholder webpage with links
to various Juju/Charmed Operator SDK pages. Hosted using a small, custom
webserver written in Go (https://github.com/jnsgruk/gosherve). Illustrates
the use of charm workloads, actions, config, storage and relations.
summary: |
A demonstration charm for Kubecon Operator Day 2021.
# ...
requires:
ingress:
interface: ingress
from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
#...
class HelloKubeconCharm(CharmBase):
"""Charm the service."""
def __init__(self, *args):
super().__init__(*args)
self.framework.observe(self.on.install, self._on_install)
self.framework.observe(self.on.config_changed, self._on_config_changed)
self.framework.observe(self.on.pull_site_action, self._pull_site_action)
self.ingress = IngressRequires(self, {
"service-hostname": "hellokubecon.juju",
"service-name": self.app.name,
"service-port": 8080
})
# Build the charm
charmcraft pack
# Refresh the deployment
juju refresh hello-kubecon --path=./hello-kubecon.charm
# Deploy the nginx-ingress-integrator
juju deploy nginx-ingress-integrator
# Relate our application to the ingress integrator
juju relate hello-kubecon nginx-ingress-integrator
# Set the ingress class for microk8s
juju config nginx-ingress-integrator ingress-class="public"
# Add an entry to our hosts file
echo "127.0.1.1 hellokubecon.juju" | sudo tee -a /etc/hosts
# Check the juju status
watch -n1 --color juju status --color
# Verify
curl hellokubecon.juju
def test_gosherve_layer(self):
# Test with empty config.
self.assertEqual(self.harness.charm.config['redirect-map'], "https://jnsgr.uk/demo-routes")
expected = {
"summary": "gosherve layer",
"description": "pebble config layer for gosherve",
"services": {
"gosherve": {
"override": "replace",
"summary": "gosherve",
"command": "/gosherve",
"startup": "enabled",
"environment": {
"REDIRECT_MAP_URL": "https://jnsgr.uk/demo-routes",
"WEBROOT": "/srv",
},
}
},
}
self.assertEqual(self.harness.charm._gosherve_layer(), expected)
# And now test with a different value in the redirect-map config option.
# Disable hook firing first.
self.harness.disable_hooks()
self.harness.update_config({"redirect-map": "test value"})
expected["services"]["gosherve"]["environment"]["REDIRECT_MAP_URL"] = "test value"
self.assertEqual(self.harness.charm._gosherve_layer(), expected)
def test_on_config_changed(self):
plan = self.harness.get_container_pebble_plan("gosherve")
self.assertEqual(plan.to_dict(), {})
# Trigger a config-changed hook. Since there was no plan initially, the
# "gosherve" service in the container won't be running so we'll be
# testing the `is_running() == False` codepath.
self.harness.update_config({"redirect-map": "test value"})
plan = self.harness.get_container_pebble_plan("gosherve")
# Get the expected layer from the gosherve_layer method (tested above)
expected = self.harness.charm._gosherve_layer()
expected.pop("summary", "")
expected.pop("description", "")
# Check the plan is as expected
self.assertEqual(plan.to_dict(), expected)
self.assertEqual(self.harness.model.unit.status, ActiveStatus())
container = self.harness.model.unit.get_container("gosherve")
self.assertEqual(container.get_service("gosherve").is_running(), True)
# Now test again with different config, knowing that the "gosherve"
# service is running (because we've just tested it above), so we'll
# be testing the `is_running() == True` codepath.
self.harness.update_config({"redirect-map": "test2 value"})
plan = self.harness.get_container_pebble_plan("gosherve")
# Adjust the expected plan
expected["services"]["gosherve"]["environment"]["REDIRECT_MAP_URL"] = "test2 value"
self.assertEqual(plan.to_dict(), expected)
self.assertEqual(container.get_service("gosherve").is_running(), True)
self.assertEqual(self.harness.model.unit.status, ActiveStatus())
# And finally test again with the same config to ensure we exercise
# the case where the plan we've created matches the active one. We're
# going to mock the container.stop and container.start calls to confirm
# they were not called.
with patch('ops.model.Container.start') as _start, patch('ops.model.Container.stop') as _stop:
self.harness.charm.on.config_changed.emit()
_start.assert_not_called()
_stop.assert_not_called()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment