diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..75ba30a --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,17 @@ +FROM node:22 + +ARG USERNAME=node + +# Set `DEVCONTAINER` environment variable to help with orientation +ENV DEVCONTAINER=true + +RUN \ + apt update \ + && apt install -y sudo jq shellcheck rsync \ + # Ensure default `node` user has access to `sudo` + && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ + && chmod 0440 /etc/sudoers.d/$USERNAME \ + # Install tool for devcontainer template development + && npm install --global @devcontainers/cli + +USER $USERNAME \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..2794e32 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,17 @@ +{ + "build": { + "dockerfile": "Dockerfile" + }, + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {} + }, + "postCreateCommand": "npm install", + "customizations": { + "vscode": { + "extensions": [ + "mads-hartmann.bash-ide-vscode", + "redhat.vscode-yaml" + ] + } + } +} \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..0c20645 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8ce87f7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 The Nefarious Developer + +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/README.md b/README.md new file mode 100644 index 0000000..cb76799 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Nefarious Development Container Templates diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6892ccb --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "devcontainer-templates", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9f3663c --- /dev/null +++ b/package.json @@ -0,0 +1,48 @@ +{ + "name": "devcontainer-templates", + "version": "1.0.0", + "description": "Nefarious Development Container Templates", + "author": "Nicholas Checan", + "license": "MIT", + "homepage": "https://github.com/The-Nefarious-Developer/devcontainer-temmplates#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/The-Nefarious-Developer/devcontainer-templates.git" + }, + "bugs": { + "url": "https://github.com/The-Nefarious-Developer/devcontainer-templates/issues" + }, + "devDependencies": { + "@devcontainers/cli": "^0.71.0", + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/commit-analyzer": "^13.0.0", + "@semantic-release/github": "^11.0.0", + "@semantic-release/release-notes-generator": "^14.0.1", + "semantic-release": "^24.1.2" + }, + "release": { + "branches": [ "main" ], + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + "@semantic-release/changelog", + [ + "@semantic-release/github", + { + "assets": [ + { + "path": "CHANGELOG.md", + "label": "Changelog" + } + ] + } + ] + ] + }, + "scripts": { + "build": "scripts/build.sh", + "semantic-release": "semantic-release", + "test:semantic-release": "semantic-release --dry-run --debug", + "test:local": "VARIANT=22-bookworm test/sap-cap-javascript-node/test.sh" + } +} \ No newline at end of file diff --git a/src/sap-cap-javascript-node/.devcontainer/devcontainer.json b/src/sap-cap-javascript-node/.devcontainer/devcontainer.json new file mode 100644 index 0000000..8489578 --- /dev/null +++ b/src/sap-cap-javascript-node/.devcontainer/devcontainer.json @@ -0,0 +1,21 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +{ + "name": "CAP", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "ghcr.io/the-nefarious-developer/sap-cap-javascript-node:${templateOption:imageVariant}" + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "yarn install", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} \ No newline at end of file diff --git a/src/sap-cap-javascript-node/NOTES.md b/src/sap-cap-javascript-node/NOTES.md new file mode 100644 index 0000000..dd41412 --- /dev/null +++ b/src/sap-cap-javascript-node/NOTES.md @@ -0,0 +1,4 @@ +This template references an image that was [pre-built](https://containers.dev/implementors/reference/#prebuilding) to automatically include needed devcontainer.json metadata. + +* **Image**: ghcr.io/the-nefarious-developer/sap-cap-javascript-node:20-bullseye ([source](https://github.com/The-Nefarious-Developer/devcontainer-images)) +* **Applies devcontainer.json contents from image**: Yes ([source](https://github.com/The-Nefarious-Developer/devcontainer-images/blob/main/src/sap-cap-javascript-node/.devcontainer/devcontainer.json)) \ No newline at end of file diff --git a/src/sap-cap-javascript-node/devcontainer-template.json b/src/sap-cap-javascript-node/devcontainer-template.json new file mode 100644 index 0000000..fcb5906 --- /dev/null +++ b/src/sap-cap-javascript-node/devcontainer-template.json @@ -0,0 +1,30 @@ +{ + "id": "javascript-node", + "version": "1.0.0", + "name": "SAP CAP", + "description": "Develop CAP based projects for the SAP BTP Cloud Foundry runtime.", + "publisher": "The Nefarious Developer", + "options": { + "imageVariant": { + "type": "string", + "description": "Upstream Node version (use -bookworm, -bullseye variants on local arm64/Apple Silicon):", + "proposals": [ + "22-bookworm", + "22-bullseye", + "20-bookworm", + "20-bullseye", + "18-bookworm", + "20-bullseye", + "18-bullseye" + ], + "default": "22-bookworm" + } + }, + "platforms": [ + "Node.js", + "JavaScript", + "SAP", + "BTP", + "CloudFoundry" + ] +} \ No newline at end of file diff --git a/test/sap-cap-javascript-node/test.sh b/test/sap-cap-javascript-node/test.sh new file mode 100755 index 0000000..acde0bb --- /dev/null +++ b/test/sap-cap-javascript-node/test.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail +# shellcheck disable=SC1091 +source "$(dirname "$0")/../test-utils/harness.sh" + +# setup "sap-cap-javascript-node" "22-bookworm" +setup "sap-cap-javascript-node" "$VARIANT" + +run_test "Node version is correct" "node -v" "${IMAGE_TAG:0:2}" +run_test "NPM is present" "npm --help" "npm " +run_test "CloudFoundry CLI is present" "cf --version" "cf version 8" +run_test "CAP Development Toolkit is present" "cds version" "@cap-js/asyncapi" +run_test "Container defaults to non-root user" "whoami" "node" +run_test "Non-root user is able to sudo" "sudo whoami" "root" \ No newline at end of file diff --git a/test/test-utils/harness.sh b/test/test-utils/harness.sh new file mode 100644 index 0000000..91033fc --- /dev/null +++ b/test/test-utils/harness.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +# Setup environment for tests to run (must run before anything else) +# Arguments: +# - name of the current container template being tested (e.g. `sap-cap-javascript-node`) +# - image tag (will replace `${templateOption:imageVariant}`, e.g. `22-bookworm`) +setup() { + # Set input variables + IMAGE=$1 + IMAGE_TAG=$2 + + PASSED_TESTS=0 + FAILED_TESTS=0 + + ID_LABEL="devcontainer-test=$IMAGE" + + SRC_DIR=$(dirname "$0") + TEST_ROOT=/tmp/devcontainer-templates + TEST_DIR=$TEST_ROOT/$IMAGE + + # Setup an empty test directory, ensuring test root directory exists + mkdir -p $TEST_ROOT || true + rm -rf "$TEST_DIR" + + # Copy devcontainer sources and test scripts/data to the temporary directory + cp -R "$SRC_DIR"/../../src/"$IMAGE" $TEST_ROOT/ + cp -R "$SRC_DIR"/../../test/"$IMAGE" $TEST_ROOT/ + + # Ensure the temporary directory is writable by the container + chmod -R 755 $TEST_ROOT + + # Replace the image tag from the template options. This is necessary as there is no --build-arg + # parameter on the devcontainer build command available. + # A "supporting tool" (e.g. VS Code) would normally do this as part of the interactive installation + sed -i -e "s/\${templateOption:imageVariant}/${IMAGE_TAG}/g" "$TEST_DIR"/.devcontainer/devcontainer.json + + # Start the devcontainer + npx devcontainer up --workspace-folder "$TEST_DIR" --id-label "$ID_LABEL" +} + +# Run test and evaluate result +# Arguments: +# - test description +# - command to be executed +# - expected result +run_test() { + local description=$1 + local cmd=$2 + local expected_result=$3 + + local result + # `devcontainer exec` gives its _own_ output on stdout, with the actual output of the command run + # on stderr. In theory, we could validate that the exec output includes `{"outcome": "success"}`, + # but if we get the expected output from the exec'd command back that's good enough anyway. + # Also as this script is set to abort on error, make sure we continue running even if the exit + # code of the in-container execution is non-zero. + # + # We _want_ $cmd to be split here as it could include arguments: + # shellcheck disable=SC2086 + result=$(npx devcontainer exec --workspace-folder "$TEST_DIR" --id-label "$ID_LABEL" $cmd || true) + + case "$result" in + *$expected_result*) + echo "✅ [PASS] $description" + PASSED_TESTS=$((PASSED_TESTS + 1)) + ;; + *) + echo "❌ [FAIL] $description" + echo " Expected output of '$cmd' to contain '$expected_result', but got '$result'" + FAILED_TESTS=$((FAILED_TESTS + 1)) + ;; + esac +} + +# Cleanup after success or failure +cleanup() { + # Get rid of container if running + CONTAINER=$(docker container ls -f "label=${ID_LABEL}" -q) + [[ -z "$CONTAINER" ]] || docker rm -f "$CONTAINER" > /dev/null + + # Remove test directory + rm -rf "$TEST_DIR" +} +cleanup_from_error_or_interrupt() { + cleanup + exit 1 +} +trap "cleanup_from_error_or_interrupt" ERR SIGINT + +# Print results, clean up, and return an appropriate exit code when the script finishes +end_tests() { + # Cleanup and print test output + cleanup + echo "Passed test(s): $PASSED_TESTS, failed test(s): $FAILED_TESTS" + + # Succeed if there are no failed tests, but also make sure that at least one test has passed + # (otherwise something else might have gone wrong) + [ "$FAILED_TESTS" -eq 0 ] && [ "$PASSED_TESTS" -gt 0 ] +} +trap "end_tests" EXIT \ No newline at end of file