Skip to content

Commit

Permalink
[meta] oci.Container component
Browse files Browse the repository at this point in the history
Implement a basic oci container component
  • Loading branch information
PhilTaken committed Jul 13, 2023
1 parent 96dae9f commit 86ca9fb
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 0 deletions.
107 changes: 107 additions & 0 deletions src/batou_ext/oci.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import hashlib
import os
import re
from typing import Optional

import pkg_resources
from batou import UpdateNeeded
from batou.component import Attribute, Component
from batou.lib.file import File
from batou.lib.service import Service

import batou_ext.nix


@batou_ext.nix.rebuild
class Container(Component):
"""
A OCI Container component.
With this component you can dynamically schedule docker containers to be run on the target host
Example:
```
self += batou_ext.oci.Container(image = "mysql", version = "8.0")
```
"""

# general options
image = Attribute(str)
version: str = "latest"
container_name: Optional[str] = None

# specific options
entrypoint: Optional[str] = None
envfile: Optional[str] = None
mounts: dict = {}
ports: dict = {}
env: dict = {}

# secrets
registry_address = Attribute(Optional[str], None)
registry_user = Attribute(Optional[str], None)
registry_password = Attribute(Optional[str], None)

# internal use
_specifier_pattern = r"^(?:(?P<registry>[^\/]+)\/)?(?P<container>[^:]+)(?::(?P<tag>[^\/]+))?$"
_required_params_ = {
"image": "mysql",
}

def configure(self):
match = re.match(self._specifier_pattern, self.image)

if match:
spec_registry = match.group("registry")
spec_containername = match.group("container")
spec_tag = match.group("tag")

# the image-provided registry addresss overrides the default one
if spec_registry:
self.registry_address = spec_registry

# the container_name argument overrides the image-provided container specifier
if not self.container_name:
self.container_name = spec_containername.replace("/", "_")

# spec version overrides the argument provided or default version
if spec_tag:
self.version = spec_tag
else:
raise RuntimeError(
f"could not match the docker spec against the provided container image string: '{self.image}'"
)

if self.registry_password:
self += File(
"registry-password", mode=0o600, content=self.registry_password
)
self.password_file = self._

if self.envfile is None:
self += File(
f"{self.container_name}_env",
sensitive_data=True,
content="""{% for key, value in component.env.items() | sort -%}
{{key}}={{value}}
{% endfor %}""",
)
self.envfile = self._

self += File(
f"/etc/local/nixos/docker_{self.container_name}.nix",
sensitive_data=True,
source=os.path.join(
os.path.dirname(__file__), "resources/oci-template.nix"
),
)

def verify(self):
local_digest, stderr = self.cmd(
"docker image inspect {{component.image}} | jq -r 'first | .RepoDigests | first | split(\"@\") | last' || echo image not available locally"
)
remote_digest, stderr = self.cmd(
"docker manifest inspect {{component.image}} -v | jq -r 'if type ==\"array\" then (. | first) else . end | .Descriptor.digest'"
)

if local_digest != remote_digest:
raise UpdateNeeded()
53 changes: 53 additions & 0 deletions src/batou_ext/resources/oci-template.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{ ... }: {
flyingcircus = {
services.sensu-client.checks."docker-{{component.container_name}}" = {
notification = "Status of container {{component.container_name}}";
command = ''
if $(systemctl is-active --quiet docker-{{component.container_name}}); then
echo "docker container {{component.container_name}} is ok"
else
echo "docker container {{component.container_name}} is inactive"
exit 2
fi
'';
};
};


virtualisation.oci-containers = {
backend = "docker";
containers."{{component.container_name}}" = {
# {% if component.entrypoint != None %}
entrypoint = "{{component.entrypoint}}";
# {% endif %}

# {% if component.registry_address %}
login = {
registry = "{{component.registry_address}}";

# {% if component.registry_user and component.registry_password %}
username = "{{component.registry_user}}";
passwordFile = "{{component.password_file.path}}";
# {% endif %}
};
# {% endif %}

extraOptions = [ "--pull=always" ];

volumes = [
# {% for key, value in component.mounts.items() | sort %}
"{{key}}:{{value}}"
# {% endfor %}
];

image = "{{component.image}}";
environmentFiles = [ {{component.envfile.path}} ];

ports = [
# {% for key, value in component.ports.items() | sort %}
"{{key}}:{{value}}"
# {% endfor %}
];
};
};
}

0 comments on commit 86ca9fb

Please sign in to comment.