From 38b864f0b84bde81631c70e6db2305395ea7a6a0 Mon Sep 17 00:00:00 2001 From: Michele Mancioppi Date: Tue, 24 Sep 2024 18:09:35 +0200 Subject: [PATCH 1/8] feat(injector): add custom Dash0 LD_PRELOAD injector Add LD_PRELOAD injector binary to the Dash0 instrumentation image. This image works irrespective of the libc flavour used by the dynamically-linked host process (e.g., Node v8, CPython, JVM). Environment variables that the host process looks up through getenv are modified on the fly, to inject Dash0 OpenTelemetry distributions. For now, this is only implemented for Node.js via NODE_OPTIONS. The injector replicates a subset of internal libc APIs related to string comparison and manipulation. This code is adapted from musl libc, and the respective copyright notice is included. --- images/instrumentation/Dockerfile | 21 +- images/instrumentation/injector/bin/LICENSE | 202 ++++++++++++++++++ images/instrumentation/injector/bin/NOTICE | 41 ++++ .../injector/src/dash0_injector.c | 109 ++++++++++ .../injector/src/dash0_injector.exports.map | 6 + .../injector/test/node/Dockerfile | 13 ++ .../injector/test/node/base-images | 10 + .../package.json | 3 + .../server.js | 6 + images/instrumentation/injector/test/test.sh | 53 +++++ 10 files changed, 463 insertions(+), 1 deletion(-) create mode 100644 images/instrumentation/injector/bin/LICENSE create mode 100644 images/instrumentation/injector/bin/NOTICE create mode 100644 images/instrumentation/injector/src/dash0_injector.c create mode 100644 images/instrumentation/injector/src/dash0_injector.exports.map create mode 100644 images/instrumentation/injector/test/node/Dockerfile create mode 100644 images/instrumentation/injector/test/node/base-images create mode 100644 images/instrumentation/injector/test/node/test-cases/node-options-includes-dash0-require/package.json create mode 100644 images/instrumentation/injector/test/node/test-cases/node-options-includes-dash0-require/server.js create mode 100755 images/instrumentation/injector/test/test.sh diff --git a/images/instrumentation/Dockerfile b/images/instrumentation/Dockerfile index 1515a8d0..18171b61 100644 --- a/images/instrumentation/Dockerfile +++ b/images/instrumentation/Dockerfile @@ -1,6 +1,24 @@ +# build injector +FROM ubuntu:24.04 AS build-injector + +RUN apt-get update && \ + apt-get install --no-install-recommends build-essential -y && \ + apt-get autoremove -y && \ + apt-get clean -y + +COPY ./injector /dash0-init-container/injector +WORKDIR /dash0-init-container/injector +RUN gcc \ + -shared \ + -nostdlib \ + -fPIC \ + -Wl,--version-script=src/dash0_injector.exports.map \ + src/dash0_injector.c \ + -o bin/dash0_injector.so + # build Node.js artifacts FROM node:20.13.1-alpine3.19 AS build-node.js -RUN mkdir -p /dash0-init-container/instrumentation/node.js +RUN mkdir -p /instrumentation/node.js WORKDIR /dash0-init-container/instrumentation/node.js COPY node.js/package* ./ COPY node.js/dash0hq-opentelemetry-*.tgz . @@ -17,6 +35,7 @@ COPY copy-instrumentation.sh / # copy node.js artifacts RUN mkdir -p /dash0-init-container/instrumentation +COPY --from=build-injector /dash0-init-container/injector/bin /dash0-init-container/injector COPY --from=build-node.js /dash0-init-container/instrumentation/node.js /dash0-init-container/instrumentation/node.js WORKDIR / diff --git a/images/instrumentation/injector/bin/LICENSE b/images/instrumentation/injector/bin/LICENSE new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/images/instrumentation/injector/bin/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/images/instrumentation/injector/bin/NOTICE b/images/instrumentation/injector/bin/NOTICE new file mode 100644 index 00000000..350e7a69 --- /dev/null +++ b/images/instrumentation/injector/bin/NOTICE @@ -0,0 +1,41 @@ +Copyright 2024 Dash0 Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + + + +This codebase contains code from musl libc (https://musl.libc.org) +subject to the following license terms: + +---------------------------------------------------------------------- +Copyright © 2005-2014 Rich Felker, et al. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/images/instrumentation/injector/src/dash0_injector.c b/images/instrumentation/injector/src/dash0_injector.c new file mode 100644 index 00000000..23f4359a --- /dev/null +++ b/images/instrumentation/injector/src/dash0_injector.c @@ -0,0 +1,109 @@ +#include +#include + +#define ALIGN (sizeof(size_t)) +#define UCHAR_MAX 255 +#define ONES ((size_t)-1/UCHAR_MAX) +#define HIGHS (ONES * (UCHAR_MAX/2+1)) +#define HASZERO(x) ((x)-ONES & ~(x) & HIGHS) + +#define NODE_OPTIONS_ENV_VAR_NAME "NODE_OPTIONS" +#define NODE_OPTIONS_DASH0_REQUIRE "--require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry" + +extern char **__environ; + +size_t __strlen(const char *s) +{ + const char *a = s; + const size_t *w; + for (; (uintptr_t)s % ALIGN; s++) if (!*s) return s-a; + for (w = (const void *)s; !HASZERO(*w); w++); + for (s = (const void *)w; *s; s++); + return s-a; +} + +char *__strchrnul(const char *s, int c) +{ + size_t *w, k; + + c = (unsigned char)c; + if (!c) return (char *)s + __strlen(s); + + for (; (uintptr_t)s % ALIGN; s++) + if (!*s || *(unsigned char *)s == c) return (char *)s; + k = ONES * c; + for (w = (void *)s; !HASZERO(*w) && !HASZERO(*w^k); w++); + for (s = (void *)w; *s && *(unsigned char *)s != c; s++); + return (char *)s; +} + +char *__strcpy(char *restrict dest, const char *restrict src) +{ + const unsigned char *s = src; + unsigned char *d = dest; + while ((*d++ = *s++)); + return dest; +} + +char *__strcat(char *restrict dest, const char *restrict src) +{ + __strcpy(dest + __strlen(dest), src); + return dest; +} + +int __strcmp(const char *l, const char *r) +{ + for (; *l==*r && *l; l++, r++); + return *(unsigned char *)l - *(unsigned char *)r; +} + +int __strncmp(const char *_l, const char *_r, size_t n) +{ + const unsigned char *l=(void *)_l, *r=(void *)_r; + if (!n--) return 0; + for (; *l && *r && n && *l == *r ; l++, r++, n--); + return *l - *r; +} + +char *__getenv(const char *name) +{ + size_t l = __strchrnul(name, '=') - name; + if (l && !name[l] && __environ) + for (char **e = __environ; *e; e++) + if (!__strncmp(name, *e, l) && l[*e] == '=') + return *e + l+1; + return 0; +} + +/* + * Buffers of statically-allocated memory that we can use to safely return to + * the program manipulated values of env vars without dynamic allocations. + */ +char val1[1012]; +char val2[1012]; + +char *getenv(const char *name) +{ + char *origValue = __getenv(name); + int l = __strlen(name); + + char *nodeOptionsVarName = NODE_OPTIONS_ENV_VAR_NAME; + if (__strcmp(name, nodeOptionsVarName) == 0) + { + if (__strlen(val1) == 0) + { + if (origValue != NULL) + { + __strcat(val1, origValue); + __strcat(val1, " "); + } + + char *nodeOptionsDash0Require = NODE_OPTIONS_DASH0_REQUIRE; + __strcat(val1, nodeOptionsDash0Require); + } + + return val1; + } + + return origValue; +} \ No newline at end of file diff --git a/images/instrumentation/injector/src/dash0_injector.exports.map b/images/instrumentation/injector/src/dash0_injector.exports.map new file mode 100644 index 00000000..adb980b7 --- /dev/null +++ b/images/instrumentation/injector/src/dash0_injector.exports.map @@ -0,0 +1,6 @@ +{ + global: + getenv; + local: + *; +}; \ No newline at end of file diff --git a/images/instrumentation/injector/test/node/Dockerfile b/images/instrumentation/injector/test/node/Dockerfile new file mode 100644 index 00000000..cb58eaf6 --- /dev/null +++ b/images/instrumentation/injector/test/node/Dockerfile @@ -0,0 +1,13 @@ +ARG base_image +ARG instr_image + +FROM ${instr_image} AS instr + +FROM ${base_image} + +# TODO Normalize node.js path + +COPY ./test-cases /test-cases + +COPY --from=instr /dash0-init-container /__dash0__ +ENV LD_PRELOAD=/__dash0__/injector/dash0_injector.so \ No newline at end of file diff --git a/images/instrumentation/injector/test/node/base-images b/images/instrumentation/injector/test/node/base-images new file mode 100644 index 00000000..69bde4ef --- /dev/null +++ b/images/instrumentation/injector/test/node/base-images @@ -0,0 +1,10 @@ +# One base image per line +node:22-alpine +node:22-bookworm +node:22-bullseye +node:20-alpine +node:20-bookworm +node:20-bullseye +node:18-alpine +node:18-bookworm +node:18-bullseye \ No newline at end of file diff --git a/images/instrumentation/injector/test/node/test-cases/node-options-includes-dash0-require/package.json b/images/instrumentation/injector/test/node/test-cases/node-options-includes-dash0-require/package.json new file mode 100644 index 00000000..e83de75e --- /dev/null +++ b/images/instrumentation/injector/test/node/test-cases/node-options-includes-dash0-require/package.json @@ -0,0 +1,3 @@ +{ + "name": "@dash0/injector_tests_node-options-includes-dash0-require" +} \ No newline at end of file diff --git a/images/instrumentation/injector/test/node/test-cases/node-options-includes-dash0-require/server.js b/images/instrumentation/injector/test/node/test-cases/node-options-includes-dash0-require/server.js new file mode 100644 index 00000000..e8957954 --- /dev/null +++ b/images/instrumentation/injector/test/node/test-cases/node-options-includes-dash0-require/server.js @@ -0,0 +1,6 @@ +const nodeOptions = process.env["NODE_OPTIONS"] || ""; + +if (!nodeOptions.includes("--require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry")) { + console.error(`No Dash0 distro require found in NODE_OPTIONS value: '${nodeOptions}'`) + process.exit(1); +} \ No newline at end of file diff --git a/images/instrumentation/injector/test/test.sh b/images/instrumentation/injector/test/test.sh new file mode 100755 index 00000000..d2e6cf05 --- /dev/null +++ b/images/instrumentation/injector/test/test.sh @@ -0,0 +1,53 @@ +#/bin/env bash + +readonly script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +function run_tests () { + local runtime="${1}" + local docker_image="${2}" + + for t in ${script_dir}/${runtime}/test-cases/*/ ; do + test=$(basename $(realpath "${t}")) + printf '%s' "Test '${test}' " | sed 's/^/ /' # `echo -n` does not work on Mac OS + tempfile=$(mktemp) + docker run ${docker_image} "npm" "start" "--prefix" "/test-cases/${test}" > ${tempfile} 2>&1 + if [ $? -eq 0 ]; then + echo OK + else + echo FAIL + echo 'Output (stdout + stderr):' + cat ${tempfile} | sed 's/^/ /' + fi + rm ${tempfile} + done +} + +# Build instr image +instr_image="${1}" + +if [ -z "${instr_image}" ]; then + instr_image="dash0-instrumentation:test-latest" + echo "Building instrumentation image '${instr_image}' (no instrumentation image tag provided in input)" + + tempfile=$(mktemp) + if ! docker build ../.. -t "${instr_image}" > ${tempfile} 2>&1; then + echo "Failed to build the instrumentation image:" + cat ${tempfile} | sed 's/^/ /' + fi + rm ${tempfile} +fi + +# Run tests +for r in ${script_dir}/*/ ; do + runtime=$(basename $(realpath "${r}")) + echo "Runtime '${runtime}'" + grep '^[^#;]' "${script_dir}/${runtime}/base-images" | while read -r base_image ; do + echo "Base image '${base_image}'" | sed 's/^/ /' + build_output=$(docker build "${script_dir}/${runtime}" --build-arg "instr_image=${instr_image}" --build-arg "base_image=${base_image}" -t "test-${runtime}:latest" 2>&1 | sed 's/^/ /') + if [ $? -ne 0 ]; then + echo "${build_output}" + exit 1 + fi + run_tests "${runtime}" "test-${runtime}:latest" | sed 's/^/ /' + done +done \ No newline at end of file From 9dbace9c00686f8228baa68b7f5d393ecd92c776 Mon Sep 17 00:00:00 2001 From: Bastian Krol Date: Mon, 21 Oct 2024 08:54:05 +0200 Subject: [PATCH 2/8] test(injector): add isolated injector tests --- .github/workflows/ci.yaml | 2 +- .../instrumentation/injector/test/README.md | 2 + .../injector/test/app/index.js | 43 ++++++++++ .../injector/test/bin/.gitignore | 1 + .../injector/test/docker/.gitignore | 1 + .../injector/test/docker/Dockerfile-test | 22 ++++++ .../injector/test/docker/noop.js | 3 + .../test/scripts/build-in-container.sh | 49 ++++++++++++ .../test/scripts/run-tests-for-container.sh | 78 ++++++++++++++++++ .../scripts/run-tests-within-container.sh | 79 +++++++++++++++++++ .../injector/test/scripts/test-all.sh | 77 ++++++++++++++++++ images/instrumentation/test/README.md | 4 + .../{injector => }/test/node/Dockerfile | 0 .../{injector => }/test/node/base-images | 0 .../package.json | 0 .../server.js | 0 .../{injector => }/test/test.sh | 0 17 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 images/instrumentation/injector/test/README.md create mode 100644 images/instrumentation/injector/test/app/index.js create mode 100644 images/instrumentation/injector/test/bin/.gitignore create mode 100644 images/instrumentation/injector/test/docker/.gitignore create mode 100644 images/instrumentation/injector/test/docker/Dockerfile-test create mode 100644 images/instrumentation/injector/test/docker/noop.js create mode 100755 images/instrumentation/injector/test/scripts/build-in-container.sh create mode 100755 images/instrumentation/injector/test/scripts/run-tests-for-container.sh create mode 100755 images/instrumentation/injector/test/scripts/run-tests-within-container.sh create mode 100755 images/instrumentation/injector/test/scripts/test-all.sh create mode 100644 images/instrumentation/test/README.md rename images/instrumentation/{injector => }/test/node/Dockerfile (100%) rename images/instrumentation/{injector => }/test/node/base-images (100%) rename images/instrumentation/{injector => }/test/node/test-cases/node-options-includes-dash0-require/package.json (100%) rename images/instrumentation/{injector => }/test/node/test-cases/node-options-includes-dash0-require/server.js (100%) rename images/instrumentation/{injector => }/test/test.sh (100%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b4d86c15..d435a37c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -55,7 +55,7 @@ jobs: run: | helm plugin install https://github.com/helm-unittest/helm-unittest.git - - name: test + - name: run operator and Helm chart unit tests run: | make test diff --git a/images/instrumentation/injector/test/README.md b/images/instrumentation/injector/test/README.md new file mode 100644 index 00000000..52e16eba --- /dev/null +++ b/images/instrumentation/injector/test/README.md @@ -0,0 +1,2 @@ +This directory contains isolated tests for the injector code. +The difference to images/instrumentation/test is that the latter tests the whole instrumentation image. diff --git a/images/instrumentation/injector/test/app/index.js b/images/instrumentation/injector/test/app/index.js new file mode 100644 index 00000000..49f22008 --- /dev/null +++ b/images/instrumentation/injector/test/app/index.js @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + +const process = require('node:process'); + +function echoEnvVar(envVarName) { + const envVarValue = process.env[envVarName]; + if (!envVarValue) { + process.stdout.write(`${envVarName}: -`); + } else { + process.stdout.write(`${envVarName}: ${envVarValue}`); + } +} + +function main () { + const testCase = process.argv[2]; + if (!testCase) { + console.error("error: not enough arguments, the name of the test case needs to be specifed"); + process.exit(1) + } + + switch (testCase) { + case "non-existing": + echoEnvVar("DOES_NOT_EXIST"); + break; + case "term": + echoEnvVar("TERM"); + break; + case "node_options": + echoEnvVar("NODE_OPTIONS"); + break; + case "node_options_twice": + echoEnvVar("NODE_OPTIONS"); + process.stdout.write("; ") + echoEnvVar("NODE_OPTIONS"); + break; + default: + console.error(`unknown test case: ${testCase}`); + process.exit(1) + } +} + +main(); diff --git a/images/instrumentation/injector/test/bin/.gitignore b/images/instrumentation/injector/test/bin/.gitignore new file mode 100644 index 00000000..140f8cf8 --- /dev/null +++ b/images/instrumentation/injector/test/bin/.gitignore @@ -0,0 +1 @@ +*.so diff --git a/images/instrumentation/injector/test/docker/.gitignore b/images/instrumentation/injector/test/docker/.gitignore new file mode 100644 index 00000000..234ed3e4 --- /dev/null +++ b/images/instrumentation/injector/test/docker/.gitignore @@ -0,0 +1 @@ +Dockerfile-build diff --git a/images/instrumentation/injector/test/docker/Dockerfile-test b/images/instrumentation/injector/test/docker/Dockerfile-test new file mode 100644 index 00000000..1550ea3a --- /dev/null +++ b/images/instrumentation/injector/test/docker/Dockerfile-test @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + +ARG base_image="node:20.18-bookworm" +FROM ${base_image} + +ARG injector_binary=no_value +# fail build immediately if build arg injector_binary is missing +RUN test ${injector_binary} != "no_value" + +WORKDIR /usr/src/dash0/injector/ + +COPY app app +COPY scripts/run-tests-within-container.sh scripts/ + +RUN echo ${injector_binary} + +COPY bin/${injector_binary} /dash0-init-container/injector/dash0_injector.so + +COPY docker/noop.js /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry/index.js + +CMD ["scripts/run-tests-within-container.sh"] diff --git a/images/instrumentation/injector/test/docker/noop.js b/images/instrumentation/injector/test/docker/noop.js new file mode 100644 index 00000000..1fd0cd7e --- /dev/null +++ b/images/instrumentation/injector/test/docker/noop.js @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + diff --git a/images/instrumentation/injector/test/scripts/build-in-container.sh b/images/instrumentation/injector/test/scripts/build-in-container.sh new file mode 100755 index 00000000..c8725a30 --- /dev/null +++ b/images/instrumentation/injector/test/scripts/build-in-container.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +cd "$(dirname "$0")"/../../.. + +if [ -z "${ARCH:-}" ]; then + ARCH=arm64 +fi +if [ "$ARCH" = arm64 ]; then + docker_platform=linux/arm64 +elif [ "$ARCH" = x86_64 ]; then + docker_platform=linux/amd64 +else + echo "The architecture $ARCH is not supported." + exit 1 +fi + +dockerfile_name=injector/test/docker/Dockerfile-build +image_name=dash0-injector-builder-$ARCH +container_name=$image_name + +docker_run_extra_arguments="" +if [ "${INTERACTIVE:-}" = "true" ]; then + docker_run_extra_arguments=/bin/bash +fi + +echo +echo +echo ">>> Building the library on $ARCH <<<" + +docker rmi -f "$image_name" 2> /dev/null +docker rm -f "$container_name" 2> /dev/null + +docker build \ + --platform "$docker_platform" \ + . \ + -f "$dockerfile_name" \ + -t "$image_name" + +container_id=$(docker create "$image_name") +docker container cp \ + $container_id:/dash0-init-container/injector/bin/dash0_injector.so \ + injector/test/bin/dash0_injector_$ARCH.so +docker rm -v $container_id + diff --git a/images/instrumentation/injector/test/scripts/run-tests-for-container.sh b/images/instrumentation/injector/test/scripts/run-tests-for-container.sh new file mode 100755 index 00000000..e5e07b6d --- /dev/null +++ b/images/instrumentation/injector/test/scripts/run-tests-for-container.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +cd "$(dirname "$0")"/.. + +if [ -z "${ARCH:-}" ]; then + ARCH=arm64 +fi +if [ "$ARCH" = arm64 ]; then + docker_platform=linux/arm64 + expected_cpu_architecture=aarch64 + injector_binary=dash0_injector_arm64.so +elif [ "$ARCH" = x86_64 ]; then + docker_platform=linux/amd64 + expected_cpu_architecture=x86_64 + injector_binary=dash0_injector_x86_64.so +else + echo "The architecture $ARCH is not supported." + exit 1 +fi + +if [ -z "${LIBC:-}" ]; then + LIBC=glibc +fi + +base_image=node:20.18-bookworm +if [[ "$LIBC" == "musl" ]]; then + base_image=node:20.18-alpine3.20 +fi + +dockerfile_name="docker/Dockerfile-test" +image_name=dash0-injector-test-$ARCH-$LIBC +container_name=$image_name + +docker_run_extra_arguments="" +if [ "${INTERACTIVE:-}" = "true" ]; then + if [ "$LIBC" = glibc ]; then + docker_run_extra_arguments=/bin/bash + elif [ "$LIBC" = musl ]; then + docker_run_extra_arguments=/bin/sh + else + echo "The libc flavor $LIBC is not supported." + exit 1 + fi +fi + +echo +echo --------------------------------------- +echo "testing the library on $ARCH and $LIBC" +echo --------------------------------------- + +set -x + +docker rm -f "$container_name" 2> /dev/null +docker rmi -f "$image_name" 2> /dev/null + +docker build \ + --platform "$docker_platform" \ + --build-arg "base_image=${base_image}" \ + --build-arg "injector_binary=${injector_binary}" \ + . \ + -f "$dockerfile_name" \ + -t "$image_name" + +docker run \ + --platform "$docker_platform" \ + --env EXPECTED_CPU_ARCHITECTURE="$expected_cpu_architecture" \ + --name "$container_name" \ + -it \ + "$image_name" \ + $docker_run_extra_arguments + +set +x + diff --git a/images/instrumentation/injector/test/scripts/run-tests-within-container.sh b/images/instrumentation/injector/test/scripts/run-tests-within-container.sh new file mode 100755 index 00000000..22dfb6a1 --- /dev/null +++ b/images/instrumentation/injector/test/scripts/run-tests-within-container.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +if [ -z "${EXPECTED_CPU_ARCHITECTURE:-}" ]; then + echo "EXPECTED_CPU_ARCHITECTURE is not set for $0." + exit 1 +fi + +arch=$(uname -m) +arch_exit_code=$? +if [ $arch_exit_code != 0 ]; then + printf "${RED}verifying CPU architecture failed:${NC}\n" + echo "exit code: $arch_exit_code" + echo "output: $arch" + exit 1 +elif [ "$arch" != "$EXPECTED_CPU_ARCHITECTURE" ]; then + printf "${RED}verifying CPU architecture failed:${NC}\n" + echo "expected: $EXPECTED_CPU_ARCHITECTURE" + echo "actual: $arch" + exit 1 +else + printf "${GREEN}verifying CPU architecture %s successful${NC}\n" "$EXPECTED_CPU_ARCHITECTURE" +fi + +injector_binary=/dash0-init-container/injector/dash0_injector.so +if [ ! -f $injector_binary ]; then + printf "${RED}error: $injector_binary does not exist, not running any tests.${NC}\n" + exit 1 +fi + +run_test_case() { + test_case=$1 + command=$2 + expected=$3 + existing_node_options_value=${4:-} + set +e + if [ "$existing_node_options_value" != "" ]; then + test_output=$(LD_PRELOAD="$injector_binary" NODE_OPTIONS="$existing_node_options_value" node index.js "$command") + else + test_output=$(LD_PRELOAD="$injector_binary" node index.js "$command") + fi + test_exit_code=$? + set -e + if [ $test_exit_code != 0 ]; then + printf "${RED}test \"%s\" crashed:${NC}\n" "$test_case" + echo "received exit code: $test_exit_code" + echo "output: $test_output" + exit_code=1 + elif [ "$test_output" != "$expected" ]; then + printf "${RED}test \"%s\" failed:${NC}\n" "$test_case" + echo "expected: $expected" + echo "actual: $test_output" + exit_code=1 + else + printf "${GREEN}test \"%s\" successful${NC}\n" "$test_case" + fi +} + +exit_code=0 + +cd app + +run_test_case "getenv: returns undefined for non-existing environment variable" non-existing "DOES_NOT_EXIST: -" +run_test_case "getenv: returns environment variable unchanged" term "TERM: xterm" +run_test_case "getenv: overrides NODE_OPTIONS if it is not present" node_options "NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry" +run_test_case "getenv: ask for NODE_OPTIONS (unset) twice" node_options_twice "NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry; NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry" +run_test_case "getenv: prepends to NODE_OPTIONS if it is present" node_options "NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry --existing-node-options" "--existing-node-options" +run_test_case "getenv: ask for NODE_OPTIONS (set) twice" node_options_twice "NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry --existing-node-options; NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry --existing-node-options" "--existing-node-options" + +exit $exit_code + diff --git a/images/instrumentation/injector/test/scripts/test-all.sh b/images/instrumentation/injector/test/scripts/test-all.sh new file mode 100755 index 00000000..4a1f6136 --- /dev/null +++ b/images/instrumentation/injector/test/scripts/test-all.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +cd "$(dirname "$0")"/../../.. + +# remove all outdated injector binaries +rm -rf injector/test/bin/* + +# Create a Dockerfile for building the injector in a container by re-using an excerpt from the ../Dockerfile. This makes +# sure we keep the way we build the injector binary here for this test suite and for actual production usage in the +# instrumentation image in sync. +copy_lines=false +rm -f injector/test/docker/Dockerfile-build +while IFS= read -r line; do + if [[ "$line" =~ ^[[:space:]]*#.* ]]; then + # skip comments + continue + fi + if [[ $copy_lines == true && "$line" =~ ^FROM[[:space:]]+.*[[:space:]]+AS[[:space:]]+build-node.js$ ]]; then + copy_lines=false + fi + if [[ "$line" =~ ^FROM[[:space:]]+.*[[:space:]]+AS[[:space:]]+build-injector$ ]]; then + copy_lines=true + fi + if [[ $copy_lines == true ]]; then + echo $line >> injector/test/docker/Dockerfile-build + fi +done < Dockerfile + +if [[ ! -e injector/test/docker/Dockerfile-build ]]; then + echo "\nError: The file injector/test/docker/Dockerfile-build has not been generated, stopping." + exit 1 +fi + +# build injector binary for both architectures +ARCH=arm64 injector/test/scripts/build-in-container.sh +ARCH=x86_64 injector/test/scripts/build-in-container.sh + +exit_code=0 +summary="" +run_tests_for_architecture_and_libc_flavor() { + arch=$1 + libc=$2 + set +e + ARCH=$arch LIBC=$libc injector/test/scripts/run-tests-for-container.sh + test_exit_code=$? + set -e + echo + echo --------------------------------------- + if [ $test_exit_code != 0 ]; then + printf "${RED}tests for %s/%s failed (see above for details)${NC}\n" "$arch" "$libc" + exit_code=1 + summary="$summary\n$arch/$libc:\tfailed" + else + printf "${GREEN}tests for %s/%s were successful${NC}\n" "$arch" "$libc" + summary="$summary\n$arch/$libc:\tok" + fi + echo --------------------------------------- + echo +} + +run_tests_for_architecture_and_libc_flavor arm64 glibc +run_tests_for_architecture_and_libc_flavor x86_64 glibc +run_tests_for_architecture_and_libc_flavor arm64 musl +run_tests_for_architecture_and_libc_flavor x86_64 musl + +echo "$summary" +exit $exit_code + diff --git a/images/instrumentation/test/README.md b/images/instrumentation/test/README.md new file mode 100644 index 00000000..b272b5b7 --- /dev/null +++ b/images/instrumentation/test/README.md @@ -0,0 +1,4 @@ +This directory contains tests for the instrumentation image. +The difference to images/instrumentation/injector/test is that the latter tests the ld-preload injector in isolation +while the tests in this directory test the complete instrumentation image. +The tests in this directory also cover a wider variety of base images. diff --git a/images/instrumentation/injector/test/node/Dockerfile b/images/instrumentation/test/node/Dockerfile similarity index 100% rename from images/instrumentation/injector/test/node/Dockerfile rename to images/instrumentation/test/node/Dockerfile diff --git a/images/instrumentation/injector/test/node/base-images b/images/instrumentation/test/node/base-images similarity index 100% rename from images/instrumentation/injector/test/node/base-images rename to images/instrumentation/test/node/base-images diff --git a/images/instrumentation/injector/test/node/test-cases/node-options-includes-dash0-require/package.json b/images/instrumentation/test/node/test-cases/node-options-includes-dash0-require/package.json similarity index 100% rename from images/instrumentation/injector/test/node/test-cases/node-options-includes-dash0-require/package.json rename to images/instrumentation/test/node/test-cases/node-options-includes-dash0-require/package.json diff --git a/images/instrumentation/injector/test/node/test-cases/node-options-includes-dash0-require/server.js b/images/instrumentation/test/node/test-cases/node-options-includes-dash0-require/server.js similarity index 100% rename from images/instrumentation/injector/test/node/test-cases/node-options-includes-dash0-require/server.js rename to images/instrumentation/test/node/test-cases/node-options-includes-dash0-require/server.js diff --git a/images/instrumentation/injector/test/test.sh b/images/instrumentation/test/test.sh similarity index 100% rename from images/instrumentation/injector/test/test.sh rename to images/instrumentation/test/test.sh From eb77b7985f3294b9d4d53feb1cc64b1ecdfe312e Mon Sep 17 00:00:00 2001 From: Bastian Krol Date: Mon, 21 Oct 2024 10:16:49 +0200 Subject: [PATCH 3/8] fix(injector): prepend original NODE_OPTIONS value instead of appending --- .../instrumentation/injector/src/dash0_injector.c | 13 ++++++++----- .../test/scripts/run-tests-within-container.sh | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/images/instrumentation/injector/src/dash0_injector.c b/images/instrumentation/injector/src/dash0_injector.c index 23f4359a..b63ec67d 100644 --- a/images/instrumentation/injector/src/dash0_injector.c +++ b/images/instrumentation/injector/src/dash0_injector.c @@ -92,18 +92,21 @@ char *getenv(const char *name) { if (__strlen(val1) == 0) { + // Prepend our --require as the first item to the NODE_OPTIONS string. + char *nodeOptionsDash0Require = NODE_OPTIONS_DASH0_REQUIRE; + __strcat(val1, nodeOptionsDash0Require); + if (origValue != NULL) { - __strcat(val1, origValue); + // If NODE_OPTIONS were present, append the existing NODE_OPTIONS after our --require. __strcat(val1, " "); + __strcat(val1, origValue); } - - char *nodeOptionsDash0Require = NODE_OPTIONS_DASH0_REQUIRE; - __strcat(val1, nodeOptionsDash0Require); } return val1; } return origValue; -} \ No newline at end of file +} + diff --git a/images/instrumentation/injector/test/scripts/run-tests-within-container.sh b/images/instrumentation/injector/test/scripts/run-tests-within-container.sh index 22dfb6a1..2ae3d0dd 100755 --- a/images/instrumentation/injector/test/scripts/run-tests-within-container.sh +++ b/images/instrumentation/injector/test/scripts/run-tests-within-container.sh @@ -72,8 +72,8 @@ run_test_case "getenv: returns undefined for non-existing environment variable" run_test_case "getenv: returns environment variable unchanged" term "TERM: xterm" run_test_case "getenv: overrides NODE_OPTIONS if it is not present" node_options "NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry" run_test_case "getenv: ask for NODE_OPTIONS (unset) twice" node_options_twice "NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry; NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry" -run_test_case "getenv: prepends to NODE_OPTIONS if it is present" node_options "NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry --existing-node-options" "--existing-node-options" -run_test_case "getenv: ask for NODE_OPTIONS (set) twice" node_options_twice "NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry --existing-node-options; NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry --existing-node-options" "--existing-node-options" +run_test_case "getenv: prepends to NODE_OPTIONS if it is present" node_options "NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry --no-deprecation" "--no-deprecation" +run_test_case "getenv: ask for NODE_OPTIONS (set) twice" node_options_twice "NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry --no-deprecation; NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry --no-deprecation" "--no-deprecation" exit $exit_code From 4e4c544a81aacfbaf4255210d2079eeefd12f2be Mon Sep 17 00:00:00 2001 From: Bastian Krol Date: Mon, 21 Oct 2024 14:26:54 +0200 Subject: [PATCH 4/8] chore(injector): use LD_PRELOAD injector when modifying workloads --- images/instrumentation/Dockerfile | 4 +- .../instrumentation/copy-instrumentation.sh | 10 +++++ .../webhooks/instrumentation_webhook_test.go | 12 +++--- internal/workloads/workload_modifier.go | 41 +++++++++---------- internal/workloads/workload_modifier_test.go | 16 ++++---- test/util/resources.go | 20 ++++----- test/util/verification.go | 20 ++++----- 7 files changed, 65 insertions(+), 58 deletions(-) diff --git a/images/instrumentation/Dockerfile b/images/instrumentation/Dockerfile index 18171b61..84227c27 100644 --- a/images/instrumentation/Dockerfile +++ b/images/instrumentation/Dockerfile @@ -33,9 +33,9 @@ RUN NPM_CONFIG_UPDATE_NOTIFIER=false \ FROM alpine:3.19.1 COPY copy-instrumentation.sh / -# copy node.js artifacts +# copy artifacts (distros, injector binary) from the build stages to the final image RUN mkdir -p /dash0-init-container/instrumentation -COPY --from=build-injector /dash0-init-container/injector/bin /dash0-init-container/injector +COPY --from=build-injector /dash0-init-container/injector/bin/dash0_injector.so /dash0-init-container/dash0_injector.so COPY --from=build-node.js /dash0-init-container/instrumentation/node.js /dash0-init-container/instrumentation/node.js WORKDIR / diff --git a/images/instrumentation/copy-instrumentation.sh b/images/instrumentation/copy-instrumentation.sh index 89560f12..e376ba87 100755 --- a/images/instrumentation/copy-instrumentation.sh +++ b/images/instrumentation/copy-instrumentation.sh @@ -8,6 +8,13 @@ fi cd -P -- "$(dirname -- "$0")" +if [ -z "${DASH0_INJECTOR_FOLDER_SOURCE:-}" ]; then + DASH0_INJECTOR_FOLDER_SOURCE=/dash0-init-container +fi +if [ ! -d "${DASH0_INJECTOR_FOLDER_SOURCE}" ]; then + >&2 echo "[Dash0] injector source directory ${DASH0_INJECTOR_FOLDER_SOURCE} does not exist." + exit 1 +fi if [ -z "${DASH0_INSTRUMENTATION_FOLDER_SOURCE:-}" ]; then DASH0_INSTRUMENTATION_FOLDER_SOURCE=/dash0-init-container/instrumentation fi @@ -24,6 +31,9 @@ fi # be an existing mount point provided externally. cp -R "${DASH0_INSTRUMENTATION_FOLDER_SOURCE}"/ "${DASH0_INSTRUMENTATION_FOLDER_DESTINATION}" +# Copy the injector from the init container to the monitored container. +cp "${DASH0_INJECTOR_FOLDER_SOURCE}"/*.so "${DASH0_INSTRUMENTATION_FOLDER_DESTINATION}"/ + if [ -n "${DASH0_DEBUG:-}" ]; then find "${DASH0_INSTRUMENTATION_FOLDER_DESTINATION}" fi diff --git a/internal/webhooks/instrumentation_webhook_test.go b/internal/webhooks/instrumentation_webhook_test.go index 0bb90694..44070a7e 100644 --- a/internal/webhooks/instrumentation_webhook_test.go +++ b/internal/webhooks/instrumentation_webhook_test.go @@ -197,7 +197,7 @@ var _ = Describe("The Dash0 instrumentation webhook", func() { VolumeMounts: 2, Dash0VolumeMountIdx: 1, EnvVars: 4, - NodeOptionsEnvVarIdx: 1, + LdPreloadEnvVarIdx: 1, NodeIpIdx: 2, Dash0CollectorBaseUrlEnvVarIdx: 3, Dash0CollectorBaseUrlEnvVarExpectedValue: OTelCollectorBaseUrlTest, @@ -206,7 +206,7 @@ var _ = Describe("The Dash0 instrumentation webhook", func() { VolumeMounts: 3, Dash0VolumeMountIdx: 2, EnvVars: 5, - NodeOptionsEnvVarIdx: 2, + LdPreloadEnvVarIdx: 2, NodeIpIdx: 3, Dash0CollectorBaseUrlEnvVarIdx: 4, Dash0CollectorBaseUrlEnvVarExpectedValue: OTelCollectorBaseUrlTest, @@ -235,8 +235,8 @@ var _ = Describe("The Dash0 instrumentation webhook", func() { VolumeMounts: 2, Dash0VolumeMountIdx: 1, EnvVars: 4, - NodeOptionsEnvVarIdx: 1, - NodeOptionsUsesValueFrom: true, + LdPreloadEnvVarIdx: 1, + LdPreloadUsesValueFrom: true, NodeIpIdx: 2, Dash0CollectorBaseUrlEnvVarIdx: 3, Dash0CollectorBaseUrlEnvVarExpectedValue: OTelCollectorBaseUrlTest, @@ -245,8 +245,8 @@ var _ = Describe("The Dash0 instrumentation webhook", func() { VolumeMounts: 3, Dash0VolumeMountIdx: 1, EnvVars: 4, - NodeOptionsEnvVarIdx: 1, - NodeOptionsValue: "--require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry --require something-else --experimental-modules", + LdPreloadEnvVarIdx: 1, + LdPreloadValue: "/__dash0__/dash0_injector.so third_party_preload.so another_third_party_preload.so", NodeIpIdx: 2, Dash0CollectorBaseUrlEnvVarIdx: 0, Dash0CollectorBaseUrlEnvVarExpectedValue: OTelCollectorBaseUrlTest, diff --git a/internal/workloads/workload_modifier.go b/internal/workloads/workload_modifier.go index a046cac8..9f76c00d 100644 --- a/internal/workloads/workload_modifier.go +++ b/internal/workloads/workload_modifier.go @@ -28,12 +28,10 @@ const ( dash0DirectoryEnvVarName = "DASH0_INSTRUMENTATION_FOLDER_DESTINATION" dash0InstrumentationBaseDirectory = "/__dash0__" dash0InstrumentationDirectory = "/__dash0__/instrumentation" - // envVarLdPreloadName = "LD_PRELOAD" - // envVarLdPreloadValue = "/__dash0__/preload/inject.so" - envVarNodeOptionsName = "NODE_OPTIONS" - envVarNodeOptionsValue = "--require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry" - envVarDash0CollectorBaseUrlName = "DASH0_OTEL_COLLECTOR_BASE_URL" - envVarDash0NodeIp = "DASH0_NODE_IP" + envVarLdPreloadName = "LD_PRELOAD" + envVarLdPreloadValue = "/__dash0__/dash0_injector.so" + envVarDash0CollectorBaseUrlName = "DASH0_OTEL_COLLECTOR_BASE_URL" + envVarDash0NodeIp = "DASH0_NODE_IP" ) var ( @@ -238,8 +236,7 @@ func (m *ResourceModifier) addMount(container *corev1.Container) { } func (m *ResourceModifier) addEnvironmentVariables(container *corev1.Container, perContainerLogger logr.Logger) { - // For now, we directly modify NODE_OPTIONS. Consider migrating to an LD_PRELOAD hook at some point. - m.handleNodeOptionsEnvVar(container, perContainerLogger) + m.handleLdPreloadEnvVar(container, perContainerLogger) m.addOrReplaceEnvironmentVariable( container, @@ -261,7 +258,7 @@ func (m *ResourceModifier) addEnvironmentVariables(container *corev1.Container, ) } -func (m *ResourceModifier) handleNodeOptionsEnvVar( +func (m *ResourceModifier) handleLdPreloadEnvVar( container *corev1.Container, perContainerLogger logr.Logger, ) { @@ -269,13 +266,13 @@ func (m *ResourceModifier) handleNodeOptionsEnvVar( container.Env = make([]corev1.EnvVar, 0) } idx := slices.IndexFunc(container.Env, func(c corev1.EnvVar) bool { - return c.Name == envVarNodeOptionsName + return c.Name == envVarLdPreloadName }) if idx < 0 { container.Env = append(container.Env, corev1.EnvVar{ - Name: envVarNodeOptionsName, - Value: envVarNodeOptionsValue, + Name: envVarLdPreloadName, + Value: envVarLdPreloadValue, }) } else { // Note: This needs to be a point to the env var, otherwise updates would only be local to this function. @@ -285,15 +282,15 @@ func (m *ResourceModifier) handleNodeOptionsEnvVar( fmt.Sprintf( "Dash0 cannot prepend anything to the environment variable %s as it is specified via "+ "ValueFrom. This container will not be instrumented.", - envVarNodeOptionsName)) + envVarLdPreloadName)) return } - if !strings.Contains(envVar.Value, envVarNodeOptionsValue) { + if !strings.Contains(envVar.Value, envVarLdPreloadValue) { if envVar.Value == "" { - envVar.Value = envVarNodeOptionsValue + envVar.Value = envVarLdPreloadValue } else { - envVar.Value = fmt.Sprintf("%s %s", envVarNodeOptionsValue, envVar.Value) + envVar.Value = fmt.Sprintf("%s %s", envVarLdPreloadValue, envVar.Value) } } } @@ -408,17 +405,17 @@ func (m *ResourceModifier) removeMount(container *corev1.Container) { } func (m *ResourceModifier) removeEnvironmentVariables(container *corev1.Container) { - m.removeNodeOptions(container) + m.removeLdPreload(container) m.removeEnvironmentVariable(container, envVarDash0NodeIp) m.removeEnvironmentVariable(container, envVarDash0CollectorBaseUrlName) } -func (m *ResourceModifier) removeNodeOptions(container *corev1.Container) { +func (m *ResourceModifier) removeLdPreload(container *corev1.Container) { if container.Env == nil { return } idx := slices.IndexFunc(container.Env, func(c corev1.EnvVar) bool { - return c.Name == envVarNodeOptionsName + return c.Name == envVarLdPreloadName }) if idx < 0 { @@ -428,14 +425,14 @@ func (m *ResourceModifier) removeNodeOptions(container *corev1.Container) { previousValue := envVar.Value if previousValue == "" && envVar.ValueFrom != nil { // Specified via ValueFrom, this has not been done by us, so we assume there is no Dash0-specific - // NODE_OPTIONS part. + // LD_PRELOAD part. return - } else if previousValue == envVarNodeOptionsValue { + } else if previousValue == envVarLdPreloadValue { container.Env = slices.Delete(container.Env, idx, idx+1) return } - container.Env[idx].Value = strings.Replace(previousValue, envVarNodeOptionsValue, "", -1) + container.Env[idx].Value = strings.Replace(previousValue, envVarLdPreloadValue, "", -1) } } diff --git a/internal/workloads/workload_modifier_test.go b/internal/workloads/workload_modifier_test.go index 2a1f6216..57e1da37 100644 --- a/internal/workloads/workload_modifier_test.go +++ b/internal/workloads/workload_modifier_test.go @@ -75,7 +75,7 @@ var _ = Describe("Dash0 Workload Modification", func() { VolumeMounts: 2, Dash0VolumeMountIdx: 1, EnvVars: 4, - NodeOptionsEnvVarIdx: 1, + LdPreloadEnvVarIdx: 1, NodeIpIdx: 2, Dash0CollectorBaseUrlEnvVarIdx: 3, Dash0CollectorBaseUrlEnvVarExpectedValue: OTelCollectorBaseUrlTest, @@ -84,7 +84,7 @@ var _ = Describe("Dash0 Workload Modification", func() { VolumeMounts: 3, Dash0VolumeMountIdx: 2, EnvVars: 5, - NodeOptionsEnvVarIdx: 2, + LdPreloadEnvVarIdx: 2, NodeIpIdx: 3, Dash0CollectorBaseUrlEnvVarIdx: 4, Dash0CollectorBaseUrlEnvVarExpectedValue: OTelCollectorBaseUrlTest, @@ -108,8 +108,8 @@ var _ = Describe("Dash0 Workload Modification", func() { VolumeMounts: 2, Dash0VolumeMountIdx: 1, EnvVars: 4, - NodeOptionsEnvVarIdx: 1, - NodeOptionsUsesValueFrom: true, + LdPreloadEnvVarIdx: 1, + LdPreloadUsesValueFrom: true, NodeIpIdx: 2, Dash0CollectorBaseUrlEnvVarIdx: 3, Dash0CollectorBaseUrlEnvVarExpectedValue: OTelCollectorBaseUrlTest, @@ -118,8 +118,8 @@ var _ = Describe("Dash0 Workload Modification", func() { VolumeMounts: 3, Dash0VolumeMountIdx: 1, EnvVars: 4, - NodeOptionsEnvVarIdx: 1, - NodeOptionsValue: "--require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry --require something-else --experimental-modules", + LdPreloadEnvVarIdx: 1, + LdPreloadValue: "/__dash0__/dash0_injector.so third_party_preload.so another_third_party_preload.so", NodeIpIdx: 2, Dash0CollectorBaseUrlEnvVarIdx: 0, Dash0CollectorBaseUrlEnvVarExpectedValue: OTelCollectorBaseUrlTest, @@ -296,7 +296,7 @@ var _ = Describe("Dash0 Workload Modification", func() { VolumeMounts: 1, Dash0VolumeMountIdx: -1, EnvVars: 1, - NodeOptionsEnvVarIdx: -1, + LdPreloadEnvVarIdx: -1, NodeIpIdx: -1, Dash0CollectorBaseUrlEnvVarIdx: -1, }, @@ -304,7 +304,7 @@ var _ = Describe("Dash0 Workload Modification", func() { VolumeMounts: 2, Dash0VolumeMountIdx: -1, EnvVars: 2, - NodeOptionsEnvVarIdx: -1, + LdPreloadEnvVarIdx: -1, NodeIpIdx: -1, Dash0CollectorBaseUrlEnvVarIdx: -1, }, diff --git a/test/util/resources.go b/test/util/resources.go index 1d758356..2e4d286c 100644 --- a/test/util/resources.go +++ b/test/util/resources.go @@ -700,9 +700,9 @@ func DeploymentWithExistingDash0Artifacts(namespace string, name string) *appsv1 Value: "value", }, { - // The operator does not support injecting into containers that already have NODE_OPTIONS set via a + // The operator does not support injecting into containers that already have LD_PRELOAD set via a // ValueFrom clause, thus this env var will not be modified. - Name: "NODE_OPTIONS", + Name: "LD_PRELOAD", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{FieldPath: "metadata.namespace"}}, }, { @@ -739,8 +739,8 @@ func DeploymentWithExistingDash0Artifacts(namespace string, name string) *appsv1 Value: "base url will be replaced", }, { - Name: "NODE_OPTIONS", - Value: "--require something-else --experimental-modules", + Name: "LD_PRELOAD", + Value: "third_party_preload.so another_third_party_preload.so", }, { Name: "DASH0_NODE_IP", @@ -823,8 +823,8 @@ func InstrumentedDeploymentWithMoreBellsAndWhistles(namespace string, name strin Value: "value", }, { - Name: "NODE_OPTIONS", - Value: "--require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry", + Name: "LD_PRELOAD", + Value: "/__dash0__/dash0_injector.so", }, { Name: "DASH0_NODE_IP", @@ -863,8 +863,8 @@ func InstrumentedDeploymentWithMoreBellsAndWhistles(namespace string, name strin ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{FieldPath: "metadata.namespace"}}, }, { - Name: "NODE_OPTIONS", - Value: "--require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry", + Name: "LD_PRELOAD", + Value: "/__dash0__/dash0_injector.so", }, { Name: "DASH0_OTEL_COLLECTOR_BASE_URL", @@ -902,8 +902,8 @@ func simulateInstrumentedPodSpec(podSpec *corev1.PodSpec, meta *metav1.ObjectMet }} container.Env = []corev1.EnvVar{ { - Name: "NODE_OPTIONS", - Value: "--require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry", + Name: "LD_PRELOAD", + Value: "/__dash0__/dash0_injector.so", }, { Name: "DASH0_NODE_IP", diff --git a/test/util/verification.go b/test/util/verification.go index 081ac1be..a1eb752f 100644 --- a/test/util/verification.go +++ b/test/util/verification.go @@ -23,9 +23,9 @@ type ContainerExpectations struct { VolumeMounts int Dash0VolumeMountIdx int EnvVars int - NodeOptionsEnvVarIdx int - NodeOptionsValue string - NodeOptionsUsesValueFrom bool + LdPreloadEnvVarIdx int + LdPreloadValue string + LdPreloadUsesValueFrom bool NodeIpIdx int Dash0CollectorBaseUrlEnvVarIdx int Dash0CollectorBaseUrlEnvVarExpectedValue string @@ -53,7 +53,7 @@ func BasicInstrumentedPodSpecExpectations() PodSpecExpectations { VolumeMounts: 1, Dash0VolumeMountIdx: 0, EnvVars: 3, - NodeOptionsEnvVarIdx: 0, + LdPreloadEnvVarIdx: 0, NodeIpIdx: 1, Dash0CollectorBaseUrlEnvVarIdx: 2, Dash0CollectorBaseUrlEnvVarExpectedValue:// @@ -255,16 +255,16 @@ func verifyPodSpec(podSpec corev1.PodSpec, expectations PodSpecExpectations) { } Expect(container.Env).To(HaveLen(containerExpectations.EnvVars)) for j, envVar := range container.Env { - if j == containerExpectations.NodeOptionsEnvVarIdx { - Expect(envVar.Name).To(Equal("NODE_OPTIONS")) - if containerExpectations.NodeOptionsUsesValueFrom { + if j == containerExpectations.LdPreloadEnvVarIdx { + Expect(envVar.Name).To(Equal("LD_PRELOAD")) + if containerExpectations.LdPreloadUsesValueFrom { Expect(envVar.Value).To(BeEmpty()) Expect(envVar.ValueFrom).To(Not(BeNil())) - } else if containerExpectations.NodeOptionsValue != "" { - Expect(envVar.Value).To(Equal(containerExpectations.NodeOptionsValue)) + } else if containerExpectations.LdPreloadValue != "" { + Expect(envVar.Value).To(Equal(containerExpectations.LdPreloadValue)) } else { Expect(envVar.Value).To(Equal( - "--require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry", + "/__dash0__/dash0_injector.so", )) } } else if j == containerExpectations.NodeIpIdx { From f64c9bea2eba52b0017f0b20452da14afa954937 Mon Sep 17 00:00:00 2001 From: Bastian Krol Date: Mon, 21 Oct 2024 22:14:17 +0200 Subject: [PATCH 5/8] test(instrumentation): test on arm64 & x86_64, add more test cases Also: - refactor test scripts - lint scripts with shellcheck --- images/instrumentation/build.sh | 6 +- .../instrumentation/copy-instrumentation.sh | 3 + .../test/scripts/build-in-container.sh | 11 +- .../test/scripts/run-tests-for-container.sh | 7 +- .../scripts/run-tests-within-container.sh | 3 +- .../injector/test/scripts/test-all.sh | 7 +- images/instrumentation/node.js/build.sh | 2 +- images/instrumentation/test/node/Dockerfile | 19 ++- images/instrumentation/test/node/base-images | 8 +- .../node/test-cases/existing-unmodified/.env | 1 + .../test-cases/existing-unmodified/index.js | 9 ++ .../test-cases/node-options-already-set/.env | 1 + .../node-options-already-set/index.js | 9 ++ .../package.json | 3 - .../server.js | 6 - .../node/test-cases/node-options-unset/.env | 0 .../test-cases/node-options-unset/index.js | 9 ++ .../undefined-for-non-existing/.env | 0 .../undefined-for-non-existing/index.js | 9 ++ .../test-cases/verify-distro-is-loaded/.env | 0 .../verify-distro-is-loaded/index.js | 10 ++ images/instrumentation/test/test-all.sh | 136 ++++++++++++++++++ images/instrumentation/test/test.sh | 53 ------- 23 files changed, 222 insertions(+), 90 deletions(-) create mode 100644 images/instrumentation/test/node/test-cases/existing-unmodified/.env create mode 100644 images/instrumentation/test/node/test-cases/existing-unmodified/index.js create mode 100644 images/instrumentation/test/node/test-cases/node-options-already-set/.env create mode 100644 images/instrumentation/test/node/test-cases/node-options-already-set/index.js delete mode 100644 images/instrumentation/test/node/test-cases/node-options-includes-dash0-require/package.json delete mode 100644 images/instrumentation/test/node/test-cases/node-options-includes-dash0-require/server.js create mode 100644 images/instrumentation/test/node/test-cases/node-options-unset/.env create mode 100644 images/instrumentation/test/node/test-cases/node-options-unset/index.js create mode 100644 images/instrumentation/test/node/test-cases/undefined-for-non-existing/.env create mode 100644 images/instrumentation/test/node/test-cases/undefined-for-non-existing/index.js create mode 100644 images/instrumentation/test/node/test-cases/verify-distro-is-loaded/.env create mode 100644 images/instrumentation/test/node/test-cases/verify-distro-is-loaded/index.js create mode 100755 images/instrumentation/test/test-all.sh delete mode 100755 images/instrumentation/test/test.sh diff --git a/images/instrumentation/build.sh b/images/instrumentation/build.sh index 0884bd1d..719b294b 100755 --- a/images/instrumentation/build.sh +++ b/images/instrumentation/build.sh @@ -1,7 +1,11 @@ #!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + set -euo pipefail -cd "$(dirname ${BASH_SOURCE})" +cd "$(dirname "${0}")" pushd node.js > /dev/null ./build.sh diff --git a/images/instrumentation/copy-instrumentation.sh b/images/instrumentation/copy-instrumentation.sh index e376ba87..5e7926aa 100755 --- a/images/instrumentation/copy-instrumentation.sh +++ b/images/instrumentation/copy-instrumentation.sh @@ -1,5 +1,8 @@ #!/bin/sh +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + set -eu if [ -n "${DASH0_DEBUG:-}" ]; then diff --git a/images/instrumentation/injector/test/scripts/build-in-container.sh b/images/instrumentation/injector/test/scripts/build-in-container.sh index c8725a30..0381889c 100755 --- a/images/instrumentation/injector/test/scripts/build-in-container.sh +++ b/images/instrumentation/injector/test/scripts/build-in-container.sh @@ -23,11 +23,6 @@ dockerfile_name=injector/test/docker/Dockerfile-build image_name=dash0-injector-builder-$ARCH container_name=$image_name -docker_run_extra_arguments="" -if [ "${INTERACTIVE:-}" = "true" ]; then - docker_run_extra_arguments=/bin/bash -fi - echo echo echo ">>> Building the library on $ARCH <<<" @@ -43,7 +38,7 @@ docker build \ container_id=$(docker create "$image_name") docker container cp \ - $container_id:/dash0-init-container/injector/bin/dash0_injector.so \ - injector/test/bin/dash0_injector_$ARCH.so -docker rm -v $container_id + "$container_id":/dash0-init-container/injector/bin/dash0_injector.so \ + injector/test/bin/dash0_injector_"$ARCH".so +docker rm -v "$container_id" diff --git a/images/instrumentation/injector/test/scripts/run-tests-for-container.sh b/images/instrumentation/injector/test/scripts/run-tests-for-container.sh index e5e07b6d..f48fed61 100755 --- a/images/instrumentation/injector/test/scripts/run-tests-for-container.sh +++ b/images/instrumentation/injector/test/scripts/run-tests-for-container.sh @@ -53,11 +53,10 @@ echo --------------------------------------- echo "testing the library on $ARCH and $LIBC" echo --------------------------------------- -set -x - docker rm -f "$container_name" 2> /dev/null docker rmi -f "$image_name" 2> /dev/null +set -x docker build \ --platform "$docker_platform" \ --build-arg "base_image=${base_image}" \ @@ -73,6 +72,4 @@ docker run \ -it \ "$image_name" \ $docker_run_extra_arguments - -set +x - +{ set +x; } 2> /dev/null diff --git a/images/instrumentation/injector/test/scripts/run-tests-within-container.sh b/images/instrumentation/injector/test/scripts/run-tests-within-container.sh index 2ae3d0dd..bc3eb204 100755 --- a/images/instrumentation/injector/test/scripts/run-tests-within-container.sh +++ b/images/instrumentation/injector/test/scripts/run-tests-within-container.sh @@ -1,4 +1,5 @@ #!/usr/bin/env sh +# shellcheck disable=SC2059 # SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. # SPDX-License-Identifier: Apache-2.0 @@ -32,7 +33,7 @@ fi injector_binary=/dash0-init-container/injector/dash0_injector.so if [ ! -f $injector_binary ]; then - printf "${RED}error: $injector_binary does not exist, not running any tests.${NC}\n" + printf "${RED}error: %s does not exist, not running any tests.${NC}\n" "$injector_binary" exit 1 fi diff --git a/images/instrumentation/injector/test/scripts/test-all.sh b/images/instrumentation/injector/test/scripts/test-all.sh index 4a1f6136..94f5a818 100755 --- a/images/instrumentation/injector/test/scripts/test-all.sh +++ b/images/instrumentation/injector/test/scripts/test-all.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC2059 # SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. # SPDX-License-Identifier: Apache-2.0 @@ -31,12 +32,12 @@ while IFS= read -r line; do copy_lines=true fi if [[ $copy_lines == true ]]; then - echo $line >> injector/test/docker/Dockerfile-build + echo "$line" >> injector/test/docker/Dockerfile-build fi done < Dockerfile if [[ ! -e injector/test/docker/Dockerfile-build ]]; then - echo "\nError: The file injector/test/docker/Dockerfile-build has not been generated, stopping." + printf "\nError: The file injector/test/docker/Dockerfile-build has not been generated, stopping." exit 1 fi @@ -72,6 +73,6 @@ run_tests_for_architecture_and_libc_flavor x86_64 glibc run_tests_for_architecture_and_libc_flavor arm64 musl run_tests_for_architecture_and_libc_flavor x86_64 musl -echo "$summary" +printf "$summary\n" exit $exit_code diff --git a/images/instrumentation/node.js/build.sh b/images/instrumentation/node.js/build.sh index 10ac67cb..2a029b93 100755 --- a/images/instrumentation/node.js/build.sh +++ b/images/instrumentation/node.js/build.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -cd "$(dirname ${BASH_SOURCE})" +cd "$(dirname "$0")" if [[ "${USE_LOCAL_SOURCES_FOR_NODEJS_DISTRIBUTION:-}" = true ]]; then echo "Node.js: using the local sources for @dash0hq/opentelemetry" diff --git a/images/instrumentation/test/node/Dockerfile b/images/instrumentation/test/node/Dockerfile index cb58eaf6..4a4435fe 100644 --- a/images/instrumentation/test/node/Dockerfile +++ b/images/instrumentation/test/node/Dockerfile @@ -1,13 +1,18 @@ -ARG base_image -ARG instr_image +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 -FROM ${instr_image} AS instr +ARG instrumentation_image=dash0-instrumentation:latest +ARG base_image=node:20.18-bookworm + +FROM ${instrumentation_image} AS init FROM ${base_image} -# TODO Normalize node.js path +COPY test-cases /test-cases -COPY ./test-cases /test-cases +# The following lines emulate the behavior of running the instrumentation image as a Kubernetes init container and +# setting the LD_PRELOAD environment variable via the operator. +COPY --from=init /dash0-init-container/*.so /__dash0__/ +COPY --from=init /dash0-init-container/instrumentation /__dash0__/instrumentation +ENV LD_PRELOAD=/__dash0__/dash0_injector.so -COPY --from=instr /dash0-init-container /__dash0__ -ENV LD_PRELOAD=/__dash0__/injector/dash0_injector.so \ No newline at end of file diff --git a/images/instrumentation/test/node/base-images b/images/instrumentation/test/node/base-images index 69bde4ef..f1d2604e 100644 --- a/images/instrumentation/test/node/base-images +++ b/images/instrumentation/test/node/base-images @@ -1,4 +1,7 @@ -# One base image per line +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + +# one base image per line node:22-alpine node:22-bookworm node:22-bullseye @@ -7,4 +10,5 @@ node:20-bookworm node:20-bullseye node:18-alpine node:18-bookworm -node:18-bullseye \ No newline at end of file +node:18-bullseye + diff --git a/images/instrumentation/test/node/test-cases/existing-unmodified/.env b/images/instrumentation/test/node/test-cases/existing-unmodified/.env new file mode 100644 index 00000000..c69679e9 --- /dev/null +++ b/images/instrumentation/test/node/test-cases/existing-unmodified/.env @@ -0,0 +1 @@ +AN_ENVIRONMENT_VARIABLE=value diff --git a/images/instrumentation/test/node/test-cases/existing-unmodified/index.js b/images/instrumentation/test/node/test-cases/existing-unmodified/index.js new file mode 100644 index 00000000..641e2d02 --- /dev/null +++ b/images/instrumentation/test/node/test-cases/existing-unmodified/index.js @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + +const process = require('node:process'); + +if (process.env["AN_ENVIRONMENT_VARIABLE"] !== 'value') { + console.error(`Unexpected value for AN_ENVIRONMENT_VARIABLE: ${process.env["AN_ENVIRONMENT_VARIABLE"]}`); + process.exit(1); +} diff --git a/images/instrumentation/test/node/test-cases/node-options-already-set/.env b/images/instrumentation/test/node/test-cases/node-options-already-set/.env new file mode 100644 index 00000000..b5524a85 --- /dev/null +++ b/images/instrumentation/test/node/test-cases/node-options-already-set/.env @@ -0,0 +1 @@ +NODE_OPTIONS=--no-deprecation diff --git a/images/instrumentation/test/node/test-cases/node-options-already-set/index.js b/images/instrumentation/test/node/test-cases/node-options-already-set/index.js new file mode 100644 index 00000000..78f710f7 --- /dev/null +++ b/images/instrumentation/test/node/test-cases/node-options-already-set/index.js @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + +const process = require('node:process'); + +if (process.env["NODE_OPTIONS"] !== "--require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry --no-deprecation") { + console.error(`Unexpected value for NODE_OPTIONS: ${process.env["NODE_OPTIONS"]}`); + process.exit(1); +} diff --git a/images/instrumentation/test/node/test-cases/node-options-includes-dash0-require/package.json b/images/instrumentation/test/node/test-cases/node-options-includes-dash0-require/package.json deleted file mode 100644 index e83de75e..00000000 --- a/images/instrumentation/test/node/test-cases/node-options-includes-dash0-require/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "@dash0/injector_tests_node-options-includes-dash0-require" -} \ No newline at end of file diff --git a/images/instrumentation/test/node/test-cases/node-options-includes-dash0-require/server.js b/images/instrumentation/test/node/test-cases/node-options-includes-dash0-require/server.js deleted file mode 100644 index e8957954..00000000 --- a/images/instrumentation/test/node/test-cases/node-options-includes-dash0-require/server.js +++ /dev/null @@ -1,6 +0,0 @@ -const nodeOptions = process.env["NODE_OPTIONS"] || ""; - -if (!nodeOptions.includes("--require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry")) { - console.error(`No Dash0 distro require found in NODE_OPTIONS value: '${nodeOptions}'`) - process.exit(1); -} \ No newline at end of file diff --git a/images/instrumentation/test/node/test-cases/node-options-unset/.env b/images/instrumentation/test/node/test-cases/node-options-unset/.env new file mode 100644 index 00000000..e69de29b diff --git a/images/instrumentation/test/node/test-cases/node-options-unset/index.js b/images/instrumentation/test/node/test-cases/node-options-unset/index.js new file mode 100644 index 00000000..659d28a1 --- /dev/null +++ b/images/instrumentation/test/node/test-cases/node-options-unset/index.js @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + +const process = require('node:process'); + +if (process.env["NODE_OPTIONS"] !== "--require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry") { + console.error(`Unexpected value for NODE_OPTIONS: ${process.env["NODE_OPTIONS"]}`); + process.exit(1); +} diff --git a/images/instrumentation/test/node/test-cases/undefined-for-non-existing/.env b/images/instrumentation/test/node/test-cases/undefined-for-non-existing/.env new file mode 100644 index 00000000..e69de29b diff --git a/images/instrumentation/test/node/test-cases/undefined-for-non-existing/index.js b/images/instrumentation/test/node/test-cases/undefined-for-non-existing/index.js new file mode 100644 index 00000000..62e56076 --- /dev/null +++ b/images/instrumentation/test/node/test-cases/undefined-for-non-existing/index.js @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + +const process = require('node:process') + +if (process.env["UNDEFINED_ENVIRONMENT_VARIABLE"] !== undefined) { + console.error(`Unexpected value for UNDEFINED_ENVIRONMENT_VARIABLE: ${process.env["UNDEFINED_ENVIRONMENT_VARIABLE"]}`); + process.exit(1); +} diff --git a/images/instrumentation/test/node/test-cases/verify-distro-is-loaded/.env b/images/instrumentation/test/node/test-cases/verify-distro-is-loaded/.env new file mode 100644 index 00000000..e69de29b diff --git a/images/instrumentation/test/node/test-cases/verify-distro-is-loaded/index.js b/images/instrumentation/test/node/test-cases/verify-distro-is-loaded/index.js new file mode 100644 index 00000000..3d3fbea5 --- /dev/null +++ b/images/instrumentation/test/node/test-cases/verify-distro-is-loaded/index.js @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + +const process = require('node:process'); + +const loadedModules = Object.keys(require.cache); +if (!loadedModules.includes('/__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry/dist/src/init.js')) { + console.error(`It looks like the Dash0 Node.js OpenTelemetry distribution has not been loaded.`); + process.exit(1); +} diff --git a/images/instrumentation/test/test-all.sh b/images/instrumentation/test/test-all.sh new file mode 100755 index 00000000..0e261bd8 --- /dev/null +++ b/images/instrumentation/test/test-all.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2059 + +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail +shopt -s lastpipe + +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +cd "$(dirname "$0")"/.. + +script_dir="test" +exit_code=0 +summary="" + +build_instrumentation_image() { + instrumentation_image=${1:-} + arch=${2:-} + docker_platform=${3:-} + + if [[ -z ${instrumentation_image} ]]; then + echo "missing mandatory argument: instrumentation_image" + exit 1 + fi + if [[ -z ${arch} ]]; then + echo "missing mandatory argument: architecture" + exit 1 + fi + if [[ -z ${docker_platform} ]]; then + echo "missing mandatory argument: docker_platform" + exit 1 + fi + + echo "building instrumentation image \"${instrumentation_image} for architecture ${arch}" + if ! build_output=$( + docker build \ + --platform "$docker_platform" \ + . \ + -t "${instrumentation_image}" \ + 2>&1 + ); then + echo "${build_output}" + exit 1 + fi +} + +run_tests_for_runtime() { + local runtime="${1:-}" + local image_name_test="${2:-}" + local base_image="${3:?}" + + if [[ -z $runtime ]]; then + echo "missing parameter: runtime" + exit 1 + fi + if [[ -z $image_name_test ]]; then + echo "missing parameter: image_name_test" + exit 1 + fi + + for t in "${script_dir}"/"${runtime}"/test-cases/*/ ; do + test=$(basename "$(realpath "${t}")") + if docker_run_output=$(docker run \ + --env-file="${script_dir}/${runtime}/test-cases/${test}/.env" \ + "${image_name_test}" \ + node "/test-cases/${test}" \ + 2>&1 + ); then + printf "${GREEN}test case \"${test}\": OK${NC}\n" + else + printf "${RED}test case \"${test}\": FAIL\n" + printf "test output:${NC}\n" + echo "$docker_run_output" + exit_code=1 + summary="$summary\n${runtime}/${base_image}\t- ${test}:\tfailed" + fi + done +} + +run_tests_for_architecture() { + arch="${1:-}" + if [[ -z ${arch} ]]; then + echo "missing mandatory argument: architecture" + exit 1 + fi + if [ "$arch" = arm64 ]; then + docker_platform=linux/arm64 + elif [ "$arch" = x86_64 ]; then + docker_platform=linux/amd64 + else + echo "The architecture $arch is not supported." + exit 1 + fi + + instrumentation_image="dash0-instrumentation-$arch:latest" + build_instrumentation_image "$instrumentation_image" "$arch" "$docker_platform" + for r in "${script_dir}"/*/ ; do + runtime=$(basename "$(realpath "${r}")") + echo "runtime '${runtime}'" + grep '^[^#;]' "${script_dir}/${runtime}/base-images" | while read -r base_image ; do + echo "base image '${base_image}'" + image_name_test="test-${runtime}-${arch}:latest" + echo "building test image for ${arch}/${runtime}/${base_image} with instrumentation image ${instrumentation_image}" + if ! build_output=$( + docker build \ + --platform "$docker_platform" \ + --build-arg "instrumentation_image=${instrumentation_image}" \ + --build-arg "base_image=${base_image}" \ + "${script_dir}/${runtime}" \ + -t "$image_name_test" \ + 2>&1 + ); then + echo "${build_output}" + exit 1 + fi + run_tests_for_runtime "${runtime}" "$image_name_test" "$base_image" + done + done +} + +run_tests_for_architecture arm64 +run_tests_for_architecture x86_64 + +if [[ $exit_code -ne 0 ]]; then + printf "\n${RED}There have been failing test cases:" + printf "$summary\n" + printf "\nSee above for details.${NC}\n" +else + printf "\n${GREEN}All test cases have passed.${NC}\n" +fi +exit $exit_code + diff --git a/images/instrumentation/test/test.sh b/images/instrumentation/test/test.sh deleted file mode 100755 index d2e6cf05..00000000 --- a/images/instrumentation/test/test.sh +++ /dev/null @@ -1,53 +0,0 @@ -#/bin/env bash - -readonly script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) - -function run_tests () { - local runtime="${1}" - local docker_image="${2}" - - for t in ${script_dir}/${runtime}/test-cases/*/ ; do - test=$(basename $(realpath "${t}")) - printf '%s' "Test '${test}' " | sed 's/^/ /' # `echo -n` does not work on Mac OS - tempfile=$(mktemp) - docker run ${docker_image} "npm" "start" "--prefix" "/test-cases/${test}" > ${tempfile} 2>&1 - if [ $? -eq 0 ]; then - echo OK - else - echo FAIL - echo 'Output (stdout + stderr):' - cat ${tempfile} | sed 's/^/ /' - fi - rm ${tempfile} - done -} - -# Build instr image -instr_image="${1}" - -if [ -z "${instr_image}" ]; then - instr_image="dash0-instrumentation:test-latest" - echo "Building instrumentation image '${instr_image}' (no instrumentation image tag provided in input)" - - tempfile=$(mktemp) - if ! docker build ../.. -t "${instr_image}" > ${tempfile} 2>&1; then - echo "Failed to build the instrumentation image:" - cat ${tempfile} | sed 's/^/ /' - fi - rm ${tempfile} -fi - -# Run tests -for r in ${script_dir}/*/ ; do - runtime=$(basename $(realpath "${r}")) - echo "Runtime '${runtime}'" - grep '^[^#;]' "${script_dir}/${runtime}/base-images" | while read -r base_image ; do - echo "Base image '${base_image}'" | sed 's/^/ /' - build_output=$(docker build "${script_dir}/${runtime}" --build-arg "instr_image=${instr_image}" --build-arg "base_image=${base_image}" -t "test-${runtime}:latest" 2>&1 | sed 's/^/ /') - if [ $? -ne 0 ]; then - echo "${build_output}" - exit 1 - fi - run_tests "${runtime}" "test-${runtime}:latest" | sed 's/^/ /' - done -done \ No newline at end of file From afb71f32ccd591c00538386961bff04415f95a4a Mon Sep 17 00:00:00 2001 From: Bastian Krol Date: Tue, 22 Oct 2024 12:49:11 +0200 Subject: [PATCH 6/8] chore: check all scripts with shellcheck via make lint --- .husky/hooks/pre-commit | 2 +- .husky/hooks/pre-push | 2 +- Makefile | 35 ++++++++++++++++--- helm-chart/bin/publish.sh | 2 +- .../dash0-operator/tests/update-snapshots.sh | 2 +- images/collector/src/image/entrypoint.sh | 10 +++--- images/instrumentation/build.sh | 2 +- .../test/scripts/build-in-container.sh | 2 +- .../test/scripts/run-tests-for-container.sh | 2 +- .../injector/test/scripts/test-all.sh | 2 +- images/instrumentation/node.js/build.sh | 2 +- images/instrumentation/test/test-all.sh | 2 +- test-resources/bin/ensure-namespace-exists.sh | 4 +-- test-resources/bin/render-templates.sh | 4 ++- test-resources/bin/test-cleanup.sh | 12 +++---- .../bin/test-scenario-01-aum-operator-cr.sh | 8 ++--- .../bin/test-scenario-02-operator-cr-aum.sh | 8 ++--- test-resources/node.js/express/deploy.sh | 6 ++-- test-resources/node.js/express/undeploy.sh | 4 +-- 19 files changed, 69 insertions(+), 42 deletions(-) diff --git a/.husky/hooks/pre-commit b/.husky/hooks/pre-commit index a9177f71..d11402b4 100755 --- a/.husky/hooks/pre-commit +++ b/.husky/hooks/pre-commit @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -cd "$(dirname ${BASH_SOURCE})"/../.. +cd "$(dirname "${BASH_SOURCE[0]}")"/../.. gofmt -s -w . diff --git a/.husky/hooks/pre-push b/.husky/hooks/pre-push index 35135305..10ef0b5d 100755 --- a/.husky/hooks/pre-push +++ b/.husky/hooks/pre-push @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -cd "$(dirname ${BASH_SOURCE})"/../.. +cd "$(dirname "${BASH_SOURCE[0]}")"/../.. make make lint diff --git a/Makefile b/Makefile index d530b365..022b86bd 100644 --- a/Makefile +++ b/Makefile @@ -155,17 +155,42 @@ test-e2e: GOLANGCI_LINT = $(shell pwd)/bin/golangci-lint GOLANGCI_LINT_VERSION ?= v1.61.0 -golangci-lint: +golangci-lint-install: @[ -f $(GOLANGCI_LINT) ] || { \ set -e ;\ curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell dirname $(GOLANGCI_LINT)) $(GOLANGCI_LINT_VERSION) ;\ } -.PHONY: lint -lint: golangci-lint ## Run golangci-lint linter & yamllint - @echo -------------------------------- +.PHONY: golangci-lint +golangci-lint: golangci-lint-install + @echo "-------------------------------- (linting Go code)" $(GOLANGCI_LINT) run - helm lint helm-chart/dash0-operator + +.PHONY: helm-chart-lint +helm-chart-lint: + @echo "-------------------------------- (linting Helm charts)" + $(eval HELM_LINT_OUTPUT=$(shell helm lint --quiet helm-chart/dash0-operator)) + @if [ -n "$(HELM_LINT_OUTPUT)" ]; then \ + echo helm lint found issues: ; \ + echo "$(HELM_LINT_OUTPUT)" ; \ + exit 1; \ + fi + +.PHONY: shellcheck-check-installed +shellcheck-check-installed: + @set +x + @if ! shellcheck --version > /dev/null; then \ + echo "error: shellcheck is not installed. See https://github.com/koalaman/shellcheck?tab=readme-ov-file#installing for installation instructions."; \ + exit 1; \ + fi + +.PHONY: shellcheck-lint +shellcheck-lint: shellcheck-check-installed + @echo "-------------------------------- (linting shell scripts)" + find . -name \*.sh | xargs shellcheck -x + +.PHONY: lint +lint: golangci-lint helm-chart-lint shellcheck-lint .PHONY: lint-fix lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes diff --git a/helm-chart/bin/publish.sh b/helm-chart/bin/publish.sh index 16ffda20..f8f0dc83 100755 --- a/helm-chart/bin/publish.sh +++ b/helm-chart/bin/publish.sh @@ -5,7 +5,7 @@ set -euo pipefail -cd "$(dirname "${0}")"/.. +cd "$(dirname "${BASH_SOURCE[0]}")"/.. # Use DRY_RUN=true to verify that the helm chart can be successfully packaged -- all steps will be executed except for # the final git push to the gh-pages branch. diff --git a/helm-chart/dash0-operator/tests/update-snapshots.sh b/helm-chart/dash0-operator/tests/update-snapshots.sh index 86b45f1b..4507cfdc 100755 --- a/helm-chart/dash0-operator/tests/update-snapshots.sh +++ b/helm-chart/dash0-operator/tests/update-snapshots.sh @@ -5,6 +5,6 @@ set -euo pipefail -cd "$(dirname ${BASH_SOURCE})"/.. +cd "$(dirname "${BASH_SOURCE[0]}")"/.. helm unittest -f 'tests/**/*.yaml' --update-snapshot . \ No newline at end of file diff --git a/images/collector/src/image/entrypoint.sh b/images/collector/src/image/entrypoint.sh index 125bd155..01ec2618 100755 --- a/images/collector/src/image/entrypoint.sh +++ b/images/collector/src/image/entrypoint.sh @@ -4,12 +4,12 @@ DASH0_COLLECTOR_PID=$! -mkdir -p $(dirname ${DASH0_COLLECTOR_PID_FILE}) +mkdir -p "$(dirname "${DASH0_COLLECTOR_PID_FILE}")" -echo -n "${DASH0_COLLECTOR_PID}" > ${DASH0_COLLECTOR_PID_FILE} +printf "%s" "${DASH0_COLLECTOR_PID}" > "${DASH0_COLLECTOR_PID_FILE}" -echo -n "Collector pid file created at '${DASH0_COLLECTOR_PID_FILE}': " -cat ${DASH0_COLLECTOR_PID_FILE} +printf "Collector pid file created at \"%s\": " "${DASH0_COLLECTOR_PID_FILE}" +cat "${DASH0_COLLECTOR_PID_FILE}" echo -wait ${DASH0_COLLECTOR_PID} \ No newline at end of file +wait ${DASH0_COLLECTOR_PID} diff --git a/images/instrumentation/build.sh b/images/instrumentation/build.sh index 719b294b..71619e6d 100755 --- a/images/instrumentation/build.sh +++ b/images/instrumentation/build.sh @@ -5,7 +5,7 @@ set -euo pipefail -cd "$(dirname "${0}")" +cd "$(dirname "${BASH_SOURCE[0]}")" pushd node.js > /dev/null ./build.sh diff --git a/images/instrumentation/injector/test/scripts/build-in-container.sh b/images/instrumentation/injector/test/scripts/build-in-container.sh index 0381889c..ff138ae7 100755 --- a/images/instrumentation/injector/test/scripts/build-in-container.sh +++ b/images/instrumentation/injector/test/scripts/build-in-container.sh @@ -5,7 +5,7 @@ set -eu -cd "$(dirname "$0")"/../../.. +cd "$(dirname "${BASH_SOURCE[0]}")"/../../.. if [ -z "${ARCH:-}" ]; then ARCH=arm64 diff --git a/images/instrumentation/injector/test/scripts/run-tests-for-container.sh b/images/instrumentation/injector/test/scripts/run-tests-for-container.sh index f48fed61..4d05fc72 100755 --- a/images/instrumentation/injector/test/scripts/run-tests-for-container.sh +++ b/images/instrumentation/injector/test/scripts/run-tests-for-container.sh @@ -5,7 +5,7 @@ set -eu -cd "$(dirname "$0")"/.. +cd "$(dirname "${BASH_SOURCE[0]}")"/.. if [ -z "${ARCH:-}" ]; then ARCH=arm64 diff --git a/images/instrumentation/injector/test/scripts/test-all.sh b/images/instrumentation/injector/test/scripts/test-all.sh index 94f5a818..0e792d57 100755 --- a/images/instrumentation/injector/test/scripts/test-all.sh +++ b/images/instrumentation/injector/test/scripts/test-all.sh @@ -10,7 +10,7 @@ RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' -cd "$(dirname "$0")"/../../.. +cd "$(dirname "${BASH_SOURCE[0]}")"/../../.. # remove all outdated injector binaries rm -rf injector/test/bin/* diff --git a/images/instrumentation/node.js/build.sh b/images/instrumentation/node.js/build.sh index 2a029b93..87534b0b 100755 --- a/images/instrumentation/node.js/build.sh +++ b/images/instrumentation/node.js/build.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -cd "$(dirname "$0")" +cd "$(dirname "${BASH_SOURCE[0]}")" if [[ "${USE_LOCAL_SOURCES_FOR_NODEJS_DISTRIBUTION:-}" = true ]]; then echo "Node.js: using the local sources for @dash0hq/opentelemetry" diff --git a/images/instrumentation/test/test-all.sh b/images/instrumentation/test/test-all.sh index 0e261bd8..91ea4eb4 100755 --- a/images/instrumentation/test/test-all.sh +++ b/images/instrumentation/test/test-all.sh @@ -11,7 +11,7 @@ RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' -cd "$(dirname "$0")"/.. +cd "$(dirname "${BASH_SOURCE[0]}")"/.. script_dir="test" exit_code=0 diff --git a/test-resources/bin/ensure-namespace-exists.sh b/test-resources/bin/ensure-namespace-exists.sh index ab3055e8..b9e002e9 100755 --- a/test-resources/bin/ensure-namespace-exists.sh +++ b/test-resources/bin/ensure-namespace-exists.sh @@ -11,6 +11,6 @@ if [[ "${target_namespace}" == default ]]; then exit 0 fi -if ! kubectl get ns ${target_namespace} &> /dev/null; then - kubectl create ns ${target_namespace} +if ! kubectl get ns "${target_namespace}" &> /dev/null; then + kubectl create ns "${target_namespace}" fi diff --git a/test-resources/bin/render-templates.sh b/test-resources/bin/render-templates.sh index 9b4fe380..a380829d 100755 --- a/test-resources/bin/render-templates.sh +++ b/test-resources/bin/render-templates.sh @@ -5,9 +5,11 @@ set -euo pipefail -cd "$(dirname ${BASH_SOURCE})"/../.. +cd "$(dirname "${BASH_SOURCE[0]}")"/../.. source test-resources/bin/util load_env_file +# shellcheck disable=SC2002 cat test-resources/customresources/dash0operatorconfiguration/dash0operatorconfiguration.token.yaml.template | DASH0_AUTHORIZATION_TOKEN="$DASH0_AUTHORIZATION_TOKEN" envsubst > test-resources/customresources/dash0operatorconfiguration/dash0operatorconfiguration.token.yaml + diff --git a/test-resources/bin/test-cleanup.sh b/test-resources/bin/test-cleanup.sh index 35a2f71f..2c5d889f 100755 --- a/test-resources/bin/test-cleanup.sh +++ b/test-resources/bin/test-cleanup.sh @@ -5,7 +5,7 @@ set -euo pipefail -cd "$(dirname ${BASH_SOURCE})"/../.. +cd "$(dirname "${BASH_SOURCE[0]}")"/../.. target_namespace=${1:-test-namespace} delete_namespace=${2:-true} @@ -16,17 +16,17 @@ verify_kubectx resource_types=( cronjob daemonset deployment job pod replicaset statefulset ) for resource_type in "${resource_types[@]}"; do - test-resources/node.js/express/undeploy.sh ${target_namespace} ${resource_type} + test-resources/node.js/express/undeploy.sh "${target_namespace}" "${resource_type}" done -kubectl delete -n ${target_namespace} -f test-resources/customresources/dash0monitoring/dash0monitoring.yaml --wait=false || true +kubectl delete -n "${target_namespace}" -f test-resources/customresources/dash0monitoring/dash0monitoring.yaml --wait=false || true sleep 1 kubectl patch -f test-resources/customresources/dash0monitoring/dash0monitoring.yaml -p '{"metadata":{"finalizers":null}}' --type=merge || true kubectl delete -f test-resources/customresources/dash0operatorconfiguration/dash0operatorconfiguration.token.yaml || true kubectl delete dash0operatorconfigurations.operator.dash0.com/dash0-operator-configuration-auto-resource || true if [[ "${target_namespace}" != "default" ]] && [[ "${delete_namespace}" == "true" ]]; then - kubectl delete ns ${target_namespace} --ignore-not-found + kubectl delete ns "${target_namespace}" --ignore-not-found fi helm uninstall --namespace dash0-system dash0-operator --timeout 30s || true @@ -40,8 +40,8 @@ kubectl delete ns dash0-system --ignore-not-found # deliberately deleting dashboards & check rules after undeploying the operator to avoid deleting these items in # Dash0 every time. -kubectl delete -n ${target_namespace} -f test-resources/customresources/persesdashboard/persesdashboard.yaml || true -kubectl delete -n ${target_namespace} -f test-resources/customresources/prometheusrule/prometheusrule.yaml || true +kubectl delete -n "${target_namespace}" -f test-resources/customresources/persesdashboard/persesdashboard.yaml || true +kubectl delete -n "${target_namespace}" -f test-resources/customresources/prometheusrule/prometheusrule.yaml || true kubectl delete --ignore-not-found=true customresourcedefinition dash0monitorings.operator.dash0.com kubectl delete --ignore-not-found=true customresourcedefinition dash0operatorconfigurations.operator.dash0.com diff --git a/test-resources/bin/test-scenario-01-aum-operator-cr.sh b/test-resources/bin/test-scenario-01-aum-operator-cr.sh index 25fefdb7..ddc0872a 100755 --- a/test-resources/bin/test-scenario-01-aum-operator-cr.sh +++ b/test-resources/bin/test-scenario-01-aum-operator-cr.sh @@ -5,7 +5,7 @@ set -euo pipefail -cd "$(dirname ${BASH_SOURCE})"/../.. +cd "$(dirname "${BASH_SOURCE[0]}")"/../.. target_namespace=${1:-test-namespace} kind=${2:-deployment} @@ -18,11 +18,11 @@ setup_test_environment step_counter=1 echo "STEP $step_counter: remove old test resources" -test-resources/bin/test-cleanup.sh ${target_namespace} false +test-resources/bin/test-cleanup.sh "${target_namespace}" false finish_step echo "STEP $step_counter: creating target namespace (if necessary)" -test-resources/bin/ensure-namespace-exists.sh ${target_namespace} +test-resources/bin/ensure-namespace-exists.sh "${target_namespace}" finish_step echo "STEP $step_counter: creating operator namespace and authorization token secret" @@ -47,7 +47,7 @@ finish_step if [[ "${DEPLOY_APPLICATION_UNDER_MONITORING:-}" != false ]]; then echo "STEP $step_counter: deploy application under monitoring" - test-resources/node.js/express/deploy.sh ${target_namespace} ${kind} + test-resources/node.js/express/deploy.sh "${target_namespace}" "${kind}" finish_step fi diff --git a/test-resources/bin/test-scenario-02-operator-cr-aum.sh b/test-resources/bin/test-scenario-02-operator-cr-aum.sh index 57afcc21..7fee8511 100755 --- a/test-resources/bin/test-scenario-02-operator-cr-aum.sh +++ b/test-resources/bin/test-scenario-02-operator-cr-aum.sh @@ -5,7 +5,7 @@ set -euo pipefail -cd "$(dirname ${BASH_SOURCE})"/../.. +cd "$(dirname "${BASH_SOURCE[0]}")"/../.. target_namespace=${1:-test-namespace} kind=${2:-deployment} @@ -18,11 +18,11 @@ setup_test_environment step_counter=1 echo "STEP $step_counter: remove old test resources" -test-resources/bin/test-cleanup.sh ${target_namespace} false +test-resources/bin/test-cleanup.sh "${target_namespace}" false finish_step echo "STEP $step_counter: creating target namespace (if necessary)" -test-resources/bin/ensure-namespace-exists.sh ${target_namespace} +test-resources/bin/ensure-namespace-exists.sh "${target_namespace}" finish_step echo "STEP $step_counter: creating operator namespace and authorization token secret" @@ -67,7 +67,7 @@ fi if [[ "${DEPLOY_APPLICATION_UNDER_MONITORING:-}" != false ]]; then echo "STEP $step_counter: deploy application under monitoring" - test-resources/node.js/express/deploy.sh ${target_namespace} ${kind} + test-resources/node.js/express/deploy.sh "${target_namespace}" "${kind}" finish_step fi diff --git a/test-resources/node.js/express/deploy.sh b/test-resources/node.js/express/deploy.sh index 15af21b8..d264de45 100755 --- a/test-resources/node.js/express/deploy.sh +++ b/test-resources/node.js/express/deploy.sh @@ -5,7 +5,7 @@ set -euo pipefail -cd "$(dirname ${BASH_SOURCE})" +cd "$(dirname "${BASH_SOURCE[0]}")" target_namespace=${1:-test-namespace} kind=${2:-deployment} @@ -18,5 +18,5 @@ if [[ -f ${kind}.yaml ]]; then ../../bin/render-templates.sh fi -./undeploy.sh ${target_namespace} ${kind} -kubectl apply -n ${target_namespace} -f ${kind}.yaml +./undeploy.sh "${target_namespace}" "${kind}" +kubectl apply -n "${target_namespace}" -f "${kind}".yaml diff --git a/test-resources/node.js/express/undeploy.sh b/test-resources/node.js/express/undeploy.sh index df774730..55b11044 100755 --- a/test-resources/node.js/express/undeploy.sh +++ b/test-resources/node.js/express/undeploy.sh @@ -5,9 +5,9 @@ set -euo pipefail -cd "$(dirname ${BASH_SOURCE})" +cd "$(dirname "${BASH_SOURCE[0]}")" target_namespace=${1:-test-namespace} kind=${2:-deployment} -kubectl delete -n ${target_namespace} -f ${kind}.yaml --ignore-not-found || true +kubectl delete -n "${target_namespace}" -f "${kind}".yaml --ignore-not-found || true From b0367bb936a3df97df9ba67b99b3e37d671aa2e5 Mon Sep 17 00:00:00 2001 From: Bastian Krol Date: Wed, 23 Oct 2024 11:25:29 +0200 Subject: [PATCH 7/8] test(instrumentation): use multi-platform instrumentation image in tests --- .../instrumentation/injector/test/README.md | 2 + .../test/scripts/run-tests-for-container.sh | 6 +- .../injector/test/scripts/test-all.sh | 55 ++++++++++--------- images/instrumentation/test/test-all.sh | 47 ++++++++-------- 4 files changed, 58 insertions(+), 52 deletions(-) diff --git a/images/instrumentation/injector/test/README.md b/images/instrumentation/injector/test/README.md index 52e16eba..82bf3541 100644 --- a/images/instrumentation/injector/test/README.md +++ b/images/instrumentation/injector/test/README.md @@ -1,2 +1,4 @@ This directory contains isolated tests for the injector code. The difference to images/instrumentation/test is that the latter tests the whole instrumentation image. +Also, the tests in this folder do not use multi-platform images, an injector binary is build (in a container) per CPU +architecture, and then used for testing. diff --git a/images/instrumentation/injector/test/scripts/run-tests-for-container.sh b/images/instrumentation/injector/test/scripts/run-tests-for-container.sh index 4d05fc72..a5a0f489 100755 --- a/images/instrumentation/injector/test/scripts/run-tests-for-container.sh +++ b/images/instrumentation/injector/test/scripts/run-tests-for-container.sh @@ -49,9 +49,9 @@ if [ "${INTERACTIVE:-}" = "true" ]; then fi echo -echo --------------------------------------- -echo "testing the library on $ARCH and $LIBC" -echo --------------------------------------- +echo ---------------------------------------- +echo "testing the injector library on $ARCH and $LIBC" +echo ---------------------------------------- docker rm -f "$container_name" 2> /dev/null docker rmi -f "$image_name" 2> /dev/null diff --git a/images/instrumentation/injector/test/scripts/test-all.sh b/images/instrumentation/injector/test/scripts/test-all.sh index 0e792d57..8d89d75c 100755 --- a/images/instrumentation/injector/test/scripts/test-all.sh +++ b/images/instrumentation/injector/test/scripts/test-all.sh @@ -9,17 +9,41 @@ set -eu RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' +dockerfile_injector_build=injector/test/docker/Dockerfile-build cd "$(dirname "${BASH_SOURCE[0]}")"/../../.. # remove all outdated injector binaries rm -rf injector/test/bin/* +exit_code=0 +summary="" +run_tests_for_architecture_and_libc_flavor() { + arch=$1 + libc=$2 + set +e + ARCH=$arch LIBC=$libc injector/test/scripts/run-tests-for-container.sh + test_exit_code=$? + set -e + echo + echo ---------------------------------------- + if [ $test_exit_code != 0 ]; then + printf "${RED}tests for %s/%s failed (see above for details)${NC}\n" "$arch" "$libc" + exit_code=1 + summary="$summary\n$arch/$libc:\t${RED}failed${NC}" + else + printf "${GREEN}tests for %s/%s were successful${NC}\n" "$arch" "$libc" + summary="$summary\n$arch/$libc:\t${GREEN}ok${NC}" + fi + echo ---------------------------------------- + echo +} + # Create a Dockerfile for building the injector in a container by re-using an excerpt from the ../Dockerfile. This makes # sure we keep the way we build the injector binary here for this test suite and for actual production usage in the # instrumentation image in sync. copy_lines=false -rm -f injector/test/docker/Dockerfile-build +rm -f "$dockerfile_injector_build" while IFS= read -r line; do if [[ "$line" =~ ^[[:space:]]*#.* ]]; then # skip comments @@ -32,12 +56,12 @@ while IFS= read -r line; do copy_lines=true fi if [[ $copy_lines == true ]]; then - echo "$line" >> injector/test/docker/Dockerfile-build + echo "$line" >> "$dockerfile_injector_build" fi done < Dockerfile -if [[ ! -e injector/test/docker/Dockerfile-build ]]; then - printf "\nError: The file injector/test/docker/Dockerfile-build has not been generated, stopping." +if [[ ! -e "$dockerfile_injector_build" ]]; then + printf "\nError: The file $dockerfile_injector_build has not been generated, stopping." exit 1 fi @@ -45,29 +69,6 @@ fi ARCH=arm64 injector/test/scripts/build-in-container.sh ARCH=x86_64 injector/test/scripts/build-in-container.sh -exit_code=0 -summary="" -run_tests_for_architecture_and_libc_flavor() { - arch=$1 - libc=$2 - set +e - ARCH=$arch LIBC=$libc injector/test/scripts/run-tests-for-container.sh - test_exit_code=$? - set -e - echo - echo --------------------------------------- - if [ $test_exit_code != 0 ]; then - printf "${RED}tests for %s/%s failed (see above for details)${NC}\n" "$arch" "$libc" - exit_code=1 - summary="$summary\n$arch/$libc:\tfailed" - else - printf "${GREEN}tests for %s/%s were successful${NC}\n" "$arch" "$libc" - summary="$summary\n$arch/$libc:\tok" - fi - echo --------------------------------------- - echo -} - run_tests_for_architecture_and_libc_flavor arm64 glibc run_tests_for_architecture_and_libc_flavor x86_64 glibc run_tests_for_architecture_and_libc_flavor arm64 musl diff --git a/images/instrumentation/test/test-all.sh b/images/instrumentation/test/test-all.sh index 91ea4eb4..8c829786 100755 --- a/images/instrumentation/test/test-all.sh +++ b/images/instrumentation/test/test-all.sh @@ -13,32 +13,20 @@ NC='\033[0m' cd "$(dirname "${BASH_SOURCE[0]}")"/.. +instrumentation_image="dash0-instrumentation:latest" +all_docker_platforms=linux/arm64,linux/amd64 script_dir="test" exit_code=0 summary="" build_instrumentation_image() { - instrumentation_image=${1:-} - arch=${2:-} - docker_platform=${3:-} + echo ---------------------------------------- + echo "building multi-arch instrumentation image for platforms ${all_docker_platforms}" + echo ---------------------------------------- - if [[ -z ${instrumentation_image} ]]; then - echo "missing mandatory argument: instrumentation_image" - exit 1 - fi - if [[ -z ${arch} ]]; then - echo "missing mandatory argument: architecture" - exit 1 - fi - if [[ -z ${docker_platform} ]]; then - echo "missing mandatory argument: docker_platform" - exit 1 - fi - - echo "building instrumentation image \"${instrumentation_image} for architecture ${arch}" if ! build_output=$( docker build \ - --platform "$docker_platform" \ + --platform "$all_docker_platforms" \ . \ -t "${instrumentation_image}" \ 2>&1 @@ -46,6 +34,7 @@ build_instrumentation_image() { echo "${build_output}" exit 1 fi + echo } run_tests_for_runtime() { @@ -96,13 +85,16 @@ run_tests_for_architecture() { exit 1 fi - instrumentation_image="dash0-instrumentation-$arch:latest" - build_instrumentation_image "$instrumentation_image" "$arch" "$docker_platform" + echo ---------------------------------------- + echo "running tests for architecture $arch" + echo ---------------------------------------- + for r in "${script_dir}"/*/ ; do runtime=$(basename "$(realpath "${r}")") - echo "runtime '${runtime}'" + echo "- runtime: '${runtime}'" + echo grep '^[^#;]' "${script_dir}/${runtime}/base-images" | while read -r base_image ; do - echo "base image '${base_image}'" + echo "- base image: '${base_image}'" image_name_test="test-${runtime}-${arch}:latest" echo "building test image for ${arch}/${runtime}/${base_image} with instrumentation image ${instrumentation_image}" if ! build_output=$( @@ -118,10 +110,21 @@ run_tests_for_architecture() { exit 1 fi run_tests_for_runtime "${runtime}" "$image_name_test" "$base_image" + echo done done + echo + echo } +dockerDriver="$(docker info -f '{{ .DriverStatus }}')" +if [[ "$dockerDriver" != *"io.containerd."* ]]; then + echo "Error: This script requires that the containerd image store is enabled for Docker, since the script needs to build and use multi-arch images locally. You driver is $dockerDriver. Please see https://docs.docker.com/desktop/containerd/#enable-the-containerd-image-store for instructions on enabling the containerd image store." + exit 1 +fi + +build_instrumentation_image + run_tests_for_architecture arm64 run_tests_for_architecture x86_64 From 71cd000a9607a7486acec3c471119e02cfab3a22 Mon Sep 17 00:00:00 2001 From: Bastian Krol Date: Wed, 23 Oct 2024 15:15:51 +0200 Subject: [PATCH 8/8] ci(injector,instrumentation): run injector&instrumentation tests on CI Also: Enable both tests to use an existing (local or remote) instrumentation image instead of building the instrumentation image from local sources. --- .github/actions/build-image/action.yaml | 13 -- .github/workflows/ci.yaml | 135 +++++++++++++++--- images/instrumentation/Dockerfile | 8 +- .../injector/test/app/index.js | 4 +- .../test/scripts/build-in-container.sh | 9 +- .../test/scripts/run-tests-for-container.sh | 1 - .../scripts/run-tests-within-container.sh | 6 +- .../injector/test/scripts/test-all.sh | 35 ++++- .../injector/test/scripts/util | 40 ++++++ images/instrumentation/test/test-all.sh | 56 +++++--- test-resources/bin/run-gh-action-locally.sh | 31 ++++ 11 files changed, 270 insertions(+), 68 deletions(-) create mode 100644 images/instrumentation/injector/test/scripts/util create mode 100755 test-resources/bin/run-gh-action-locally.sh diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index c319809a..99bbd037 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -5,12 +5,6 @@ inputs: githubToken: description: "github token" required: true - # dockerhubUsername: - # description: "Dockerhub username" - # required: true - # dockerhubToken: - # description: "Dockerhub token" - # required: true imageName: description: "name of the container image (without registry prefix)" required: true @@ -38,12 +32,6 @@ runs: - name: set up docker buildx uses: docker/setup-buildx-action@v3 - # - name: dockerhub login - # uses: docker/login-action@v3 - # with: - # username: ${{ inputs.dockerhubUsername }} - # password: ${{ inputs.dockerhubToken }} - - name: login to GitHub container registry uses: docker/login-action@v3 with: @@ -55,7 +43,6 @@ runs: id: meta uses: docker/metadata-action@v5 with: - # add dash0hq/${{ inputs.imageName }} to images once Dockerhub has been set up images: | ghcr.io/dash0hq/${{ inputs.imageName }} tags: | diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d435a37c..8af4b524 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,16 +13,19 @@ on: - '*.md' workflow_dispatch: +concurrency: + group: ci-concurrency-group-${{ github.ref }} + cancel-in-progress: true + +env: + instrumentation_image_test_tag: ghcr.io/dash0hq/instrumentation-ci-test:${{ github.head_ref || github.ref_name }} + jobs: verify: name: Build & Test runs-on: ubuntu-latest timeout-minutes: 15 - concurrency: - group: ci-concurrency-group-${{ github.ref }} - cancel-in-progress: true - steps: - uses: actions/checkout@v4 @@ -59,6 +62,116 @@ jobs: run: | make test + injector_binary_and_instrumentation_image_tests: + name: Injector Binary & Instrumentation Image Tests + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + + - name: get branch name + id: branch-name + uses: tj-actions/branch-names@v8 + + - name: find SHA of last successful workflow run on main branch + uses: nrwl/nx-set-shas@v4 + id: last_succsesfull_commit_main_branch + with: + # Get the last successful commit on main branch (actually, on the target branch for the PR, but that is + # usually main). + main-branch-name: ${{ steps.branch-name.outputs.base_ref_branch }} + workflow_id: 'ci.yml' + + # We use the changed-files action to potentially skip the injector & instrumentation tests on PRs that contain no + # changes for the instrumentation image. This is because running the tests requires to build the instrumentation + # image for both arm64 and X86_64, and the cross-platform build is very slow (takes up to 15 minutes). We do + # always run these steps when building the main branch or a tag though. By default, changed-files would compare + # against the last non-merge commit on the target branch for pull request events (which is used in PR builds), but + # together with the nrwl/nx-set-shas step from above we compare against the SHA from the last _successful_ CI + # workflow run on the main branch. + - name: compile list of relevant changed files for the instrumentation image + id: changed-files + uses: tj-actions/changed-files@v44 + with: + base_sha: ${{ steps.last_succsesfull_commit_main_branch.outputs.base }} + files_yaml: | + instrumentation: + - .github/workflows/ci.yaml + - images/instrumentation/** + + - name: show changed files + env: + INSTRUMENTATION_CHANGED_FILES_FLAG: ${{ steps.changed-files.outputs.instrumentation_any_changed }} + INSTRUMENTATION_CHANGED_FILES_LIST: ${{ steps.changed-files.outputs.instrumentation_all_changed_files }} + run: | + echo "files for instrumentation image have changed: $INSTRUMENTATION_CHANGED_FILES_FLAG" + echo "changed files for instrumentation image: $INSTRUMENTATION_CHANGED_FILES_LIST" + + - name: set up docker buildx + uses: docker/setup-buildx-action@v3 + if: | + steps.changed-files.outputs.instrumentation_any_changed == 'true' || + github.ref == 'refs/heads/main' || contains(github.ref, 'refs/tags/') + + # Just for building on arm, buildx is enough but doing docker run with --platform=linux/arm64 (which we do when + # testing the injector binary and the instrumentation image) requires qemu. + - name: set up qemu + uses: docker/setup-qemu-action@v3 + if: | + steps.changed-files.outputs.instrumentation_any_changed == 'true' || + github.ref == 'refs/heads/main' || contains(github.ref, 'refs/tags/') + + - name: login to GitHub container registry + uses: docker/login-action@v3 + if: | + steps.changed-files.outputs.instrumentation_any_changed == 'true' || + github.ref == 'refs/heads/main' || contains(github.ref, 'refs/tags/') + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: build temporary instrumentation image + uses: docker/build-push-action@v6 + if: | + steps.changed-files.outputs.instrumentation_any_changed == 'true' || + github.ref == 'refs/heads/main' || contains(github.ref, 'refs/tags/') + with: + context: images/instrumentation + tags: ${{ env.instrumentation_image_test_tag }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha,scope=instrumentation + cache-to: type=gha,mode=max,scope=instrumentation + push: true + + - name: injector tests + if: | + steps.changed-files.outputs.instrumentation_any_changed == 'true' || + github.ref == 'refs/heads/main' || contains(github.ref, 'refs/tags/') + env: + INSTRUMENTATION_IMAGE: ${{ env.instrumentation_image_test_tag }} + run: | + images/instrumentation/injector/test/scripts/test-all.sh + + - name: instrumentation image tests + if: | + steps.changed-files.outputs.instrumentation_any_changed == 'true' || + github.ref == 'refs/heads/main' || contains(github.ref, 'refs/tags/') + env: + INSTRUMENTATION_IMAGE: ${{ env.instrumentation_image_test_tag }} + run: | + images/instrumentation/test/test-all.sh + + - name: delete test image + uses: bots-house/ghcr-delete-image-action@v1.1.0 + if: ${{ always() && ( steps.changed-files.outputs.instrumentation_any_changed == 'true' || github.ref == 'refs/heads/main' || contains(github.ref, 'refs/tags/') ) }} + with: + owner: dash0hq + name: instrumentation-ci-test + token: ${{ secrets.GITHUB_TOKEN }} + # delete untagged images from this build (and from earlier builds, if there are any leftovers) + untagged-keep-latest: 1 + # Builds and potentially pushes all container images. For pushes to PRs/branches, we simply verify that the image # build still works, the resulting image will not be pushed to any target registry. For pushes to the main branch, the # images are tagged with "main-dev", but not with a version x.y.z. Finally, for pushes to a tag (or when a tag is @@ -70,9 +183,7 @@ jobs: runs-on: ubuntu-latest needs: - verify - concurrency: - group: ci-concurrency-group-${{ github.ref }} - cancel-in-progress: true + - injector_binary_and_instrumentation_image_tests timeout-minutes: 60 steps: @@ -138,10 +249,6 @@ jobs: if: ${{ ! contains(github.ref, 'refs/tags/') && github.actor != 'dependabot[bot]'}} needs: - build-and-push-images - concurrency: - group: ci-concurrency-group-${{ github.ref }} - cancel-in-progress: true - steps: - uses: actions/checkout@v4 @@ -164,9 +271,6 @@ jobs: if: ${{ ! contains(github.ref, 'refs/tags/') && github.actor == 'dependabot[bot]'}} needs: - build-and-push-images - concurrency: - group: ci-concurrency-group-${{ github.ref }} - cancel-in-progress: true steps: - name: skipping publish helm chart (dry run) @@ -179,9 +283,6 @@ jobs: if: ${{ contains(github.ref, 'refs/tags/') && github.actor != 'dependabot[bot]'}} needs: - build-and-push-images - concurrency: - group: ci-concurrency-group-${{ github.ref }} - cancel-in-progress: true steps: - uses: actions/checkout@v4 diff --git a/images/instrumentation/Dockerfile b/images/instrumentation/Dockerfile index 84227c27..70e1d255 100644 --- a/images/instrumentation/Dockerfile +++ b/images/instrumentation/Dockerfile @@ -6,15 +6,15 @@ RUN apt-get update && \ apt-get autoremove -y && \ apt-get clean -y -COPY ./injector /dash0-init-container/injector -WORKDIR /dash0-init-container/injector +COPY ./injector /dash0-init-container +WORKDIR /dash0-init-container RUN gcc \ -shared \ -nostdlib \ -fPIC \ -Wl,--version-script=src/dash0_injector.exports.map \ src/dash0_injector.c \ - -o bin/dash0_injector.so + -o dash0_injector.so # build Node.js artifacts FROM node:20.13.1-alpine3.19 AS build-node.js @@ -35,7 +35,7 @@ COPY copy-instrumentation.sh / # copy artifacts (distros, injector binary) from the build stages to the final image RUN mkdir -p /dash0-init-container/instrumentation -COPY --from=build-injector /dash0-init-container/injector/bin/dash0_injector.so /dash0-init-container/dash0_injector.so +COPY --from=build-injector /dash0-init-container/dash0_injector.so /dash0-init-container/dash0_injector.so COPY --from=build-node.js /dash0-init-container/instrumentation/node.js /dash0-init-container/instrumentation/node.js WORKDIR / diff --git a/images/instrumentation/injector/test/app/index.js b/images/instrumentation/injector/test/app/index.js index 49f22008..22ab2b54 100644 --- a/images/instrumentation/injector/test/app/index.js +++ b/images/instrumentation/injector/test/app/index.js @@ -23,8 +23,8 @@ function main () { case "non-existing": echoEnvVar("DOES_NOT_EXIST"); break; - case "term": - echoEnvVar("TERM"); + case "existing": + echoEnvVar("TEST_VAR"); break; case "node_options": echoEnvVar("NODE_OPTIONS"); diff --git a/images/instrumentation/injector/test/scripts/build-in-container.sh b/images/instrumentation/injector/test/scripts/build-in-container.sh index ff138ae7..b2ee007c 100755 --- a/images/instrumentation/injector/test/scripts/build-in-container.sh +++ b/images/instrumentation/injector/test/scripts/build-in-container.sh @@ -7,6 +7,9 @@ set -eu cd "$(dirname "${BASH_SOURCE[0]}")"/../../.. +# shellcheck source=images/instrumentation/injector/test/scripts/util +source injector/test/scripts/util + if [ -z "${ARCH:-}" ]; then ARCH=arm64 fi @@ -36,9 +39,5 @@ docker build \ -f "$dockerfile_name" \ -t "$image_name" -container_id=$(docker create "$image_name") -docker container cp \ - "$container_id":/dash0-init-container/injector/bin/dash0_injector.so \ - injector/test/bin/dash0_injector_"$ARCH".so -docker rm -v "$container_id" +copy_injector_binary_from_container_image "$image_name" "$ARCH" "$docker_platform" diff --git a/images/instrumentation/injector/test/scripts/run-tests-for-container.sh b/images/instrumentation/injector/test/scripts/run-tests-for-container.sh index a5a0f489..3c95f093 100755 --- a/images/instrumentation/injector/test/scripts/run-tests-for-container.sh +++ b/images/instrumentation/injector/test/scripts/run-tests-for-container.sh @@ -69,7 +69,6 @@ docker run \ --platform "$docker_platform" \ --env EXPECTED_CPU_ARCHITECTURE="$expected_cpu_architecture" \ --name "$container_name" \ - -it \ "$image_name" \ $docker_run_extra_arguments { set +x; } 2> /dev/null diff --git a/images/instrumentation/injector/test/scripts/run-tests-within-container.sh b/images/instrumentation/injector/test/scripts/run-tests-within-container.sh index bc3eb204..472e545e 100755 --- a/images/instrumentation/injector/test/scripts/run-tests-within-container.sh +++ b/images/instrumentation/injector/test/scripts/run-tests-within-container.sh @@ -44,9 +44,9 @@ run_test_case() { existing_node_options_value=${4:-} set +e if [ "$existing_node_options_value" != "" ]; then - test_output=$(LD_PRELOAD="$injector_binary" NODE_OPTIONS="$existing_node_options_value" node index.js "$command") + test_output=$(LD_PRELOAD="$injector_binary" TEST_VAR=value NODE_OPTIONS="$existing_node_options_value" node index.js "$command") else - test_output=$(LD_PRELOAD="$injector_binary" node index.js "$command") + test_output=$(LD_PRELOAD="$injector_binary" TEST_VAR=value node index.js "$command") fi test_exit_code=$? set -e @@ -70,7 +70,7 @@ exit_code=0 cd app run_test_case "getenv: returns undefined for non-existing environment variable" non-existing "DOES_NOT_EXIST: -" -run_test_case "getenv: returns environment variable unchanged" term "TERM: xterm" +run_test_case "getenv: returns environment variable unchanged" existing "TEST_VAR: value" run_test_case "getenv: overrides NODE_OPTIONS if it is not present" node_options "NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry" run_test_case "getenv: ask for NODE_OPTIONS (unset) twice" node_options_twice "NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry; NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry" run_test_case "getenv: prepends to NODE_OPTIONS if it is present" node_options "NODE_OPTIONS: --require /__dash0__/instrumentation/node.js/node_modules/@dash0hq/opentelemetry --no-deprecation" "--no-deprecation" diff --git a/images/instrumentation/injector/test/scripts/test-all.sh b/images/instrumentation/injector/test/scripts/test-all.sh index 8d89d75c..6cfedb66 100755 --- a/images/instrumentation/injector/test/scripts/test-all.sh +++ b/images/instrumentation/injector/test/scripts/test-all.sh @@ -13,6 +13,9 @@ dockerfile_injector_build=injector/test/docker/Dockerfile-build cd "$(dirname "${BASH_SOURCE[0]}")"/../../.. +# shellcheck source=images/instrumentation/injector/test/scripts/util +source injector/test/scripts/util + # remove all outdated injector binaries rm -rf injector/test/bin/* @@ -65,15 +68,37 @@ if [[ ! -e "$dockerfile_injector_build" ]]; then exit 1 fi -# build injector binary for both architectures -ARCH=arm64 injector/test/scripts/build-in-container.sh -ARCH=x86_64 injector/test/scripts/build-in-container.sh +instrumentation_image=${INSTRUMENTATION_IMAGE:-} +if [[ -z "$instrumentation_image" ]]; then + # build injector binary for both architectures + echo ---------------------------------------- + echo building the injector binary locally from source + echo ---------------------------------------- + ARCH=arm64 injector/test/scripts/build-in-container.sh + ARCH=x86_64 injector/test/scripts/build-in-container.sh +else + if is_remote_image "$instrumentation_image"; then + echo ---------------------------------------- + printf "using injector binary from existing remote image:\n$instrumentation_image\n" + echo ---------------------------------------- + docker pull --platform linux/arm64 "$instrumentation_image" + copy_injector_binary_from_container_image "$instrumentation_image" arm64 linux/arm64 + docker pull --platform linux/amd64 "$instrumentation_image" + copy_injector_binary_from_container_image "$instrumentation_image" x86_64 linux/amd64 + else + echo ---------------------------------------- + printf "using injector binary from existing local image:\n$instrumentation_image\n" + echo ---------------------------------------- + copy_injector_binary_from_container_image "$instrumentation_image" arm64 linux/arm64 + copy_injector_binary_from_container_image "$instrumentation_image" x86_64 linux/amd64 + fi +fi +echo run_tests_for_architecture_and_libc_flavor arm64 glibc run_tests_for_architecture_and_libc_flavor x86_64 glibc run_tests_for_architecture_and_libc_flavor arm64 musl run_tests_for_architecture_and_libc_flavor x86_64 musl -printf "$summary\n" +printf "$summary\n\n" exit $exit_code - diff --git a/images/instrumentation/injector/test/scripts/util b/images/instrumentation/injector/test/scripts/util new file mode 100644 index 00000000..91b2c8d0 --- /dev/null +++ b/images/instrumentation/injector/test/scripts/util @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + +is_remote_image() { + image_name=${1:-} + if [[ -z "$image_name" ]]; then + echo "error: mandatory argument \"image_name\" is missing" + exit 1 + fi + + if [[ "$image_name" == *"/"* ]]; then + return 0 + else + return 1 + fi +} + +copy_injector_binary_from_container_image() { + image_name=${1:-} + if [[ -z "$image_name" ]]; then + echo "error: mandatory argument \"image_name\" is missing" + exit 1 + fi + arch=${2:-} + if [[ -z "$arch" ]]; then + echo "error: mandatory argument \"arch\" is missing" + exit 1 + fi + docker_platform=${3:-} + if [[ -z "$docker_platform" ]]; then + echo "error: mandatory argument \"docker_platform\" is missing" + exit 1 + fi + + container_id=$(docker create --platform "$docker_platform" "$image_name") + docker container cp \ + "$container_id":/dash0-init-container/dash0_injector.so \ + injector/test/bin/dash0_injector_"$arch".so + docker rm -v "$container_id" +} diff --git a/images/instrumentation/test/test-all.sh b/images/instrumentation/test/test-all.sh index 8c829786..f10e4ed7 100755 --- a/images/instrumentation/test/test-all.sh +++ b/images/instrumentation/test/test-all.sh @@ -13,26 +13,44 @@ NC='\033[0m' cd "$(dirname "${BASH_SOURCE[0]}")"/.. +# shellcheck source=images/instrumentation/injector/test/scripts/util +source injector/test/scripts/util + instrumentation_image="dash0-instrumentation:latest" all_docker_platforms=linux/arm64,linux/amd64 script_dir="test" exit_code=0 summary="" -build_instrumentation_image() { - echo ---------------------------------------- - echo "building multi-arch instrumentation image for platforms ${all_docker_platforms}" - echo ---------------------------------------- +build_or_pull_instrumentation_image() { + if [[ -n "${INSTRUMENTATION_IMAGE:-}" ]]; then + instrumentation_image="$INSTRUMENTATION_IMAGE" - if ! build_output=$( - docker build \ - --platform "$all_docker_platforms" \ - . \ - -t "${instrumentation_image}" \ - 2>&1 - ); then - echo "${build_output}" - exit 1 + if is_remote_image "$instrumentation_image"; then + echo ---------------------------------------- + echo "fetching instrumentation image from remote repository: $instrumentation_image" + echo ---------------------------------------- + docker pull "$instrumentation_image" + else + echo ---------------------------------------- + echo "using existing local instrumentation image: $instrumentation_image" + echo ---------------------------------------- + fi + else + echo ---------------------------------------- + echo "building multi-arch instrumentation image for platforms ${all_docker_platforms} from local sources" + echo ---------------------------------------- + + if ! build_output=$( + docker build \ + --platform "$all_docker_platforms" \ + . \ + -t "${instrumentation_image}" \ + 2>&1 + ); then + echo "${build_output}" + exit 1 + fi fi echo } @@ -117,13 +135,15 @@ run_tests_for_architecture() { echo } -dockerDriver="$(docker info -f '{{ .DriverStatus }}')" -if [[ "$dockerDriver" != *"io.containerd."* ]]; then - echo "Error: This script requires that the containerd image store is enabled for Docker, since the script needs to build and use multi-arch images locally. You driver is $dockerDriver. Please see https://docs.docker.com/desktop/containerd/#enable-the-containerd-image-store for instructions on enabling the containerd image store." - exit 1 +if [[ "${CI:-false}" != true ]]; then + dockerDriver="$(docker info -f '{{ .DriverStatus }}')" + if [[ "$dockerDriver" != *"io.containerd."* ]]; then + echo "Error: This script requires that the containerd image store is enabled for Docker, since the script needs to build and use multi-arch images locally. You driver is $dockerDriver. Please see https://docs.docker.com/desktop/containerd/#enable-the-containerd-image-store for instructions on enabling the containerd image store." + exit 1 + fi fi -build_instrumentation_image +build_or_pull_instrumentation_image run_tests_for_architecture arm64 run_tests_for_architecture x86_64 diff --git a/test-resources/bin/run-gh-action-locally.sh b/test-resources/bin/run-gh-action-locally.sh new file mode 100755 index 00000000..9d523dc3 --- /dev/null +++ b/test-resources/bin/run-gh-action-locally.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")"/../.. + +command -v act >/dev/null 2>&1 || { + cat <&2 +The executable act needs to be installed but it isn't. +See https://nektosact.com/installation/index.html for instructions. + +Aborting. +EOF + exit 1 +} + +workflow="${1:-.github/workflows/ci.yaml}" +job="${2:-verify}" +trigger="${3:-push}" + +set -x +act \ + --container-architecture linux/arm64 \ + -W "$workflow" \ + -j "$job" \ + "$trigger" +set +x +