diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..1c18b2d Binary files /dev/null and b/.DS_Store differ diff --git a/docker-socket-proxy/Dockerfile b/docker-socket-proxy/Dockerfile new file mode 100755 index 0000000..b621301 --- /dev/null +++ b/docker-socket-proxy/Dockerfile @@ -0,0 +1,118 @@ +ARG BUILD_FROM +FROM ${BUILD_FROM} + +# Build arguments +ARG BUILD_ARCH +ARG BUILD_DATE +ARG BUILD_DESCRIPTION +ARG BUILD_NAME +ARG BUILD_REF +ARG BUILD_REPOSITORY +ARG BUILD_VERSION +ARG TELEGRAF_VERSION +ARG BASHIO_VERSION +ARG TEMPIO_VERSION + +# Environment variables +ENV \ + CARGO_NET_GIT_FETCH_WITH_CLI=true \ + HOME="/root" \ + LANG="C.UTF-8" \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_FIND_LINKS=https://wheels.home-assistant.io/musllinux/ \ + PIP_NO_CACHE_DIR=1 \ + PIP_PREFER_BINARY=1 \ + PS1="$(whoami)@$(hostname):$(pwd)$ " \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + YARN_HTTP_TIMEOUT=1000000 \ + TERM="xterm-256color" + +# Copy root filesystem +COPY rootfs / + +# Set shell +SHELL ["/bin/ash", "-o", "pipefail", "-c"] + +RUN \ + set -o pipefail && \ + apk add --no-cache --virtual .build-dependencies \ + tar=1.34-r0 \ + xz=5.2.5-r1 && \ + apk add --no-cache \ + libcrypto1.1=1.1.1o-r0 \ + libssl1.1=1.1.1o-r0 \ + musl-utils=1.2.3-r0 \ + musl=1.2.3-r0 && \ + apk add --no-cache \ + bash=5.1.16-r2 \ + curl=7.83.1-r1 \ + jq=1.6-r1 \ + tzdata=2022a-r0 && \ + curl -J -L -o /tmp/bashio.tar.gz \ + "https://github.com/hassio-addons/bashio/archive/v0.14.3.tar.gz" && \ + mkdir /tmp/bashio && \ + tar zxvf \ + /tmp/bashio.tar.gz \ + --strip 1 -C /tmp/bashio && \ + mv /tmp/bashio/lib /usr/lib/bashio && \ + ln -s /usr/lib/bashio/bashio /usr/bin/bashio && \ + curl -L -s -o /usr/bin/tempio \ + "https://github.com/home-assistant/tempio/releases/download/2021.09.0/tempio_${BUILD_ARCH}" && \ + chmod a+x /usr/bin/tempio && \ + apk del --no-cache --purge .build-dependencies && \ + rm -f -r \ + /tmp/* + +ENV ALLOW_RESTARTS=0 \ + AUTH=0 \ + BUILD=0 \ + COMMIT=0 \ + CONFIGS=0 \ + CONTAINERS=0 \ + DISTRIBUTION=0 \ + EVENTS=1 \ + EXEC=0 \ + GRPC=0 \ + IMAGES=0 \ + INFO=0 \ + LOG_LEVEL=info \ + NETWORKS=0 \ + NODES=0 \ + PING=1 \ + PLUGINS=0 \ + POST=0 \ + SECRETS=0 \ + SERVICES=0 \ + SESSION=0 \ + SOCKET_PATH=/var/run/docker.sock \ + SWARM=0 \ + SYSTEM=0 \ + TASKS=0 \ + VERSION=1 \ + VOLUMES=0 +COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg + +EXPOSE 2375 + +COPY entrypoint.sh /entrypoint.sh +COPY settings.sh /settings.sh +ENTRYPOINT ["/entrypoint.sh"] +CMD ["telegraf"] + +# Labels +LABEL \ + io.hass.name="${BUILD_NAME}" \ + io.hass.description="${BUILD_DESCRIPTION}" \ + io.hass.arch="${BUILD_ARCH}" \ + io.hass.type="addon" \ + io.hass.version=${BUILD_VERSION} \ + maintainer="fbonelle" \ + org.opencontainers.image.title="${BUILD_NAME}" \ + org.opencontainers.image.description="${BUILD_DESCRIPTION}" \ + org.opencontainers.image.vendor="fbonelle's addons" \ + org.opencontainers.image.authors="fbonelle" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.created=${BUILD_DATE} \ + org.opencontainers.image.revision=${BUILD_REF} \ + org.opencontainers.image.version=${BUILD_VERSION} diff --git a/docker-socket-proxy/apparmor.txt b/docker-socket-proxy/apparmor.txt new file mode 100644 index 0000000..3e887c3 --- /dev/null +++ b/docker-socket-proxy/apparmor.txt @@ -0,0 +1,52 @@ +#include + +profile hassio_telegraf flags=(attach_disconnected,mediate_deleted) { + #include + + # Capabilities + file, + signal (send) set=(kill,term,int,hup,cont), + + # S6-Overlay + /init ix, + /bin/** ix, + /usr/bin/** ix, + /run/{s6,s6-rc*,service}/** ix, + /package/** ix, + /command/** ix, + /etc/services.d/** rwix, + /etc/cont-init.d/** rwix, + /etc/cont-finish.d/** rwix, + /run/{,**} rwk, + /dev/tty rw, + + # Bashio + /usr/lib/bashio/** ix, + /tmp/** rwk, + + # Access to options.json and other files within your addon + /data/** rw, + + # Start new profile for service + /usr/bin/myprogram cx -> myprogram, + + profile myprogram flags=(attach_disconnected,mediate_deleted) { + #include + + # Receive signals from S6-Overlay + signal (receive) peer=*_ADDON_SLUG, + + # Access to options.json and other files within your addon + /data/** rw, + + # Access to mapped volumes specified in config.json + /share/** rw, + + # Access required for service functionality + /usr/bin/myprogram r, + /bin/bash rix, + /bin/echo ix, + /etc/passwd r, + /dev/tty rw, + } +} \ No newline at end of file diff --git a/docker-socket-proxy/build.yaml b/docker-socket-proxy/build.yaml new file mode 100644 index 0000000..f1a7fca --- /dev/null +++ b/docker-socket-proxy/build.yaml @@ -0,0 +1,6 @@ +build_from: + aarch64: library/haproxy:2.6-alpine + amd64: library/haproxy:2.6-alpine + armhf: library/haproxy:2.6-alpine + armv7: library/haproxy:2.6-alpine + i386: library/haproxy:2.6-alpine diff --git a/docker-socket-proxy/config.yaml b/docker-socket-proxy/config.yaml new file mode 100644 index 0000000..9a78f13 --- /dev/null +++ b/docker-socket-proxy/config.yaml @@ -0,0 +1,85 @@ +--- +name: Docker-Socket-Proxy +version: 2.6 +slug: hassio_docker_socket_proxy +description: An addon to enable TCP docker access. +url: https://gitea.bonelle-family.dscloud.biz/francois.bonelle/hassio-repo.git +init: false +arch: + - aarch64 + - amd64 + - armhf + - armv7 + - i386 +ports: + 2375/tcp: 2375 +hassio_api: true +hassio_role: default +host_network: true +auth_api: true +privileged: +- SYS_ADMIN +apparmor: false +map: +- config:rw +- ssl:rw +- addons:rw +- backup:rw +- share:rw +startup: services +boot: manual +docker_api: true +host_pid: true +full_access: true +options: + events: true + ping: true + version: true + build: false + commit: false + configs: false + containers: false + allow_restart: false + distribution: false + exec: false + grpc: false + images: false + info: false + networks: false + node: false + plugins: false + services: false + session: false + swarm: false + system: false + tasks: false + volumes: false + auth: false + secrets: false + post: false +schema: + events: bool + ping: bool + version: bool + build: bool + commit: bool + configs: bool + containers: bool + allow_restart: bool + distribution: bool + exec: bool + grpc: bool + images: bool + info: bool + networks: bool + node: bool + plugins: bool + services: bool + session: bool + swarm: bool + system: bool + tasks: bool + volumes: bool + auth: bool + secrets: bool + post: bool diff --git a/docker-socket-proxy/entrypoint.sh b/docker-socket-proxy/entrypoint.sh new file mode 100755 index 0000000..179e78f --- /dev/null +++ b/docker-socket-proxy/entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -e + +bashio /settings.sh +. /variables.sh + +# first arg is `-f` or `--some-option` +if [ "${1#-}" != "$1" ]; then + set -- haproxy "$@" +fi + +if [ "$1" = 'haproxy' ]; then + shift # "haproxy" + # if the user wants "haproxy", let's add a couple useful flags + # -W -- "master-worker mode" (similar to the old "haproxy-systemd-wrapper"; allows for reload via "SIGUSR2") + # -db -- disables background mode + set -- haproxy -W -db "$@" +fi + +exec "$@" \ No newline at end of file diff --git a/docker-socket-proxy/haproxy.cfg b/docker-socket-proxy/haproxy.cfg new file mode 100644 index 0000000..90c4d05 --- /dev/null +++ b/docker-socket-proxy/haproxy.cfg @@ -0,0 +1,71 @@ +global + log stdout format raw daemon "${LOG_LEVEL}" + + pidfile /run/haproxy.pid + maxconn 4000 + + # Turn on stats unix socket + server-state-file /var/lib/haproxy/server-state + +defaults + mode http + log global + option httplog + option dontlognull + option http-server-close + option redispatch + retries 3 + timeout http-request 10s + timeout queue 1m + timeout connect 10s + timeout client 10m + timeout server 10m + timeout http-keep-alive 10s + timeout check 10s + maxconn 3000 + + # Allow seamless reloads + load-server-state-from-file global + + # Use provided example error pages + errorfile 400 /usr/local/etc/haproxy/errors/400.http + errorfile 403 /usr/local/etc/haproxy/errors/403.http + errorfile 408 /usr/local/etc/haproxy/errors/408.http + errorfile 500 /usr/local/etc/haproxy/errors/500.http + errorfile 502 /usr/local/etc/haproxy/errors/502.http + errorfile 503 /usr/local/etc/haproxy/errors/503.http + errorfile 504 /usr/local/etc/haproxy/errors/504.http + +backend dockerbackend + server dockersocket $SOCKET_PATH + +frontend dockerfrontend + bind :2375 + http-request deny unless METH_GET || { env(POST) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/[a-zA-Z0-9_.-]+/((stop)|(restart)|(kill)) } { env(ALLOW_RESTARTS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/auth } { env(AUTH) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/build } { env(BUILD) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/commit } { env(COMMIT) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/configs } { env(CONFIGS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers } { env(CONTAINERS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/distribution } { env(DISTRIBUTION) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/events } { env(EVENTS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/exec } { env(EXEC) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/grpc } { env(GRPC) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/images } { env(IMAGES) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/info } { env(INFO) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/networks } { env(NETWORKS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/nodes } { env(NODES) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/_ping } { env(PING) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/plugins } { env(PLUGINS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/secrets } { env(SECRETS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/services } { env(SERVICES) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/session } { env(SESSION) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/swarm } { env(SWARM) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/system } { env(SYSTEM) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/tasks } { env(TASKS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/version } { env(VERSION) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/volumes } { env(VOLUMES) -m bool } + http-request deny + default_backend dockerbackend + \ No newline at end of file diff --git a/docker-socket-proxy/rootfs/etc/conf-init.d/00-banner.sh b/docker-socket-proxy/rootfs/etc/conf-init.d/00-banner.sh new file mode 100755 index 0000000..c031ad8 --- /dev/null +++ b/docker-socket-proxy/rootfs/etc/conf-init.d/00-banner.sh @@ -0,0 +1,37 @@ +#!/command/with-contenv bashio +# ============================================================================== +# Home Assistant Community Add-on: Base Images +# Displays a simple add-on banner on startup +# ============================================================================== +if bashio::supervisor.ping; then + bashio::log.blue \ + '-----------------------------------------------------------' + bashio::log.blue " Add-on: $(bashio::addon.name)" + bashio::log.blue " $(bashio::addon.description)" + bashio::log.blue \ + '-----------------------------------------------------------' + + bashio::log.blue " Add-on version: $(bashio::addon.version)" + if bashio::var.true "$(bashio::addon.update_available)"; then + bashio::log.magenta ' There is an update available for this add-on!' + bashio::log.magenta \ + " Latest add-on version: $(bashio::addon.version_latest)" + bashio::log.magenta ' Please consider upgrading as soon as possible.' + else + bashio::log.green ' You are running the latest version of this add-on.' + fi + + bashio::log.blue " System: $(bashio::info.operating_system)" \ + " ($(bashio::info.arch) / $(bashio::info.machine))" + bashio::log.blue " Home Assistant Core: $(bashio::info.homeassistant)" + bashio::log.blue " Home Assistant Supervisor: $(bashio::info.supervisor)" + + bashio::log.blue \ + '-----------------------------------------------------------' + bashio::log.blue \ + ' Please, share the above information when looking for help' + bashio::log.blue \ + ' or support in, e.g., GitHub, forums or the Discord chat.' + bashio::log.blue \ + '-----------------------------------------------------------' +fi \ No newline at end of file diff --git a/docker-socket-proxy/rootfs/etc/conf-init.d/01-log-level.sh b/docker-socket-proxy/rootfs/etc/conf-init.d/01-log-level.sh new file mode 100755 index 0000000..b3b1295 --- /dev/null +++ b/docker-socket-proxy/rootfs/etc/conf-init.d/01-log-level.sh @@ -0,0 +1,46 @@ +#!/command/with-contenv bashio +# ============================================================================== +# Home Assistant Community Add-on: Base Images +# Sets the log level correctly +# ============================================================================== +declare log_level + +# Check if the log level configuration option exists +if bashio::config.exists log_level; then + + # Find the matching LOG_LEVEL + log_level=$(bashio::string.lower "$(bashio::config log_level)") + case "${log_level}" in + all) + log_level="${__BASHIO_LOG_LEVEL_ALL}" + ;; + trace) + log_level="${__BASHIO_LOG_LEVEL_TRACE}" + ;; + debug) + log_level="${__BASHIO_LOG_LEVEL_DEBUG}" + ;; + info) + log_level="${__BASHIO_LOG_LEVEL_INFO}" + ;; + notice) + log_level="${__BASHIO_LOG_LEVEL_NOTICE}" + ;; + warning) + log_level="${__BASHIO_LOG_LEVEL_WARNING}" + ;; + error) + log_level="${__BASHIO_LOG_LEVEL_ERROR}" + ;; + fatal) + log_level="${__BASHIO_LOG_LEVEL_FATAL}" + ;; + off) + log_level="${__BASHIO_LOG_LEVEL_OFF}" + ;; + *) + bashio::exit.nok "Unknown log_level: ${log_level}" + esac + + bashio::log.blue "Log level is set to ${__BASHIO_LOG_LEVELS[$log_level]}" +fi \ No newline at end of file diff --git a/docker-socket-proxy/settings.sh b/docker-socket-proxy/settings.sh new file mode 100755 index 0000000..7267b10 --- /dev/null +++ b/docker-socket-proxy/settings.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bashio +declare hostname +bashio::require.unprotected + +SETTINGS_CONF=/variables.sh +touch ${SETTINGS_CONF} + +if bashio::var.true 'events'; then + bashio::log.info "Enabling events" + echo "export EVENTS=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling events" + echo "export EVENTS=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'ping'; then + bashio::log.info "Enabling ping" + echo "export PING=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling ping" + echo "export PING=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'version'; then + bashio::log.info "Enabling version" + echo "export VERSION=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling version" + echo "export VERSION=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'build'; then + bashio::log.info "Enabling build" + echo "export BUILD=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling build" + echo "export BUILD=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'commit'; then + bashio::log.info "Enabling commit" + echo "export COMMIT=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling commit" + echo "export COMMIT=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'configs'; then + bashio::log.info "Enabling configs" + echo "export CONFIGS=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling configs" + echo "export CONFIGS=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'allow_restart'; then + bashio::log.info "Enabling allow_restart" + echo "export ALLOW_RESTART=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling allow_restart" + echo "export ALLOW_RESTART=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'distribution'; then + bashio::log.info "Enabling distribution" + echo "export DISTRIBUTION=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling distribution" + echo "export DISTRIBUTION=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'exec'; then + bashio::log.info "Enabling exec" + echo "export EXEC=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling exec" + echo "export EXEC=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'grpc'; then + bashio::log.info "Enabling grpc" + echo "export GRPC=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling grpc" + echo "export GRPC=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'images'; then + bashio::log.info "Enabling images" + echo "export IMAGES=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling images" + echo "export IMAGES=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'info'; then + bashio::log.info "Enabling info" + echo "export INFO=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling info" + echo "export INFO=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'networks'; then + bashio::log.info "Enabling networks" + echo "export NETWORKS=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling networks" + echo "export NETWORKS=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'node'; then + bashio::log.info "Enabling node" + echo "export NODE=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling node" + echo "export NODE=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'plugins'; then + bashio::log.info "Enabling plugins" + echo "export PLUGINS=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling plugins" + echo "export PLUGINS=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'services'; then + bashio::log.info "Enabling services" + echo "export SERVICES=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling services" + echo "export SERVICES=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'session'; then + bashio::log.info "Enabling session" + echo "export SESSION=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling session" + echo "export SESSION=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'swarm'; then + bashio::log.info "Enabling swarm" + echo "export SWARM=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling swarm" + echo "export SWARM=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'system'; then + bashio::log.info "Enabling system" + echo "export SYSTEM=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling system" + echo "export SYSTEM=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'tasks'; then + bashio::log.info "Enabling tasks" + echo "export TASKS=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling tasks" + echo "export TASKS=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'volumes'; then + bashio::log.info "Enabling volumes" + echo "export VOLUMES=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling volumes" + echo "export VOLUMES=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'auth'; then + bashio::log.info "Enabling auth" + echo "export AUTH=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling auth" + echo "export AUTH=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'secrets'; then + bashio::log.info "Enabling secrets" + echo "export SECRETS=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling secrets" + echo "export SECRETS=0" >> ${SETTINGS_CONF} +fi + +if bashio::var.true 'post'; then + bashio::log.info "Enabling post" + echo "export POST=1" >> ${SETTINGS_CONF} +else + bashio::log.info "Disabling post" + echo "export POST=0" >> ${SETTINGS_CONF} +fi + +bashio::log.info "Finished settings config, Starting docker-socker-proxy" diff --git a/telegraf/settings.sh b/telegraf/settings.sh index 5bf21f3..c1c28a9 100755 --- a/telegraf/settings.sh +++ b/telegraf/settings.sh @@ -10,6 +10,4 @@ bashio::log.info "Using custom conf file" rm /etc/telegraf/telegraf.conf cp "${CUSTOM_CONF}" /etc/telegraf/telegraf.conf -bashio::log.info "Finished updating config, Starting Telegraf" - -telegraf +bashio::log.info "Finished updating config, Starting Telegraf" \ No newline at end of file