diff --git a/.dockerignore b/.dockerignore index 820e358..b95d2cc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,3 @@ build-* tmp +.vscode diff --git a/.github/actions/docker-build-action/action.yml b/.github/actions/docker-build-action/action.yml index 79b389d..72a5e4b 100644 --- a/.github/actions/docker-build-action/action.yml +++ b/.github/actions/docker-build-action/action.yml @@ -53,12 +53,12 @@ runs: using: composite steps: - name: Set up Docker buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Set up QEMU if: ${{ inputs.use_qemu == 'true'}} - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Build image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . push: false diff --git a/.github/actions/metadata-action/action.yml b/.github/actions/metadata-action/action.yml index 78e44d1..2138e8c 100644 --- a/.github/actions/metadata-action/action.yml +++ b/.github/actions/metadata-action/action.yml @@ -44,7 +44,7 @@ runs: steps: - name: Create metadata for docker image id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: ${{ inputs.repository }} # adds the suffix for all tags, even latest. diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 2606158..66adf4d 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -7,21 +7,21 @@ name: Sign and prerelease on: push: branches: - - 'main' + - "main" tags: # semver, e.g. 1.2.0 (does not match 0.1.2) - - '[1-9]+.[0-9]+.[0-9]+' + - "[1-9]+.[0-9]+.[0-9]+" # semver with prerelease info, e.g. 1.0.2-beta.1 or 1.2.3-rc.10 - - '[1-9]+.[0-9]+.[0-9]+-[a-z]+.[0-9]+' + - "[1-9]+.[0-9]+.[0-9]+-[a-z]+.[0-9]+" # do not match prerelease starting w/ 0, e.g. 1.0.2-beta.0 or 1.2.3-rc.01 - - '![1-9]+.[0-9]+.[0-9]+-[a-z]+.[0]*' + - "![1-9]+.[0-9]+.[0-9]+-[a-z]+.[0]*" # semver with date info, e.g. 1.0.2-20221125 - - '[1-9]+.[0-9]+.[0-9]+-[0-9]+' + - "[1-9]+.[0-9]+.[0-9]+-[0-9]+" # do not match date starting w/ 0, e.g. 1.0.2-01232023 - - '![1-9]+.[0-9]+.[0-9]+-[0]*' + - "![1-9]+.[0-9]+.[0-9]+-[0]*" pull_request: branches: - - 'main' + - "main" jobs: # Builds docker ACAP using the build.sh script, then signs the eap-file in @@ -43,8 +43,8 @@ jobs: uses: ./.github/actions/metadata-action with: suffix: -${{ matrix.arch }} - repository: 'docker-compose-acap' - get_version: 'true' + repository: "docker-compose-acap" + get_version: "true" - name: Update manifest file if: ( github.ref_type == 'tag') uses: ./.github/actions/update-acap-manifest-action @@ -91,7 +91,7 @@ jobs: echo "HTTP_RESPONSE is empty or not a valid integer: $HTTP_RESPONSE" fi - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.SIGNED_EAP_FILE }} path: build/${{ env.SIGNED_EAP_FILE }} @@ -123,7 +123,7 @@ jobs: id: vars run: echo "TAG=${GITHUB_REF#refs/*/}" >> ${GITHUB_ENV} - name: Create prerelease - uses: actions/github-script@v6 + uses: actions/github-script@v7 id: prerelease env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 365cd7d..667f2b2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,7 +16,7 @@ jobs: fetch-depth: 0 - name: Lint codebase - uses: super-linter/super-linter/slim@v5 + uses: super-linter/super-linter/slim@v6 env: VALIDATE_ALL_CODEBASE: true DEFAULT_BRANCH: main diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..e63e6f4 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "DavidAnson.vscode-markdownlint", + "editorconfig.editorconfig", + "streetsidesoftware.code-spell-checker" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f57b11c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "C_Cpp.clang_format_style": "file", + "[markdown]": { + "editor.defaultFormatter": "DavidAnson.vscode-markdownlint", + "editor.formatOnSave": true, + "editor.formatOnPaste": true + }, + "markdown.extension.list.indentationSize": "inherit", + "markdown.extension.toc.levels": "1..3", + "cSpell.words": [ + "anyauth", + "Buildx", + "containerd", + "dockerdwrapperwithcompose", + "rootpasswd", + "VAPIX" + ] +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f25481e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,232 @@ + +# Regarding contributions + +All types of contributions are encouraged and valued. See the [Table of contents](#table-of-contents) +for different ways to help and details about how this project handles them. Please make sure to read +the relevant section before making your contribution. It will make it a lot easier for us maintainers +and smooth out the experience for all involved. We look forward to your contributions. + +> And if you like the project, but just don't have time to contribute, that's fine. There are other +> easy ways to support the project and show your appreciation, which we would also be very happy about: +> +> - Star the project +> - Tweet about it +> - Refer this project in your project's readme +> - Mention the project at local meetups and tell your friends/colleagues + + +## Table of contents + +- [I have a question](#i-have-a-question) +- [I want to contribute](#i-want-to-contribute) + - [Reporting bugs](#reporting-bugs) + - [Before submitting a bug report](#before-submitting-a-bug-report) + - [How do I submit a good bug report?](#how-do-i-submit-a-good-bug-report) + - [Suggesting enhancements](#suggesting-enhancements) + - [Before Submitting an Enhancement](#before-submitting-an-enhancement) + - [How do I submit a good enhancement suggestion?](#how-do-i-submit-a-good-enhancement-suggestion) + - [Your first code contribution](#your-first-code-contribution) + - [Lint of codebase](#lint-of-codebase) + - [Linters in GitHub Action](#linters-in-github-action) + - [Run super-linter locally](#run-super-linter-locally) + - [Run super-linter interactively](#run-super-linter-interactively) + +## I have a question + +Before you ask a question, it is best to search for existing [issues][issues] that might help you. +In case you have found a suitable issue and still need clarification, you can write your question in +this issue. It is also advisable to search the internet for answers first. + +If you then still feel the need to ask a question and need clarification, please +follow the steps in [Reporting bugs](#reporting-bugs). + +## I want to contribute + +### Reporting bugs + +#### Before submitting a bug report + +A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we +ask you to investigate carefully, collect information and describe the issue in detail in your report. +Please complete the following steps in advance to help us fix any potential bug as fast as possible: + +- Make sure that you are using the latest version. +- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment + components/versions. +- To see if other users have experienced (and potentially already solved) the same issue you are having, + check if there is not already a bug report existing for your bug or error in the [bug tracker][issues_bugs]. +- Also make sure to search the internet to see if users outside of the GitHub community have discussed + the issue. +- Collect information about the bug: + - Axis device model + - Axis device firmware version + - Stack trace + - OS and version (Windows, Linux, macOS, x86, ARM) + - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what + seems relevant + - Possibly your input and the output + - Can you reliably reproduce the issue? And can you also reproduce it with older versions? + +#### How do I submit a good bug report? + +We use GitHub issues to track bugs and errors. If you run into an issue with the project: + +- Open an [issue][issues_new]. +- Explain the behavior you would expect and the actual behavior. +- Please provide as much context as possible and describe the *reproduction steps* that someone else + can follow to recreate the issue on their own. +- Provide the information you collected in the previous section. + +Once it's filed: + +- The project team will label the issue accordingly. +- A team member will try to reproduce the issue with your provided steps. If there are no reproduction + steps or no obvious way to reproduce the issue, the team will ask you for those steps. Bugs without + steps will not be addressed until they can be reproduced. +- If the team is able to reproduce the issue, it will be prioritized according to severity. + +### Suggesting enhancements + +This section guides you through submitting an enhancement suggestion, +**including completely new features and minor improvements to existing functionality**. +Following these guidelines will help maintainers and the community to understand your suggestion and +find related suggestions. + +#### Before Submitting an Enhancement + +- Make sure that you are using the latest version. +- Read the documentation carefully and find out if the functionality is already covered, maybe by an + individual configuration. +- Perform a [search][issues] to see if the enhancement has already been suggested. If it has, add a + comment to the existing issue instead of opening a new one. +- Find out whether your idea fits with the scope and aims of the project. Keep in mind that we want + features that will be useful to the majority of our users and not just a small subset. + +#### How do I submit a good enhancement suggestion? + +Enhancement suggestions are tracked as [GitHub issues][issues]. + +- Use a **clear and descriptive title** for the issue to identify the suggestion. +- Provide a **step-by-step description of the suggested enhancement** in as many details as possible. +- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. + At this point you can also tell which alternatives do not work for you. +- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or + point out the part which the suggestion is related to. +- **Explain why this enhancement would be useful** to most users. You may also want to point out the + other projects that solved it better and which could serve as inspiration. + +### Your first code contribution + +Start by [forking the repository](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo), +i.e. copying the repository to your account to grant you write access. Continue with cloning the +forked repository to your local machine. + +```sh +git clone https://github.com//AxisCommunications/docker-compose-acap.git +``` + +Navigate into the cloned directory and create a new branch: + +```sh +cd docker-compose-acap +git switch -c +``` + +Update the code according to your requirements, and commit the changes using the +[conventional commits](https://www.conventionalcommits.org) message style: + +```sh +git commit -a -m 'Follow the conventional commit messages style to write this message' +``` + +Continue with pushing the local commits to GitHub: + +```sh +git push origin +``` + +Before opening a Pull Request (PR), please consider the following guidelines: + +- Please make sure that the code builds perfectly fine on your local system. +- Make sure that all linters pass, see [Lint of codebase](#lint-of-codebase) +- The PR will have to meet the code standard already available in the repository. +- Explanatory comments related to code functions are required. Please write code comments for a better + understanding of the code for other developers. +- Note that code changes or additions to the `.github` folder (or sub-folders) will not be accepted. + +And finally when you are satisfied with your changes, open a new PR. + +### Lint of codebase + +A set of different linters test the codebase and these must pass in order to get a pull request approved. + +#### Linters in GitHub Action + +When you create a pull request, a set of linters will run syntax and format checks on different file +types in GitHub actions by making use of a tool called [super-linter][super-linter]. If any of the +linters gives an error, this will be shown in the action connected to the pull request. + +In order to speed up development, it's possible to run linters as part of your local development environment. + +#### Run super-linter locally + +Since super-linter is using a Docker image in GitHub Actions, users of other editors may run it locally +to lint the codebase. For complete instructions and guidance, see super-linter page for [running locally][super-linter-local]. + +To run a number of linters on the codebase from command line: + +```sh +docker run --rm \ + -v $PWD:/tmp/lint \ + -e RUN_LOCAL=true \ + -e LINTER_RULES_PATH=/ \ + -e VALIDATE_BASH=true \ + -e VALIDATE_DOCKERFILE_HADOLINT=true \ + -e VALIDATE_MARKDOWN=true \ + -e VALIDATE_SHELL_SHFMT=true \ + -e VALIDATE_YAML=true \ + ghcr.io/super-linter/super-linter:slim-v6 +``` + +See [`.github/workflows/lint.yml`](.github/workflows/lint.yml) for the exact setup used by this project. + +#### Run super-linter interactively + +It might be more convenient to run super-linter interactively. Run container and enter command line: + +```sh +docker run --rm \ + -v $PWD:/tmp/lint \ + -w /tmp/lint \ + --entrypoint /bin/bash \ + -it ghcr.io/super-linter/super-linter:slim-v6 +``` + +Then from the container terminal, the following commands can lint the the code base for different +file types: + +```sh +# Lint Dockerfile files +hadolint $(find -type f -name "Dockerfile*") + +# Lint Markdown files +markdownlint . + +# Lint YAML files +yamllint . + +# Lint shell script files +shellcheck $(shfmt -f .) +shfmt -d . +``` + +To lint only a specific file, replace `.` or `$(COMMAND)` with the file path. + + +[issues]: https://github.com/AxisCommunications/docker-compose-acap/issues +[issues_new]: https://github.com/AxisCommunications/docker-compose-acap/issues/new +[issues_bugs]: https://github.com/AxisCommunications/docker-compose-acap/issues?q=label%3Abug +[super-linter]: https://github.com/super-linter/super-linter +[super-linter-local]: https://github.com/super-linter/super-linter/blob/main/docs/run-linter-locally.md + + diff --git a/Dockerfile b/Dockerfile index 2857696..c237d00 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,12 @@ # syntax=docker/dockerfile:1 -ARG DOCKER_VERSION=24.0.2 -ARG DOCKER_COMPOSE_VERSION=v2.18.1 +ARG DOCKER_VERSION=26.0.0 +ARG DOCKER_COMPOSE_VERSION=v2.25.0 ARG REPO=axisecp ARG ACAPARCH=armv7hf -ARG VERSION=1.10 +ARG VERSION=1.3 ARG UBUNTU_VERSION=22.04 ARG NATIVE_SDK=acap-native-sdk @@ -40,7 +40,7 @@ RUN git clone --depth 1 -b $PROCPS_VERSION 'https://gitlab.com/procps-ng/procps' ARG BUILD_CACHE=build.cache RUN echo ac_cv_func_realloc_0_nonnull=yes >$BUILD_CACHE \ - && echo ac_cv_func_malloc_0_nonnull=yes >>$BUILD_CACHE + && echo ac_cv_func_malloc_0_nonnull=yes >>$BUILD_CACHE RUN < Meanwhile, the solution is to allow root to be able to install the Docker Compose ACAP. > > On the web page of the device: +> +> > 1. Go to the Apps page, toggle on `Allow root-privileged apps`. > 1. Go to System -> Account page, under SSH accounts toggle off `Restrict root access` to be able to send the TLS certificates. Make sure to set the password of the `root` SSH user. @@ -76,19 +78,34 @@ It's also possible to build and use a locally built image. See the ## Securing the Docker Compose ACAP using TLS -The Docker Compose ACAP can be run either unsecured or in TLS mode. The Docker Compose ACAP uses -TLS as default. Use the "Use TLS" dropdown in the web interface to switch -between the two different modes. It's also possible to toggle this option by -calling the parameter management API in [VAPIX](https://www.axis.com/vapix-library/) and setting the -`root.dockerdwrapperwithcompose.UseTLS` parameter to `yes` or `no`. The following commands would -enable TLS: +The Docker Compose ACAP application can be run in either TLS mode or unsecured mode. The Docker Compose +ACAP application uses TLS mode by default. It is important to note that Dockerd will fail to start if +TCP socket or IPC socket parameters are not selected, one of these sockets must be set to `yes`. + +Use the "Use TLS" and "TCP Socket" dropdowns in the web interface to switch between the +two different modes(yes/no). Whenever these settings change, the Docker daemon will automatically restart. +It's also possible to toggle this option by calling the parameter management API in +[VAPIX](https://www.axis.com/vapix-library/) and setting `root.dockerdwrapperwithcompose.UseTLS` and +`root.dockerdwrapperwithcompose.TCPSocket` parameters to `yes` or `no`. +The following commands would enable those parameters: ```sh DEVICE_IP= DEVICE_PASSWORD='' +``` + +Enable TLS: + +```sh +curl -s --anyauth -u "root:$DEVICE_PASSWORD" \ + "http://$DEVICE_IP/axis-cgi/param.cgi?action=update&root.dockerdwrapper.UseTLS=yes" +``` + +Enable TCP Socket: +```sh curl -s --anyauth -u "root:$DEVICE_PASSWORD" \ - "http://$DEVICE_IP/axis-cgi/param.cgi?action=update&root.dockerdwrapperwithcompose.UseTLS=yes" + "http://$DEVICE_IP/axis-cgi/param.cgi?action=update&root.dockerdwrapperwithcompose.TCPSocket=yes" ``` Note that the dockerd service will be restarted every time TLS is activated or @@ -235,6 +252,10 @@ See [Client key and certificate](#client-key-and-certificate) for an example of how to remotely run docker commands on a device running a secured Docker Compose ACAP using TLS. +The application can provide a TCP socket if the TCP Socket setting is set to `yes` and an IPC socket +if the IPC Socket setting is set to `yes`. Please be aware that at least one of these sockets must be +selected for the application to start. + ## Building the Docker Compose ACAP To build the Docker Compose ACAP use docker buildx with the provided Dockerfile: @@ -270,6 +291,14 @@ Go to your device web page above > Click on the tab **App** in the device GUI > Add **(+)** sign and browse to the newly built .eap-file > Click **Install** > Run the application by enabling the **Start** switch. +## Contributing + +Take a look at the [CONTRIBUTING.md](CONTRIBUTING.md) file. + +## License + +[Apache 2.0](LICENSE) + [all-releases]: https://github.com/AxisCommunications/docker-compose-acap/releases diff --git a/app/dockerdwrapperwithcompose.c b/app/dockerdwrapperwithcompose.c index 6a8412f..04f70f4 100644 --- a/app/dockerdwrapperwithcompose.c +++ b/app/dockerdwrapperwithcompose.c @@ -22,9 +22,21 @@ #include #include #include +#include #include #include +struct settings { + char *data_root; + bool use_tls; + bool use_tcp_socket; + bool use_ipc_socket; +}; + +struct app_state { + char *sd_card_area; +}; + /** * @brief Callback called when the dockerd process exits. */ @@ -36,17 +48,32 @@ static void dockerd_process_exited_callback(__attribute__((unused)) GPid pid, // Loop run on the main process static GMainLoop *loop = NULL; -// Exit code -static int exit_code = 0; +// Exit code of this program. Set using 'quit_program()'. +#define EX_KEEP_RUNNING -1 +static int application_exit_code = EX_KEEP_RUNNING; // Pid of the running dockerd process static pid_t dockerd_process_pid = -1; -// Full path to the SD card -static const char *sd_card_path = "/var/spool/storage/SD_DISK"; +// All ax_parameters the acap has +static const char *ax_parameters[] = {"IPCSocket", + "SDCardSupport", + "TCPSocket", + "UseTLS"}; + +static const char *tls_cert_path = + "/usr/local/packages/dockerdwrapperwithcompose/"; -// True if the dockerd_exited_callback should restart dockerd -static bool restart_dockerd = false; +static const char *tls_certs[] = {"ca.pem", + "server-cert.pem", + "server-key.pem"}; + +static void +quit_program(int exit_code) +{ + application_exit_code = exit_code; + g_main_loop_quit(loop); +} /** * @brief Signals handling @@ -60,7 +87,7 @@ handle_signals(__attribute__((unused)) int signal_num) case SIGINT: case SIGTERM: case SIGQUIT: - g_main_loop_quit(loop); + quit_program(EX_OK); } } @@ -140,21 +167,21 @@ get_parameter_value(const char *parameter_name) } /** - * @brief Retrieve the file system type of the SD card as a string. + * @brief Retrieve the file system type of the device containing this path. * * @return The file system type as a string (ext4/ext3/vfat etc...) if * successful, NULL otherwise. */ static char * -get_sd_filesystem(void) +get_filesystem_of_path(const char *path) { char buf[PATH_MAX]; struct stat sd_card_stat; - int stat_result = stat(sd_card_path, &sd_card_stat); + int stat_result = stat(path, &sd_card_stat); if (stat_result != 0) { syslog(LOG_ERR, "Cannot store data on the SD card, no storage exists at %s", - sd_card_path); + path); return NULL; } @@ -193,47 +220,206 @@ get_sd_filesystem(void) * @return True if successful, false if setup failed. */ static bool -setup_sdcard(void) +setup_sdcard(const char *data_root) { - const char *data_root = "/var/spool/storage/SD_DISK/dockerd/data"; - const char *exec_root = "/var/spool/storage/SD_DISK/dockerd/exec"; - char *create_droot_command = g_strdup_printf("mkdir -p %s", data_root); - char *create_eroot_command = g_strdup_printf("mkdir -p %s", exec_root); + g_autofree char *sd_file_system = NULL; + g_autofree char *create_droot_command = + g_strdup_printf("mkdir -p %s", data_root); + int res = system(create_droot_command); if (res != 0) { syslog(LOG_ERR, "Failed to create data_root folder at: %s. Error code: %d", data_root, res); - goto end; + return false; } - res = system(create_eroot_command); - if (res != 0) { + + // Confirm that the SD card is usable + sd_file_system = get_filesystem_of_path(data_root); + if (sd_file_system == NULL) { syslog(LOG_ERR, - "Failed to create exec_root folder at: %s. Error code: %d", - exec_root, - res); - goto end; + "Couldn't identify the file system of the SD card at %s", + data_root); + return false; } - res = 0; + if (strcmp(sd_file_system, "vfat") == 0 || + strcmp(sd_file_system, "exfat") == 0) { + syslog(LOG_ERR, + "The SD card at %s uses file system %s which does not support " + "Unix file permissions. Please reformat to a file system that " + "support Unix file permissions, such as ext4 or xfs.", + data_root, + sd_file_system); + return false; + } -end: + if (access(data_root, F_OK) == 0 && access(data_root, W_OK) != 0) { + syslog(LOG_ERR, + "The application user does not have write permissions to the SD " + "card directory at %s. Please change the directory permissions or " + "remove the directory.", + data_root); + return false; + } + + return true; +} - free(create_droot_command); - free(create_eroot_command); +// A parameter of type "bool:no,yes" is guaranteed to contain one of those +// strings, but user code is still needed to interpret it as a Boolean type. +static bool +is_parameter_yes(const char *name) +{ + g_autofree char *value = get_parameter_value(name); + return value && strcmp(value, "yes") == 0; +} - return res == 0; +// Return data root matching the current SDCardSupport selection. +// +// If SDCardSupport is "yes", data root will be located on the proved SD card +// area. Passing NULL as SD card area signals that the SD card is not availble. +static char * +prepare_data_root(const char *sd_card_area) +{ + if (is_parameter_yes("SDCardSupport")) { + if (!sd_card_area) { + syslog( + LOG_ERR, + "SD card was requested, but no SD card is available at the moment."); + return NULL; + } + char *data_root = g_strdup_printf("%s/data", sd_card_area); + if (!setup_sdcard(data_root)) { + syslog(LOG_ERR, "Failed to setup SD card."); + free(data_root); + return NULL; + } + return data_root; + } else { + return strdup( + "/var/lib/docker"); // Same location as if --data-root is omitted + } } /** - * @brief Start a new dockerd process. + * @brief Gets and verifies the UseTLS selection * - * @return True if successful, false otherwise + * @param use_tls_ret selection to be updated. + * @return True if successful, false otherwise. + */ +static gboolean +get_and_verify_tls_selection(bool *use_tls_ret) +{ + gboolean return_value = false; + char *ca_path = NULL; + char *cert_path = NULL; + char *key_path = NULL; + + const bool use_tls = is_parameter_yes("UseTLS"); + { + if (use_tls) { + char *ca_path = g_strdup_printf("%s%s", tls_cert_path, tls_certs[0]); + char *cert_path = g_strdup_printf("%s%s", tls_cert_path, tls_certs[1]); + char *key_path = g_strdup_printf("%s%s", tls_cert_path, tls_certs[2]); + + bool ca_exists = access(ca_path, F_OK) == 0; + bool cert_exists = access(cert_path, F_OK) == 0; + bool key_exists = access(key_path, F_OK) == 0; + + if (!ca_exists || !cert_exists || !key_exists) { + syslog(LOG_ERR, "One or more TLS certificates missing."); + } + + if (!ca_exists) { + syslog(LOG_ERR, + "Cannot start using TLS, no CA certificate found at %s", + ca_path); + } + if (!cert_exists) { + syslog(LOG_ERR, + "Cannot start using TLS, no server certificate found at %s", + cert_path); + } + if (!key_exists) { + syslog(LOG_ERR, + "Cannot start using TLS, no server key found at %s", + key_path); + } + + if (!ca_exists || !cert_exists || !key_exists) { + goto end; + } + } + *use_tls_ret = use_tls; + return_value = true; + } +end: + free(ca_path); + free(cert_path); + free(key_path); + return return_value; +} + +/** + * @brief Gets and verifies the TCPSocket selection + * + * @param use_tcp_socket_ret selection to be updated. + * @return True if successful, false otherwise. + */ +static gboolean +get_tcp_socket_selection(bool *use_tcp_socket_ret) +{ + *use_tcp_socket_ret = is_parameter_yes("TCPSocket"); + return true; +} + +/** + * @brief Gets and verifies the IPCSocket selection + * + * @param use_ipc_socket_ret selection to be updated. + * @return True if successful, false otherwise. */ +static gboolean +get_ipc_socket_selection(bool *use_ipc_socket_ret) +{ + *use_ipc_socket_ret = is_parameter_yes("IPCSocket"); + return true; +} + +static bool +read_settings(struct settings *settings, const struct app_state *app_state) +{ + if (!get_and_verify_tls_selection(&settings->use_tls)) { + syslog(LOG_ERR, "Failed to verify tls selection"); + return false; + } + if (!get_tcp_socket_selection(&settings->use_tcp_socket)) { + syslog(LOG_ERR, "Failed to get tcp socket selection"); + return false; + } + if (!get_ipc_socket_selection(&settings->use_ipc_socket)) { + syslog(LOG_ERR, "Failed to get ipc socket selection"); + return false; + } + if (!(settings->data_root = prepare_data_root(app_state->sd_card_area))) { + syslog(LOG_ERR, "Failed to set up dockerd data root"); + return false; + } + return true; +} + +// Return true if dockerd was successfully started. +// Log an error and return false if it failed to start properly. static bool -start_dockerd(void) +start_dockerd(const struct settings *settings, struct app_state *app_state) { + const char *data_root = settings->data_root; + const bool use_tls = settings->use_tls; + const bool use_tcp_socket = settings->use_tcp_socket; + const bool use_ipc_socket = settings->use_ipc_socket; + GError *error = NULL; bool return_value = false; @@ -246,44 +432,6 @@ start_dockerd(void) guint args_offset = 0; gchar **args_split = NULL; - // Read parameters - char *use_sd_card_value = get_parameter_value("SDCardSupport"); - char *use_tls_value = get_parameter_value("UseTLS"); - char *use_ipc_socket_value = get_parameter_value("IPCSocket"); - if (use_sd_card_value == NULL || use_tls_value == NULL || - use_ipc_socket_value == NULL) { - goto end; - } - bool use_sdcard = strcmp(use_sd_card_value, "yes") == 0; - bool use_tls = strcmp(use_tls_value, "yes") == 0; - bool use_ipc_socket = strcmp(use_ipc_socket_value, "yes") == 0; - - if (use_sdcard) { - // Confirm that the SD card is usable - char *sd_file_system = get_sd_filesystem(); - if (sd_file_system == NULL) { - syslog(LOG_ERR, - "Couldn't identify the file system of the SD card at %s", - sd_card_path); - goto end; - } - - if (strcmp(sd_file_system, "vfat") == 0 || - strcmp(sd_file_system, "exfat") == 0) { - syslog(LOG_ERR, - "The SD card at %s uses file system %s which does not support " - "Unix file permissions. Please reformat to a file system that " - "support Unix file permissions, such as ext4 or xfs.", - sd_card_path, - sd_file_system); - goto end; - } - - if (!setup_sdcard()) { - syslog(LOG_ERR, "Failed to setup SD card."); - goto end; - } - } args_offset += g_snprintf( args + args_offset, args_len - args_offset, @@ -294,84 +442,62 @@ start_dockerd(void) g_strlcpy(msg, "Starting dockerd", msg_len); - if (use_tls) { - const char *ca_path = - "/usr/local/packages/dockerdwrapperwithcompose/ca.pem"; - const char *cert_path = - "/usr/local/packages/dockerdwrapperwithcompose/server-cert.pem"; - const char *key_path = - "/usr/local/packages/dockerdwrapperwithcompose/server-key.pem"; - - bool ca_exists = access(ca_path, F_OK) == 0; - bool cert_exists = access(cert_path, F_OK) == 0; - bool key_exists = access(key_path, F_OK) == 0; - - if (!ca_exists) { - syslog(LOG_ERR, - "Cannot start using TLS, no CA certificate found at %s", - ca_path); - } - if (!cert_exists) { - syslog(LOG_ERR, - "Cannot start using TLS, no server certificate found at %s", - cert_path); - } - if (!key_exists) { - syslog(LOG_ERR, - "Cannot start using TLS, no server key found at %s", - key_path); - } - - if (!ca_exists || !cert_exists || !key_exists) { - goto end; - } + if (!use_ipc_socket && !use_tcp_socket) { + syslog(LOG_ERR, + "At least one of IPC socket or TCP socket must be set to \"yes\". " + "dockerd will not be started."); + goto end; + } + if (use_ipc_socket) { + g_strlcat(msg, " with IPC socket and", msg_len); args_offset += g_snprintf(args + args_offset, args_len - args_offset, - " %s %s %s %s %s %s %s %s", - "-H tcp://0.0.0.0:2376", - "--tlsverify", - "--tlscacert", - ca_path, - "--tlscert", - cert_path, - "--tlskey", - key_path); - - g_strlcat(msg, " in TLS mode", msg_len); - } else { - args_offset += g_snprintf(args + args_offset, - args_len - args_offset, - " %s %s", - "-H tcp://0.0.0.0:2375", - "--tls=false"); - - g_strlcat(msg, " in unsecured mode", msg_len); - } - - if (use_sdcard) { - args_offset += - g_snprintf(args + args_offset, - args_len - args_offset, - " %s", - "--data-root /var/spool/storage/SD_DISK/dockerd/data"); - - g_strlcat(msg, " using SD card as storage", msg_len); + " -H unix:///var/run/docker.sock"); } else { - g_strlcat(msg, " using internal storage", msg_len); + g_strlcat(msg, " without IPC socket and", msg_len); } - if (use_ipc_socket) { + if (use_tcp_socket) { + g_strlcat(msg, " with TCP socket", msg_len); + const uint port = use_tls ? 2376 : 2375; args_offset += g_snprintf(args + args_offset, args_len - args_offset, - " %s", - "-H unix:///var/run/docker.sock"); - - g_strlcat(msg, " with IPC socket.", msg_len); + " -H tcp://0.0.0.0:%d", + port); + if (use_tls) { + const char *ca_path = + "/usr/local/packages/dockerdwrapperwithcompose/ca.pem"; + const char *cert_path = + "/usr/local/packages/dockerdwrapperwithcompose/server-cert.pem"; + const char *key_path = + "/usr/local/packages/dockerdwrapperwithcompose/server-key.pem"; + args_offset += g_snprintf(args + args_offset, + args_len - args_offset, + " %s %s %s %s %s %s %s", + "--tlsverify", + "--tlscacert", + ca_path, + "--tlscert", + cert_path, + "--tlskey", + key_path); + g_strlcat(msg, " in TLS mode", msg_len); + } else { + args_offset += g_snprintf( + args + args_offset, args_len - args_offset, " --tls=false"); + g_strlcat(msg, " in unsecured mode", msg_len); + } } else { - g_strlcat(msg, " without IPC socket.", msg_len); + g_strlcat(msg, " without TCP socket", msg_len); } + g_autofree char *data_root_msg = + g_strdup_printf(" using %s as storage.", data_root); + g_strlcat(msg, data_root_msg, msg_len); + args_offset += g_snprintf( + args + args_offset, args_len - args_offset, " --data-root %s", data_root); + // Log startup information to syslog. syslog(LOG_INFO, "%s", msg); @@ -386,34 +512,42 @@ start_dockerd(void) &error); if (!result) { syslog(LOG_ERR, - "Could not execv the dockerd process. Return value: %d, error: %s", + "Starting dockerd failed: execv returned: %d, error: %s", result, error->message); goto end; } // Watch the child process. - g_child_watch_add(dockerd_process_pid, dockerd_process_exited_callback, NULL); + g_child_watch_add( + dockerd_process_pid, dockerd_process_exited_callback, app_state); if (!is_process_alive(dockerd_process_pid)) { - // The process died during adding of callback, tell loop to quit. - exit_code = -1; - g_main_loop_quit(loop); + syslog(LOG_ERR, + "Starting dockerd failed: Process died unexpectedly during startup"); + quit_program(EX_SOFTWARE); goto end; } - return_value = true; end: g_strfreev(args_split); - free(use_sd_card_value); - free(use_tls_value); - free(use_ipc_socket_value); g_clear_error(&error); - return return_value; } +static bool +read_settings_and_start_dockerd(struct app_state *app_state) +{ + struct settings settings = {0}; + + bool success = read_settings(&settings, app_state) && + start_dockerd(&settings, app_state); + + free(settings.data_root); + return success; +} + /** * @brief Stop the currently running dockerd process. * @@ -429,7 +563,7 @@ stop_dockerd(void) goto end; } - // Send SIGTERM to the process + syslog(LOG_INFO, "Sending SIGTERM to dockerd."); bool sigterm_successfully_sent = kill(dockerd_process_pid, SIGTERM) == 0; if (!sigterm_successfully_sent) { syslog( @@ -462,16 +596,15 @@ stop_dockerd(void) * @brief Callback called when the dockerd process exits. */ static void -dockerd_process_exited_callback(__attribute__((unused)) GPid pid, +dockerd_process_exited_callback(GPid pid, gint status, - __attribute__((unused)) gpointer user_data) + gpointer app_state_void_ptr) { + struct app_state *app_state = app_state_void_ptr; GError *error = NULL; - if (!g_spawn_check_wait_status(status, &error)) { + if (!g_spawn_check_exit_status(status, &error)) { syslog(LOG_ERR, "Dockerd process exited with error: %d", status); g_clear_error(&error); - - exit_code = -1; } dockerd_process_pid = -1; @@ -481,21 +614,11 @@ dockerd_process_exited_callback(__attribute__((unused)) GPid pid, // manner. Remove it manually. remove("/var/run/docker.pid"); - if (restart_dockerd) { - restart_dockerd = false; - if (!start_dockerd()) { - syslog(LOG_ERR, "Failed to restart dockerd, exiting."); - exit_code = -1; - g_main_loop_quit(loop); - } - } else { - // We shouldn't restart, stop instead. - g_main_loop_quit(loop); - } + g_main_loop_quit(loop); // Trigger a restart of dockerd from main() } /** - * @brief Callback function called when the SDCardSupport parameter + * @brief Callback function called when any of the parameters * changes. Will restart the dockerd process with the new setting. * * @param name Name of the updated parameter. @@ -507,27 +630,13 @@ parameter_changed_callback(const gchar *name, __attribute__((unused)) gpointer data) { const gchar *parname = name += strlen("root.dockerdwrapperwithcompose."); - // bool dockerd_started_correctly = false; - if (strcmp(parname, "SDCardSupport") == 0) { - syslog(LOG_INFO, "SDCardSupport changed to: %s", value); - restart_dockerd = true; - } else if (strcmp(parname, "UseTLS") == 0) { - syslog(LOG_INFO, "UseTLS changed to: %s", value); - restart_dockerd = true; - } else { - syslog(LOG_WARNING, "Parameter %s is not recognized", name); - restart_dockerd = false; - - // No known parameter was changed, do not restart. - return; - } - // Stop the currently running process. - if (!stop_dockerd()) { - syslog(LOG_ERR, - "Failed to stop dockerd process. Please restart the acap " - "manually."); - exit_code = -1; + for (size_t i = 0; i < sizeof(ax_parameters) / sizeof(ax_parameters[0]); + ++i) { + if (strcmp(parname, ax_parameters[i]) == 0) { + syslog(LOG_INFO, "%s changed to: %s", ax_parameters[i], value); + g_main_loop_quit(loop); // Trigger a restart of dockerd from main() + } } } @@ -543,34 +652,21 @@ setup_axparameter(void) goto end; } - gboolean geresult = ax_parameter_register_callback( - ax_parameter, - "root.dockerdwrapperwithcompose.SDCardSupport", - parameter_changed_callback, - NULL, - &error); - - if (geresult == FALSE) { - syslog(LOG_ERR, - "Could not register SDCardSupport callback. Error: %s", - error->message); - goto end; - } - - geresult = - ax_parameter_register_callback(ax_parameter, - "root.dockerdwrapperwithcompose.UseTLS", - parameter_changed_callback, - NULL, - &error); + for (size_t i = 0; i < sizeof(ax_parameters) / sizeof(ax_parameters[0]); + ++i) { + char *parameter_path = g_strdup_printf( + "%s.%s", "root.dockerdwrapperwithcompose", ax_parameters[i]); + gboolean geresult = ax_parameter_register_callback( + ax_parameter, parameter_path, parameter_changed_callback, NULL, &error); + free(parameter_path); - if (geresult == FALSE) { - syslog(LOG_ERR, - "Could not register UseTLS callback. Error: %s", - error->message); - ax_parameter_unregister_callback( - ax_parameter, "root.dockerdwrapperwithcompose.SDCardSupport"); - goto end; + if (geresult == FALSE) { + syslog(LOG_ERR, + "Could not register %s callback. Error: %s", + ax_parameters[i], + error->message); + goto end; + } } success = true; @@ -578,6 +674,7 @@ setup_axparameter(void) end: if (!success && ax_parameter != NULL) { ax_parameter_free(ax_parameter); + ax_parameter = NULL; } return ax_parameter; } @@ -585,52 +682,51 @@ setup_axparameter(void) int main(void) { - GError *error = NULL; + struct app_state app_state = {0}; + app_state.sd_card_area = strdup("/var/spool/storage/SD_DISK/dockerd"); + AXParameter *ax_parameter = NULL; - exit_code = 0; openlog(NULL, LOG_PID, LOG_USER); syslog(LOG_INFO, "Started logging."); + loop = g_main_loop_new(NULL, FALSE); + // Setup signal handling. init_signals(); // Setup ax_parameter ax_parameter = setup_axparameter(); if (ax_parameter == NULL) { - syslog(LOG_ERR, "Error in setup_axparameter: %s", error->message); - goto end; + syslog(LOG_ERR, "Error in setup_axparameter"); + quit_program(EX_SOFTWARE); } - /* Create the GLib event loop. */ - loop = g_main_loop_new(NULL, FALSE); - loop = g_main_loop_ref(loop); + while (application_exit_code == EX_KEEP_RUNNING) { + if (dockerd_process_pid == -1 && + !read_settings_and_start_dockerd(&app_state)) - if (!start_dockerd()) { - syslog(LOG_ERR, "Starting dockerd failed"); - exit_code = -1; - goto end; - } + quit_program(EX_SOFTWARE); - /* Run the GLib event loop. */ - g_main_loop_run(loop); - g_main_loop_unref(loop); + g_main_loop_run(loop); -end: - if (stop_dockerd()) { - syslog(LOG_INFO, "Shutting down. dockerd shut down successfully."); - } else { - syslog(LOG_WARNING, "Shutting down. Failed to shut down dockerd."); + if (!stop_dockerd()) + syslog(LOG_WARNING, "Failed to shut down dockerd."); } + g_main_loop_unref(loop); + if (ax_parameter != NULL) { - ax_parameter_unregister_callback( - ax_parameter, "root.dockerdwrapperwithcompose.SDCardSupport"); - ax_parameter_unregister_callback(ax_parameter, - "root.dockerdwrapperwithcompose.UseTLS"); + for (size_t i = 0; i < sizeof(ax_parameters) / sizeof(ax_parameters[0]); + ++i) { + char *parameter_path = g_strdup_printf( + "%s.%s", "root.dockerdwrapperwithcompose", ax_parameters[i]); + ax_parameter_unregister_callback(ax_parameter, parameter_path); + free(parameter_path); + } ax_parameter_free(ax_parameter); } - g_clear_error(&error); - return exit_code; + free(app_state.sd_card_area); + return application_exit_code; } diff --git a/app/manifest.json b/app/manifest.json index 490b458..3f7fadd 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,44 +1,49 @@ { - "schemaVersion": "1.3", - "acapPackageConf": { - "setup": { - "friendlyName": "Docker Daemon with Compose", - "appId": "414150", - "appName": "dockerdwrapperwithcompose", - "vendor": "Axis Communications", - "embeddedSdkVersion": "3.0", - "user": { - "group": "root", - "username": "root" - }, - "vendorUrl": "https://www.axis.com", - "runMode": "once", - "version": "1.3.0" + "schemaVersion": "1.3", + "acapPackageConf": { + "setup": { + "friendlyName": "Docker Daemon with Compose", + "appId": "414150", + "appName": "dockerdwrapperwithcompose", + "vendor": "Axis Communications", + "embeddedSdkVersion": "3.0", + "user": { + "group": "root", + "username": "root" + }, + "vendorUrl": "https://www.axis.com", + "runMode": "once", + "version": "1.3.2-LTS" + }, + "installation": { + "postInstallScript": "postinstallscript.sh" + }, + "uninstallation": { + "preUninstallScript": "preuninstallscript.sh" + }, + "configuration": { + "paramConfig": [ + { + "name": "SDCardSupport", + "default": "no", + "type": "enum:no|No, yes|Yes" }, - "installation": { - "postInstallScript": "postinstallscript.sh" + { + "name": "UseTLS", + "default": "yes", + "type": "enum:no|No, yes|Yes" }, - "uninstallation": { - "preUninstallScript": "preuninstallscript.sh" + { + "name": "TCPSocket", + "default": "yes", + "type": "enum:no|No, yes|Yes" }, - "configuration": { - "paramConfig": [ - { - "name": "SDCardSupport", - "default": "no", - "type": "enum:no|No, yes|Yes" - }, - { - "name": "UseTLS", - "default": "yes", - "type": "enum:no|No, yes|Yes" - }, - { - "name": "IPCSocket", - "default": "yes", - "type": "enum:no|No, yes|Yes" - } - ] + { + "name": "IPCSocket", + "default": "yes", + "type": "enum:no|No, yes|Yes" } + ] } + } } diff --git a/app/postinstallscript.sh b/app/postinstallscript.sh index 5544f7f..6143f46 100644 --- a/app/postinstallscript.sh +++ b/app/postinstallscript.sh @@ -1,15 +1,14 @@ #!/bin/sh # Move the daemon.json file into localdata folder -if [ ! -e localdata/daemon.json ] -then - mv empty_daemon.json localdata/daemon.json +if [ ! -e localdata/daemon.json ]; then + mv empty_daemon.json localdata/daemon.json else - rm empty_daemon.json + rm empty_daemon.json fi # Make sure containerd is started before dockerd and set PATH -cat >> /etc/systemd/system/sdkdockerdwrapperwithcompose.service << EOF +cat >>/etc/systemd/system/sdkdockerdwrapperwithcompose.service <