mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-21 16:16:31 +00:00
WIP: Add support for build docker on local repository (#42)
* Add support for build docker on local repository * Add docker support * finish build * change api * add dockerfile generator * finish it * fix lint * fix path * fix path * fix copy * add debug stuff * fix docker template * cleanups * fix addons * change handling * fix lint / cleanup code * fix lint * tag
This commit is contained in:
parent
956af2bd62
commit
f4cb16ad09
@ -14,7 +14,7 @@ from ..const import (
|
|||||||
FILE_HASSIO_ADDONS, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON,
|
FILE_HASSIO_ADDONS, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON,
|
||||||
ATTR_STARTUP, ATTR_BOOT, ATTR_MAP, ATTR_OPTIONS, ATTR_PORTS, BOOT_AUTO,
|
ATTR_STARTUP, ATTR_BOOT, ATTR_MAP, ATTR_OPTIONS, ATTR_PORTS, BOOT_AUTO,
|
||||||
DOCKER_REPO, ATTR_SCHEMA, ATTR_IMAGE, MAP_CONFIG, MAP_SSL, MAP_ADDONS,
|
DOCKER_REPO, ATTR_SCHEMA, ATTR_IMAGE, MAP_CONFIG, MAP_SSL, MAP_ADDONS,
|
||||||
MAP_BACKUP, ATTR_REPOSITORY, ATTR_URL, ATTR_ARCH)
|
MAP_BACKUP, ATTR_REPOSITORY, ATTR_URL, ATTR_ARCH, ATTR_LOCATON)
|
||||||
from ..config import Config
|
from ..config import Config
|
||||||
from ..tools import read_json_file, write_json_file
|
from ..tools import read_json_file, write_json_file
|
||||||
|
|
||||||
@ -109,6 +109,7 @@ class AddonsData(Config):
|
|||||||
|
|
||||||
# store
|
# store
|
||||||
addon_config[ATTR_REPOSITORY] = repository
|
addon_config[ATTR_REPOSITORY] = repository
|
||||||
|
addon_config[ATTR_LOCATON] = str(addon.parent)
|
||||||
self._addons_cache[addon_slug] = addon_config
|
self._addons_cache[addon_slug] = addon_config
|
||||||
|
|
||||||
except OSError:
|
except OSError:
|
||||||
@ -303,13 +304,31 @@ class AddonsData(Config):
|
|||||||
def get_image(self, addon):
|
def get_image(self, addon):
|
||||||
"""Return image name of addon."""
|
"""Return image name of addon."""
|
||||||
addon_data = self._system_data.get(
|
addon_data = self._system_data.get(
|
||||||
addon, self._addons_cache.get(addon))
|
addon, self._addons_cache.get(addon)
|
||||||
|
)
|
||||||
|
|
||||||
if ATTR_IMAGE not in addon_data:
|
# core repository
|
||||||
|
if addon_data[ATTR_REPOSITORY] == REPOSITORY_CORE:
|
||||||
return "{}/{}-addon-{}".format(
|
return "{}/{}-addon-{}".format(
|
||||||
DOCKER_REPO, self.arch, addon_data[ATTR_SLUG])
|
DOCKER_REPO, self.arch, addon_data[ATTR_SLUG])
|
||||||
|
|
||||||
return addon_data[ATTR_IMAGE].format(arch=self.arch)
|
# Repository with dockerhub images
|
||||||
|
if ATTR_IMAGE in addon_data:
|
||||||
|
return addon_data[ATTR_IMAGE].format(arch=self.arch)
|
||||||
|
|
||||||
|
# Local build addon
|
||||||
|
if addon_data[ATTR_REPOSITORY] == REPOSITORY_LOCAL:
|
||||||
|
return "local/{}-addon-{}".format(self.arch, addon_data[ATTR_SLUG])
|
||||||
|
|
||||||
|
_LOGGER.error("No image for %s", addon)
|
||||||
|
|
||||||
|
def need_build(self, addon):
|
||||||
|
"""Return True if this addon need a local build."""
|
||||||
|
addon_data = self._system_data.get(
|
||||||
|
addon, self._addons_cache.get(addon)
|
||||||
|
)
|
||||||
|
return addon_data[ATTR_REPOSITORY] == REPOSITORY_LOCAL \
|
||||||
|
and not addon_data.get(ATTR_IMAGE)
|
||||||
|
|
||||||
def map_config(self, addon):
|
def map_config(self, addon):
|
||||||
"""Return True if config map is needed."""
|
"""Return True if config map is needed."""
|
||||||
@ -333,12 +352,16 @@ class AddonsData(Config):
|
|||||||
|
|
||||||
def path_extern_data(self, addon):
|
def path_extern_data(self, addon):
|
||||||
"""Return addon data path external for docker."""
|
"""Return addon data path external for docker."""
|
||||||
return str(PurePath(self.config.path_extern_addons_data, addon))
|
return PurePath(self.config.path_extern_addons_data, addon)
|
||||||
|
|
||||||
def path_addon_options(self, addon):
|
def path_addon_options(self, addon):
|
||||||
"""Return path to addons options."""
|
"""Return path to addons options."""
|
||||||
return Path(self.path_data(addon), "options.json")
|
return Path(self.path_data(addon), "options.json")
|
||||||
|
|
||||||
|
def path_addon_location(self, addon):
|
||||||
|
"""Return path to this addon."""
|
||||||
|
return Path(self._addons_cache[addon][ATTR_LOCATON])
|
||||||
|
|
||||||
def write_addon_options(self, addon):
|
def write_addon_options(self, addon):
|
||||||
"""Return True if addon options is written to data."""
|
"""Return True if addon options is written to data."""
|
||||||
schema = self.get_schema(addon)
|
schema = self.get_schema(addon)
|
||||||
|
@ -42,6 +42,11 @@ def initialize_system_data(websession):
|
|||||||
config.path_addons_git)
|
config.path_addons_git)
|
||||||
config.path_addons_git.mkdir(parents=True)
|
config.path_addons_git.mkdir(parents=True)
|
||||||
|
|
||||||
|
if not config.path_addons_build.is_dir():
|
||||||
|
_LOGGER.info("Create Home-Assistant addon build folder %s",
|
||||||
|
config.path_addons_build)
|
||||||
|
config.path_addons_build.mkdir(parents=True)
|
||||||
|
|
||||||
# homeassistant backup folder
|
# homeassistant backup folder
|
||||||
if not config.path_backup.is_dir():
|
if not config.path_backup.is_dir():
|
||||||
_LOGGER.info("Create Home-Assistant backup folder %s",
|
_LOGGER.info("Create Home-Assistant backup folder %s",
|
||||||
|
@ -27,6 +27,7 @@ ADDONS_CORE = PurePath("addons/core")
|
|||||||
ADDONS_LOCAL = PurePath("addons/local")
|
ADDONS_LOCAL = PurePath("addons/local")
|
||||||
ADDONS_GIT = PurePath("addons/git")
|
ADDONS_GIT = PurePath("addons/git")
|
||||||
ADDONS_DATA = PurePath("addons/data")
|
ADDONS_DATA = PurePath("addons/data")
|
||||||
|
ADDONS_BUILD = PurePath("addons/build")
|
||||||
ADDONS_CUSTOM_LIST = 'addons_custom_list'
|
ADDONS_CUSTOM_LIST = 'addons_custom_list'
|
||||||
|
|
||||||
BACKUP_DATA = PurePath("backup")
|
BACKUP_DATA = PurePath("backup")
|
||||||
@ -205,7 +206,7 @@ class CoreConfig(Config):
|
|||||||
@property
|
@property
|
||||||
def path_extern_addons_local(self):
|
def path_extern_addons_local(self):
|
||||||
"""Return path for customs addons."""
|
"""Return path for customs addons."""
|
||||||
return str(PurePath(self.path_extern_hassio, ADDONS_LOCAL))
|
return PurePath(self.path_extern_hassio, ADDONS_LOCAL)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_addons_data(self):
|
def path_addons_data(self):
|
||||||
@ -215,7 +216,12 @@ class CoreConfig(Config):
|
|||||||
@property
|
@property
|
||||||
def path_extern_addons_data(self):
|
def path_extern_addons_data(self):
|
||||||
"""Return root addon data folder extern for docker."""
|
"""Return root addon data folder extern for docker."""
|
||||||
return str(PurePath(self.path_extern_hassio, ADDONS_DATA))
|
return PurePath(self.path_extern_hassio, ADDONS_DATA)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_addons_build(self):
|
||||||
|
"""Return root addon build folder."""
|
||||||
|
return Path(HASSIO_SHARE, ADDONS_BUILD)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_backup(self):
|
def path_backup(self):
|
||||||
@ -225,7 +231,7 @@ class CoreConfig(Config):
|
|||||||
@property
|
@property
|
||||||
def path_extern_backup(self):
|
def path_extern_backup(self):
|
||||||
"""Return root backup data folder extern for docker."""
|
"""Return root backup data folder extern for docker."""
|
||||||
return str(PurePath(self.path_extern_hassio, BACKUP_DATA))
|
return PurePath(self.path_extern_hassio, BACKUP_DATA)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def addons_repositories(self):
|
def addons_repositories(self):
|
||||||
|
@ -67,6 +67,7 @@ ATTR_PASSWORD = 'password'
|
|||||||
ATTR_TOTP = 'totp'
|
ATTR_TOTP = 'totp'
|
||||||
ATTR_INITIALIZE = 'initialize'
|
ATTR_INITIALIZE = 'initialize'
|
||||||
ATTR_SESSION = 'session'
|
ATTR_SESSION = 'session'
|
||||||
|
ATTR_LOCATON = 'location'
|
||||||
|
|
||||||
STARTUP_BEFORE = 'before'
|
STARTUP_BEFORE = 'before'
|
||||||
STARTUP_AFTER = 'after'
|
STARTUP_AFTER = 'after'
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
"""Init file for HassIO addon docker object."""
|
"""Init file for HassIO addon docker object."""
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
|
|
||||||
from . import DockerBase
|
from . import DockerBase
|
||||||
|
from .util import dockerfile_template
|
||||||
from ..tools import get_version_from_env
|
from ..tools import get_version_from_env
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
HASS_DOCKER_NAME = 'homeassistant'
|
|
||||||
|
|
||||||
|
|
||||||
class DockerAddon(DockerBase):
|
class DockerAddon(DockerBase):
|
||||||
"""Docker hassio wrapper for HomeAssistant."""
|
"""Docker hassio wrapper for HomeAssistant."""
|
||||||
@ -30,31 +31,31 @@ class DockerAddon(DockerBase):
|
|||||||
def volumes(self):
|
def volumes(self):
|
||||||
"""Generate volumes for mappings."""
|
"""Generate volumes for mappings."""
|
||||||
volumes = {
|
volumes = {
|
||||||
self.addons_data.path_extern_data(self.addon): {
|
str(self.addons_data.path_extern_data(self.addon)): {
|
||||||
'bind': '/data', 'mode': 'rw'
|
'bind': '/data', 'mode': 'rw'
|
||||||
}}
|
}}
|
||||||
|
|
||||||
if self.addons_data.map_config(self.addon):
|
if self.addons_data.map_config(self.addon):
|
||||||
volumes.update({
|
volumes.update({
|
||||||
self.config.path_extern_config: {
|
str(self.config.path_extern_config): {
|
||||||
'bind': '/config', 'mode': 'rw'
|
'bind': '/config', 'mode': 'rw'
|
||||||
}})
|
}})
|
||||||
|
|
||||||
if self.addons_data.map_ssl(self.addon):
|
if self.addons_data.map_ssl(self.addon):
|
||||||
volumes.update({
|
volumes.update({
|
||||||
self.config.path_extern_ssl: {
|
str(self.config.path_extern_ssl): {
|
||||||
'bind': '/ssl', 'mode': 'rw'
|
'bind': '/ssl', 'mode': 'rw'
|
||||||
}})
|
}})
|
||||||
|
|
||||||
if self.addons_data.map_addons(self.addon):
|
if self.addons_data.map_addons(self.addon):
|
||||||
volumes.update({
|
volumes.update({
|
||||||
self.config.path_extern_addons_local: {
|
str(self.config.path_extern_addons_local): {
|
||||||
'bind': '/addons', 'mode': 'rw'
|
'bind': '/addons', 'mode': 'rw'
|
||||||
}})
|
}})
|
||||||
|
|
||||||
if self.addons_data.map_backup(self.addon):
|
if self.addons_data.map_backup(self.addon):
|
||||||
volumes.update({
|
volumes.update({
|
||||||
self.config.path_extern_backup: {
|
str(self.config.path_extern_backup): {
|
||||||
'bind': '/backup', 'mode': 'rw'
|
'bind': '/backup', 'mode': 'rw'
|
||||||
}})
|
}})
|
||||||
|
|
||||||
@ -102,7 +103,69 @@ class DockerAddon(DockerBase):
|
|||||||
self.container = self.dock.containers.get(self.docker_name)
|
self.container = self.dock.containers.get(self.docker_name)
|
||||||
self.version = get_version_from_env(
|
self.version = get_version_from_env(
|
||||||
self.container.attrs['Config']['Env'])
|
self.container.attrs['Config']['Env'])
|
||||||
_LOGGER.info("Attach to image %s with version %s",
|
|
||||||
self.image, self.version)
|
_LOGGER.info(
|
||||||
|
"Attach to image %s with version %s", self.image, self.version)
|
||||||
except (docker.errors.DockerException, KeyError):
|
except (docker.errors.DockerException, KeyError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _install(self, tag):
|
||||||
|
"""Pull docker image or build it.
|
||||||
|
|
||||||
|
Need run inside executor.
|
||||||
|
"""
|
||||||
|
if self.addons_data.need_build(self.addon):
|
||||||
|
return self._build(tag)
|
||||||
|
|
||||||
|
return super()._install(tag)
|
||||||
|
|
||||||
|
async def build(self, tag):
|
||||||
|
"""Build a docker container."""
|
||||||
|
if self._lock.locked():
|
||||||
|
_LOGGER.error("Can't excute build while a task is in progress")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
return await self.loop.run_in_executor(None, self._build, tag)
|
||||||
|
|
||||||
|
def _build(self, tag):
|
||||||
|
"""Build a docker container.
|
||||||
|
|
||||||
|
Need run inside executor.
|
||||||
|
"""
|
||||||
|
build_dir = Path(self.config.path_addons_build, self.addon)
|
||||||
|
try:
|
||||||
|
# prepare temporary addon build folder
|
||||||
|
try:
|
||||||
|
source = self.addons_data.path_addon_location(self.addon)
|
||||||
|
shutil.copytree(str(source), str(build_dir))
|
||||||
|
except shutil.Error as err:
|
||||||
|
_LOGGER.error("Can't copy %s to temporary build folder -> %s",
|
||||||
|
source, build_dir)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# prepare Dockerfile
|
||||||
|
try:
|
||||||
|
dockerfile_template(
|
||||||
|
Path(build_dir, 'Dockerfile'), self.addons_data.arch, tag)
|
||||||
|
except OSError as err:
|
||||||
|
_LOGGER.error("Can't prepare dockerfile -> %s", err)
|
||||||
|
|
||||||
|
# run docker build
|
||||||
|
try:
|
||||||
|
build_tag = "{}:{}".format(self.image, tag)
|
||||||
|
|
||||||
|
_LOGGER.info("Start build %s on %s", build_tag, build_dir)
|
||||||
|
image = self.dock.images.build(
|
||||||
|
path=str(build_dir), tag=build_tag, pull=True)
|
||||||
|
|
||||||
|
_LOGGER.info("Build %s done", build_tag)
|
||||||
|
image.tag(self.image, tag='latest')
|
||||||
|
except (docker.errors.DockerException, TypeError) as err:
|
||||||
|
_LOGGER.error("Can't build %s -> %s", build_tag, err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(str(build_dir), ignore_errors=True)
|
||||||
|
@ -45,9 +45,9 @@ class DockerHomeAssistant(DockerBase):
|
|||||||
'HASSIO': self.config.api_endpoint,
|
'HASSIO': self.config.api_endpoint,
|
||||||
},
|
},
|
||||||
volumes={
|
volumes={
|
||||||
self.config.path_extern_config:
|
str(self.config.path_extern_config):
|
||||||
{'bind': '/config', 'mode': 'rw'},
|
{'bind': '/config', 'mode': 'rw'},
|
||||||
self.config.path_extern_ssl:
|
str(self.config.path_extern_ssl):
|
||||||
{'bind': '/ssl', 'mode': 'rw'},
|
{'bind': '/ssl', 'mode': 'rw'},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
32
hassio/dock/util.py
Normal file
32
hassio/dock/util.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"""HassIO docker utilitys."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
from ..const import ARCH_AARCH64, ARCH_ARMHF, ARCH_I386, ARCH_AMD64
|
||||||
|
|
||||||
|
|
||||||
|
RESIN_BASE_IMAGE = {
|
||||||
|
ARCH_ARMHF: "resin/armhf-alpine:3.5",
|
||||||
|
ARCH_AARCH64: "resin/aarch64-alpine:3.5",
|
||||||
|
ARCH_I386: "resin/i386-alpine:3.5",
|
||||||
|
ARCH_AMD64: "resin/amd64-alpine:3.5",
|
||||||
|
}
|
||||||
|
|
||||||
|
TMPL_VERSION = re.compile(r"%%VERSION%%")
|
||||||
|
TMPL_IMAGE = re.compile(r"%%BASE_IMAGE%%")
|
||||||
|
|
||||||
|
|
||||||
|
def dockerfile_template(dockerfile, arch, version):
|
||||||
|
"""Prepare a Hass.IO dockerfile."""
|
||||||
|
buff = []
|
||||||
|
resin_image = RESIN_BASE_IMAGE[arch]
|
||||||
|
|
||||||
|
# read docker
|
||||||
|
with dockerfile.open('r') as dock_input:
|
||||||
|
for line in dock_input:
|
||||||
|
line = TMPL_VERSION.sub(version, line)
|
||||||
|
line = TMPL_IMAGE.sub(resin_image, line)
|
||||||
|
buff.append(line)
|
||||||
|
|
||||||
|
# write docker
|
||||||
|
with dockerfile.open('w') as dock_output:
|
||||||
|
dock_output.writelines(buff)
|
Loading…
x
Reference in New Issue
Block a user